From 1eb1a9438929fe1b814459d82b1150379c6d212e Mon Sep 17 00:00:00 2001 From: Chesars Date: Sat, 17 Jan 2026 14:41:25 -0300 Subject: [PATCH 001/171] docs: sync main page (src/pages/index.md) with docs/index.md Update the root documentation page at https://docs.litellm.ai to match the current documentation at https://docs.litellm.ai/docs/: - Update model names to latest versions (openai/gpt-5, anthropic/claude-sonnet-4-5, vertex_ai/gemini-1.5-pro) - Add xAI (grok-2-latest) and Vercel AI Gateway providers - Update "How to use LiteLLM" section with comparison table - Add Response Format sections for completions and streaming - Update endpoint descriptions to reflect current capabilities - Update exception handling example with proper litellm exceptions - Update environment variable names (VERTEXAI_PROJECT/LOCATION) --- docs/my-website/src/pages/index.md | 239 ++++++++++++++++++++++------- 1 file changed, 186 insertions(+), 53 deletions(-) diff --git a/docs/my-website/src/pages/index.md b/docs/my-website/src/pages/index.md index 91215b33c5d..296a06bd7e9 100644 --- a/docs/my-website/src/pages/index.md +++ b/docs/my-website/src/pages/index.md @@ -7,42 +7,41 @@ https://github.com/BerriAI/litellm ## **Call 100+ LLMs using the OpenAI Input/Output Format** -- Translate inputs to provider's `completion`, `embedding`, and `image_generation` endpoints -- [Consistent output](https://docs.litellm.ai/docs/completion/output), text responses will always be available at `['choices'][0]['message']['content']` +- Translate inputs to provider's endpoints (`/chat/completions`, `/responses`, `/embeddings`, `/images`, `/audio`, `/batches`, and more) +- [Consistent output](https://docs.litellm.ai/docs/supported_endpoints) - same response format regardless of which provider you use - Retry/fallback logic across multiple deployments (e.g. Azure/OpenAI) - [Router](https://docs.litellm.ai/docs/routing) - Track spend & set budgets per project [LiteLLM Proxy Server](https://docs.litellm.ai/docs/simple_proxy) ## How to use LiteLLM -You can use litellm through either: -1. [LiteLLM Proxy Server](#litellm-proxy-server-llm-gateway) - Server (LLM Gateway) to call 100+ LLMs, load balance, cost tracking across projects -2. [LiteLLM python SDK](#basic-usage) - Python Client to call 100+ LLMs, load balance, cost tracking -### **When to use LiteLLM Proxy Server (LLM Gateway)** +You can use LiteLLM through either the Proxy Server or Python SDK. Both gives you a unified interface to access multiple LLMs (100+ LLMs). Choose the option that best fits your needs: -:::tip - -Use LiteLLM Proxy Server if you want a **central service (LLM Gateway) to access multiple LLMs** - -Typically used by Gen AI Enablement / ML PLatform Teams - -::: - - - LiteLLM Proxy gives you a unified interface to access multiple LLMs (100+ LLMs) - - Track LLM Usage and setup guardrails - - Customize Logging, Guardrails, Caching per project - -### **When to use LiteLLM Python SDK** - -:::tip - - Use LiteLLM Python SDK if you want to use LiteLLM in your **python code** - -Typically used by developers building llm projects - -::: - - - LiteLLM SDK gives you a unified interface to access multiple LLMs (100+ LLMs) - - Retry/fallback logic across multiple deployments (e.g. Azure/OpenAI) - [Router](https://docs.litellm.ai/docs/routing) + + + + + + + + + + + + + + + + + + + + + + + + + +
LiteLLM Proxy ServerLiteLLM Python SDK
Use CaseCentral service (LLM Gateway) to access multiple LLMsUse LiteLLM directly in your Python code
Who Uses It?Gen AI Enablement / ML Platform TeamsDevelopers building LLM projects
Key Features• Centralized API gateway with authentication & authorization
• Multi-tenant cost tracking and spend management per project/user
• Per-project customization (logging, guardrails, caching)
• Virtual keys for secure access control
• Admin dashboard UI for monitoring and management
• Direct Python library integration in your codebase
• Router with retry/fallback logic across multiple deployments (e.g. Azure/OpenAI) - Router
• Application-level load balancing and cost tracking
• Exception handling with OpenAI-compatible errors
• Observability callbacks (Lunary, MLflow, Langfuse, etc.)
## **LiteLLM Python SDK** @@ -67,7 +66,7 @@ import os os.environ["OPENAI_API_KEY"] = "your-api-key" response = completion( - model="gpt-3.5-turbo", + model="openai/gpt-5", messages=[{ "content": "Hello, how are you?","role": "user"}] ) ``` @@ -83,13 +82,27 @@ import os os.environ["ANTHROPIC_API_KEY"] = "your-api-key" response = completion( - model="claude-2", + model="anthropic/claude-sonnet-4-5-20250929", messages=[{ "content": "Hello, how are you?","role": "user"}] ) ``` + +```python +from litellm import completion +import os + +## set ENV variables +os.environ["XAI_API_KEY"] = "your-api-key" + +response = completion( + model="xai/grok-2-latest", + messages=[{ "content": "Hello, how are you?","role": "user"}] +) +``` + ```python @@ -97,11 +110,11 @@ from litellm import completion import os # auth: run 'gcloud auth application-default' -os.environ["VERTEX_PROJECT"] = "hardy-device-386718" -os.environ["VERTEX_LOCATION"] = "us-central1" +os.environ["VERTEXAI_PROJECT"] = "hardy-device-386718" +os.environ["VERTEXAI_LOCATION"] = "us-central1" response = completion( - model="chat-bison", + model="vertex_ai/gemini-1.5-pro", messages=[{ "content": "Hello, how are you?","role": "user"}] ) ``` @@ -212,8 +225,61 @@ response = completion( + + +```python +from litellm import completion +import os + +## set ENV variables. Visit https://vercel.com/docs/ai-gateway#using-the-ai-gateway-with-an-api-key for instructions on obtaining a key +os.environ["VERCEL_AI_GATEWAY_API_KEY"] = "your-vercel-api-key" + +response = completion( + model="vercel_ai_gateway/openai/gpt-5", + messages=[{ "content": "Hello, how are you?","role": "user"}] +) +``` + + + +### Response Format (OpenAI Chat Completions Format) + +```json +{ + "id": "chatcmpl-565d891b-a42e-4c39-8d14-82a1f5208885", + "created": 1734366691, + "model": "gpt-5", + "object": "chat.completion", + "system_fingerprint": null, + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "Hello! As an AI language model, I don't have feelings, but I'm operating properly and ready to assist you with any questions or tasks you may have. How can I help you today?", + "role": "assistant", + "tool_calls": null, + "function_call": null + } + } + ], + "usage": { + "completion_tokens": 43, + "prompt_tokens": 13, + "total_tokens": 56, + "completion_tokens_details": null, + "prompt_tokens_details": { + "audio_tokens": null, + "cached_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0 + } +} +``` + ### Responses API Use `litellm.responses()` for advanced models that support reasoning content like GPT-5, o3, etc. @@ -265,11 +331,11 @@ from litellm import responses import os # auth: run 'gcloud auth application-default' -os.environ["VERTEX_PROJECT"] = "jr-smith-386718" -os.environ["VERTEX_LOCATION"] = "us-central1" +os.environ["VERTEXAI_PROJECT"] = "jr-smith-386718" +os.environ["VERTEXAI_LOCATION"] = "us-central1" response = responses( - model="chat-bison", + model="vertex_ai/gemini-1.5-pro", messages=[{ "content": "What is the capital of France?","role": "user"}] ) ``` @@ -314,7 +380,7 @@ import os os.environ["OPENAI_API_KEY"] = "your-api-key" response = completion( - model="gpt-3.5-turbo", + model="openai/gpt-5", messages=[{ "content": "Hello, how are you?","role": "user"}], stream=True, ) @@ -331,14 +397,29 @@ import os os.environ["ANTHROPIC_API_KEY"] = "your-api-key" response = completion( - model="claude-2", + model="anthropic/claude-sonnet-4-5-20250929", messages=[{ "content": "Hello, how are you?","role": "user"}], stream=True, ) ``` + +```python +from litellm import completion +import os + +## set ENV variables +os.environ["XAI_API_KEY"] = "your-api-key" + +response = completion( + model="xai/grok-2-latest", + messages=[{ "content": "Hello, how are you?","role": "user"}], + stream=True, +) +``` + ```python @@ -346,11 +427,11 @@ from litellm import completion import os # auth: run 'gcloud auth application-default' -os.environ["VERTEX_PROJECT"] = "hardy-device-386718" -os.environ["VERTEX_LOCATION"] = "us-central1" +os.environ["VERTEXAI_PROJECT"] = "hardy-device-386718" +os.environ["VERTEXAI_LOCATION"] = "us-central1" response = completion( - model="chat-bison", + model="vertex_ai/gemini-1.5-pro", messages=[{ "content": "Hello, how are you?","role": "user"}], stream=True, ) @@ -370,7 +451,7 @@ os.environ["NVIDIA_NIM_API_BASE"] = "nvidia_nim_endpoint_url" response = completion( model="nvidia_nim/", - messages=[{ "content": "Hello, how are you?","role": "user"}] + messages=[{ "content": "Hello, how are you?","role": "user"}], stream=True, ) ``` @@ -466,22 +547,74 @@ response = completion( ``` + + + +```python +from litellm import completion +import os + +## set ENV variables. Visit https://vercel.com/docs/ai-gateway#using-the-ai-gateway-with-an-api-key for instructions on obtaining a key +os.environ["VERCEL_AI_GATEWAY_API_KEY"] = "your-vercel-api-key" + +response = completion( + model="vercel_ai_gateway/openai/gpt-5", + messages = [{ "content": "Hello, how are you?","role": "user"}], + stream=True, +) +``` + + + +### Streaming Response Format (OpenAI Format) + +```json +{ + "id": "chatcmpl-2be06597-eb60-4c70-9ec5-8cd2ab1b4697", + "created": 1734366925, + "model": "claude-sonnet-4-5-20250929", + "object": "chat.completion.chunk", + "system_fingerprint": null, + "choices": [ + { + "finish_reason": null, + "index": 0, + "delta": { + "content": "Hello", + "role": "assistant", + "function_call": null, + "tool_calls": null, + "audio": null + }, + "logprobs": null + } + ] +} +``` + ### Exception handling LiteLLM maps exceptions across all supported providers to the OpenAI exceptions. All our exceptions inherit from OpenAI's exception types, so any error-handling you have for that, should work out of the box with LiteLLM. ```python -from openai.error import OpenAIError +import litellm from litellm import completion +import os os.environ["ANTHROPIC_API_KEY"] = "bad-key" try: - # some code - completion(model="claude-instant-1", messages=[{"role": "user", "content": "Hey, how's it going?"}]) -except OpenAIError as e: - print(e) + completion(model="anthropic/claude-instant-1", messages=[{"role": "user", "content": "Hey, how's it going?"}]) +except litellm.AuthenticationError as e: + # Thrown when the API key is invalid + print(f"Authentication failed: {e}") +except litellm.RateLimitError as e: + # Thrown when you've exceeded your rate limit + print(f"Rate limited: {e}") +except litellm.APIError as e: + # Thrown for general API errors + print(f"API error: {e}") ``` ### Logging Observability - Log LLM Input/Output ([Docs](https://docs.litellm.ai/docs/observability/callbacks)) @@ -502,7 +635,7 @@ os.environ["OPENAI_API_KEY"] litellm.success_callback = ["lunary", "mlflow", "langfuse", "helicone"] # log input/output to lunary, mlflow, langfuse, helicone #openai call -response = completion(model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Hi 👋 - i'm openai"}]) +response = completion(model="openai/gpt-5", messages=[{"role": "user", "content": "Hi 👋 - i'm openai"}]) ``` ### Track Costs, Usage, Latency for streaming @@ -527,7 +660,7 @@ litellm.success_callback = [track_cost_callback] # set custom callback function # litellm.completion() call response = completion( - model="gpt-3.5-turbo", + model="openai/gpt-5", messages=[ { "role": "user", @@ -584,7 +717,7 @@ Example `litellm_config.yaml` ```yaml model_list: - - model_name: gpt-3.5-turbo + - model_name: gpt-5 litellm_params: model: azure/ api_base: os.environ/AZURE_API_BASE # runs os.getenv("AZURE_API_BASE") @@ -621,7 +754,7 @@ docker run \ import openai # openai v1.0.0+ client = openai.OpenAI(api_key="anything",base_url="http://0.0.0.0:4000") # set proxy to base_url # request sent to model set on litellm proxy, `litellm --model` -response = client.chat.completions.create(model="gpt-3.5-turbo", messages = [ +response = client.chat.completions.create(model="gpt-5", messages = [ { "role": "user", "content": "this is a test request, write a short poem" From f5e46f621a7be979e5818218cc7a13b7c170006f Mon Sep 17 00:00:00 2001 From: Chesars Date: Sat, 14 Feb 2026 21:50:12 -0300 Subject: [PATCH 002/171] feat: support per-request enable_json_schema_validation for thread safety Allow passing enable_json_schema_validation as a parameter to completion() and acompletion() instead of only relying on the global litellm.enable_json_schema_validation flag. The per-request value takes priority when provided; otherwise falls back to the global (backward compatible). This makes JSON schema validation safe for concurrent usage in FastAPI and other multi-threaded environments. --- litellm/main.py | 5 + litellm/types/utils.py | 1 + litellm/utils.py | 13 +- .../test_json_schema_validation.py | 139 ++++++++++++++++++ 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 tests/litellm/litellm_core_utils/test_json_schema_validation.py diff --git a/litellm/main.py b/litellm/main.py index bca023e65ec..362e5b8b263 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -416,6 +416,8 @@ async def acompletion( # noqa: PLR0915 web_search_options: Optional[OpenAIWebSearchOptions] = None, # Session management shared_session: Optional["ClientSession"] = None, + # Per-request JSON schema validation (overrides litellm.enable_json_schema_validation) + enable_json_schema_validation: Optional[bool] = None, **kwargs, ) -> Union[ModelResponse, CustomStreamWrapper]: """ @@ -560,6 +562,7 @@ async def acompletion( # noqa: PLR0915 "thinking": thinking, "web_search_options": web_search_options, "shared_session": shared_session, + "enable_json_schema_validation": enable_json_schema_validation, } if custom_llm_provider is None: _, custom_llm_provider, _, _ = get_llm_provider( @@ -1045,6 +1048,8 @@ def completion( # type: ignore # noqa: PLR0915 thinking: Optional[AnthropicThinkingParam] = None, # Session management shared_session: Optional["ClientSession"] = None, + # Per-request JSON schema validation (overrides litellm.enable_json_schema_validation) + enable_json_schema_validation: Optional[bool] = None, **kwargs, ) -> Union[ModelResponse, CustomStreamWrapper]: """ diff --git a/litellm/types/utils.py b/litellm/types/utils.py index e1f780ffcc3..72705b82fc2 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2939,6 +2939,7 @@ all_litellm_params = ( "shared_session", "search_tool_name", "order", + "enable_json_schema_validation", ] + list(StandardCallbackDynamicParams.__annotations__.keys()) + list(CustomPricingLiteLLMParams.model_fields.keys()) diff --git a/litellm/utils.py b/litellm/utils.py index 6fdd2d88bca..0af844772b9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1318,7 +1318,18 @@ def post_call_processing( ### POST-CALL RULES ### rules_obj.post_call_rules(input=model_response, model=model) ### JSON SCHEMA VALIDATION ### - if litellm.enable_json_schema_validation is True: + # Per-request flag takes priority over global flag + _per_request_validation = ( + optional_params.get("enable_json_schema_validation") + if optional_params is not None + else None + ) + _enable_json_schema_validation = ( + _per_request_validation + if _per_request_validation is not None + else litellm.enable_json_schema_validation + ) + if _enable_json_schema_validation is True: try: if ( optional_params is not None diff --git a/tests/litellm/litellm_core_utils/test_json_schema_validation.py b/tests/litellm/litellm_core_utils/test_json_schema_validation.py new file mode 100644 index 00000000000..859a1238a61 --- /dev/null +++ b/tests/litellm/litellm_core_utils/test_json_schema_validation.py @@ -0,0 +1,139 @@ +""" +Tests for per-request enable_json_schema_validation parameter. + +Ensures the per-request flag overrides the global litellm.enable_json_schema_validation, +making JSON schema validation thread-safe for concurrent usage. + +Related issue: https://github.com/BerriAI/litellm/issues/XXXX +""" + +import json + +import pytest + +import litellm +from litellm.types.utils import ModelResponse +from litellm.utils import Rules, post_call_processing + + +def _make_response(content: dict) -> ModelResponse: + """Create a ModelResponse with the given content as JSON string.""" + response = ModelResponse() + response.choices[0].message.content = json.dumps(content) + return response + + +def _mock_completion(): + """Mock function with __name__ == 'completion' for post_call_processing.""" + pass + + +_mock_completion.__name__ = "completion" + +# Schema that requires 'title' (string) and 'rating' (integer) +STRICT_SCHEMA = { + "type": "json_schema", + "json_schema": { + "name": "MovieReview", + "schema": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "rating": {"type": "integer"}, + }, + "required": ["title", "rating"], + }, + }, +} + +# Response that does NOT match the schema (wrong field names) +INVALID_RESPONSE = _make_response({"name": "test", "age": 25}) + +# Response that matches the schema +VALID_RESPONSE = _make_response({"title": "Inception", "rating": 9}) + + +@pytest.fixture(autouse=True) +def _reset_global_flag(): + """Reset the global flag before and after each test.""" + original = litellm.enable_json_schema_validation + litellm.enable_json_schema_validation = False + yield + litellm.enable_json_schema_validation = original + + +class TestPerRequestJsonSchemaValidation: + """Test that per-request enable_json_schema_validation overrides the global flag.""" + + def test_global_off_no_per_request_skips_validation(self): + """Global OFF + no per-request flag -> no validation (default behavior).""" + litellm.enable_json_schema_validation = False + # Should NOT raise even though response doesn't match schema + post_call_processing( + INVALID_RESPONSE, + "test-model", + {"response_format": STRICT_SCHEMA}, + _mock_completion, + Rules(), + ) + + def test_per_request_on_overrides_global_off(self): + """Global OFF + per-request ON -> validation runs and catches invalid response.""" + litellm.enable_json_schema_validation = False + with pytest.raises(litellm.JSONSchemaValidationError): + post_call_processing( + INVALID_RESPONSE, + "test-model", + { + "response_format": STRICT_SCHEMA, + "enable_json_schema_validation": True, + }, + _mock_completion, + Rules(), + ) + + def test_per_request_off_overrides_global_on(self): + """Global ON + per-request OFF -> validation skipped (per-request wins).""" + litellm.enable_json_schema_validation = True + # Should NOT raise because per-request says False + post_call_processing( + INVALID_RESPONSE, + "test-model", + { + "response_format": STRICT_SCHEMA, + "enable_json_schema_validation": False, + }, + _mock_completion, + Rules(), + ) + + def test_global_on_no_per_request_validates(self): + """Global ON + no per-request flag -> validation runs (backward compatible).""" + litellm.enable_json_schema_validation = True + with pytest.raises(litellm.JSONSchemaValidationError): + post_call_processing( + INVALID_RESPONSE, + "test-model", + {"response_format": STRICT_SCHEMA}, + _mock_completion, + Rules(), + ) + + def test_valid_response_passes_with_per_request_on(self): + """Per-request ON + valid response -> no error raised.""" + post_call_processing( + VALID_RESPONSE, + "test-model", + { + "response_format": STRICT_SCHEMA, + "enable_json_schema_validation": True, + }, + _mock_completion, + Rules(), + ) + + def test_per_request_flag_is_in_all_litellm_params(self): + """Ensure the param is registered so it doesn't leak to provider APIs.""" + from litellm.types.utils import all_litellm_params + + assert "enable_json_schema_validation" in all_litellm_params From fd4dc028bcd65c8be3476fbcdebc66124d54d66e Mon Sep 17 00:00:00 2001 From: Chesars Date: Wed, 18 Feb 2026 16:22:55 -0300 Subject: [PATCH 003/171] fix(anthropic): remove hardcoded reasoning summary in adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adapter was injecting `summary: "detailed"` into the reasoning config when routing Anthropic thinking requests to OpenAI's Responses API. Per the OpenAI spec, reasoning.summary is opt-in — it should not be added unless the user explicitly requests it. --- .../adapters/handler.py | 12 +----- ...erimental_pass_through_messages_handler.py | 39 ++++++++++++++++++- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index 73e74c228ba..e6a73fdbb59 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -44,8 +44,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: For OpenAI models, Chat Completions typically does not return reasoning text (only token accounting). To return a thinking-like content block in the - Anthropic response format, we route the request through OpenAI's Responses API - and request a reasoning summary. + Anthropic response format, we route the request through OpenAI's Responses API. """ custom_llm_provider = completion_kwargs.get("custom_llm_provider") if custom_llm_provider is None: @@ -80,16 +79,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: if isinstance(reasoning_effort, str) and reasoning_effort: completion_kwargs["reasoning_effort"] = { "effort": reasoning_effort, - "summary": "detailed", } - elif isinstance(reasoning_effort, dict): - if ( - "summary" not in reasoning_effort - and "generate_summary" not in reasoning_effort - ): - updated_reasoning_effort = dict(reasoning_effort) - updated_reasoning_effort["summary"] = "detailed" - completion_kwargs["reasoning_effort"] = updated_reasoning_effort @staticmethod def _prepare_completion_kwargs( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 376d14416a3..a4d5ade568b 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -177,8 +177,8 @@ def test_openai_model_with_thinking_converts_to_reasoning_effort(): # Verify reasoning_effort is set (converted from thinking) assert "reasoning_effort" in call_kwargs, "reasoning_effort should be passed to completion" - # reasoning_effort is transformed into a dict with effort and summary fields - expected_reasoning_effort = {"effort": "minimal", "summary": "detailed"} + # reasoning_effort is transformed into a dict with effort field only (no hardcoded summary) + expected_reasoning_effort = {"effort": "minimal"} assert call_kwargs["reasoning_effort"] == expected_reasoning_effort, \ f"reasoning_effort should be {expected_reasoning_effort} for budget_tokens=1024, got {call_kwargs.get('reasoning_effort')}" @@ -218,3 +218,38 @@ class TestThinkingParameterTransformation: assert result == {"reasoning_effort": "minimal"} assert "thinking" not in result + + +class TestNoHardcodedReasoningSummary: + """Tests for issue #20998: adapter must not hardcode reasoning summary. + + Per OpenAI spec, reasoning.summary is opt-in. The adapter should not + inject summary='detailed' when the user didn't request it. + """ + + def test_no_summary_added_when_not_requested(self): + """reasoning_effort dict should only contain 'effort', no 'summary'.""" + from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( + LiteLLMMessagesToCompletionTransformationHandler, + ) + + thinking = {"type": "enabled", "budget_tokens": 5000} + completion_kwargs = {"model": "openai/gpt-5.1", "reasoning_effort": "medium"} + LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( + completion_kwargs, thinking=thinking + ) + assert completion_kwargs["reasoning_effort"] == {"effort": "medium"} + assert "summary" not in completion_kwargs["reasoning_effort"] + + def test_model_prefixed_with_responses(self): + """Model should be prefixed with 'responses/' for Responses API routing.""" + from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( + LiteLLMMessagesToCompletionTransformationHandler, + ) + + thinking = {"type": "enabled", "budget_tokens": 5000} + completion_kwargs = {"model": "openai/gpt-5.1", "reasoning_effort": "medium"} + LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( + completion_kwargs, thinking=thinking + ) + assert completion_kwargs["model"] == "responses/openai/gpt-5.1" From 5596545fb99743b0e8870c5a5b1a30aa88ccd02d Mon Sep 17 00:00:00 2001 From: Chesars Date: Thu, 19 Feb 2026 15:01:41 -0300 Subject: [PATCH 004/171] fix(gemini): correct streaming finish_reason for tool calls Gemini returns finishReason="STOP" even when tool calls are present, and sends tool_calls and finishReason in separate streaming chunks. The ModelResponseIterator now tracks tool_calls across chunks and correctly maps finish_reason to "tool_calls" per the OpenAI spec. Fixes #21041 --- .../vertex_and_google_ai_studio_gemini.py | 35 +++ ...emini_streaming_tool_call_finish_reason.py | 232 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 tests/test_litellm/llms/vertex_ai/gemini/test_gemini_streaming_tool_call_finish_reason.py diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index bef83b6d35e..3c06ba94450 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -2864,6 +2864,7 @@ class ModelResponseIterator: self.logging_obj = logging_obj self.is_function_call = check_is_function_call(logging_obj) self.cumulative_tool_call_index: int = 0 + self.has_seen_tool_calls: bool = False def chunk_parser(self, chunk: dict) -> Optional["ModelResponseStream"]: try: @@ -2902,6 +2903,40 @@ class ModelResponseIterator: cumulative_tool_call_index=self.cumulative_tool_call_index, ) + # Track whether tool_calls have been seen across streaming chunks. + # Gemini sends tool_calls and finishReason in separate chunks, + # so we need to remember if earlier chunks contained tool_calls + # to correctly set finish_reason="tool_calls" per the OpenAI spec. + if not self.has_seen_tool_calls: + for choice in model_response.choices: + if hasattr(choice, "delta") and choice.delta and choice.delta.tool_calls: + self.has_seen_tool_calls = True + break + + # Handle final chunk with finishReason but no content. + # _process_candidates skips candidates without "content", + # so the finish_reason from the final chunk is lost. + if not model_response.choices and _candidates: + from litellm.types.utils import Delta, StreamingChoices + + for candidate in _candidates: + finish_reason_str = candidate.get("finishReason") + if finish_reason_str is not None: + if self.has_seen_tool_calls: + mapped_finish_reason = "tool_calls" + else: + mapped_finish_reason = VertexGeminiConfig._check_finish_reason( + None, finish_reason_str + ) + choice = StreamingChoices( + finish_reason=mapped_finish_reason, + index=candidate.get("index", 0), + delta=Delta(content=None, role=None), + logprobs=None, + enhancements=None, + ) + model_response.choices.append(choice) + setattr(model_response, "vertex_ai_grounding_metadata", grounding_metadata) # type: ignore setattr(model_response, "vertex_ai_url_context_metadata", url_context_metadata) # type: ignore setattr(model_response, "vertex_ai_safety_ratings", safety_ratings) # type: ignore diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_gemini_streaming_tool_call_finish_reason.py b/tests/test_litellm/llms/vertex_ai/gemini/test_gemini_streaming_tool_call_finish_reason.py new file mode 100644 index 00000000000..3f8efd47fa3 --- /dev/null +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_gemini_streaming_tool_call_finish_reason.py @@ -0,0 +1,232 @@ +""" +Tests for Gemini streaming tool call finish_reason mapping. + +Gemini returns finishReason: "STOP" even when tool calls are present. +Per the OpenAI spec, finish_reason must be "tool_calls" when the model +called a tool. The ModelResponseIterator must track tool_calls across +streaming chunks and correctly set finish_reason on the final chunk. + +Ref: https://github.com/BerriAI/litellm/issues/21041 +""" + +from unittest.mock import MagicMock + +from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + ModelResponseIterator, +) + + +def _make_logging_obj(**kwargs): + """Create a minimal mock logging object for ModelResponseIterator.""" + logging_obj = MagicMock() + logging_obj.optional_params = kwargs.get("optional_params", {}) + return logging_obj + + +def test_streaming_tool_call_finish_reason_is_tool_calls(): + """ + When Gemini streams tool calls across two chunks: + - Chunk 1: has tool call parts, no finishReason + - Chunk 2: has finishReason="STOP", no content + + The final chunk must have finish_reason="tool_calls" (not "stop"). + """ + logging_obj = _make_logging_obj() + iterator = ModelResponseIterator( + streaming_response=iter([]), + sync_stream=True, + logging_obj=logging_obj, + ) + + # Chunk 1: tool call with no finishReason + chunk_with_tool_calls = { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "get_current_weather", + "args": {"location": "Boston, MA"}, + } + } + ], + "role": "model", + }, + "index": 0, + } + ], + } + + # Chunk 2: finishReason="STOP" with no content + chunk_with_finish_reason = { + "candidates": [ + { + "finishReason": "STOP", + "index": 0, + } + ], + "usageMetadata": { + "promptTokenCount": 50, + "candidatesTokenCount": 20, + "totalTokenCount": 70, + }, + } + + # Process chunk 1 + response1 = iterator.chunk_parser(chunk_with_tool_calls) + assert response1 is not None + assert len(response1.choices) == 1 + assert response1.choices[0].delta.tool_calls is not None + assert response1.choices[0].finish_reason == "tool_calls" + assert iterator.has_seen_tool_calls is True + + # Process chunk 2 (final chunk) + response2 = iterator.chunk_parser(chunk_with_finish_reason) + assert response2 is not None + assert len(response2.choices) == 1 + assert response2.choices[0].finish_reason == "tool_calls" + + +def test_streaming_no_tool_calls_finish_reason_is_stop(): + """ + When Gemini streams a regular text response (no tool calls), + the final chunk with finishReason="STOP" should map to "stop". + """ + logging_obj = _make_logging_obj() + iterator = ModelResponseIterator( + streaming_response=iter([]), + sync_stream=True, + logging_obj=logging_obj, + ) + + # Chunk 1: text content, no finishReason + chunk_with_text = { + "candidates": [ + { + "content": { + "parts": [{"text": "Hello! How can I help?"}], + "role": "model", + }, + "index": 0, + } + ], + } + + # Chunk 2: finishReason="STOP" with no content + chunk_with_finish_reason = { + "candidates": [ + { + "finishReason": "STOP", + "index": 0, + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "candidatesTokenCount": 8, + "totalTokenCount": 18, + }, + } + + # Process chunk 1 + response1 = iterator.chunk_parser(chunk_with_text) + assert response1 is not None + assert len(response1.choices) == 1 + assert iterator.has_seen_tool_calls is False + + # Process chunk 2 + response2 = iterator.chunk_parser(chunk_with_finish_reason) + assert response2 is not None + assert len(response2.choices) == 1 + assert response2.choices[0].finish_reason == "stop" + + +def test_streaming_multiple_tool_calls_finish_reason(): + """ + When Gemini streams multiple tool calls across chunks, + the final finish_reason must still be "tool_calls". + """ + logging_obj = _make_logging_obj() + iterator = ModelResponseIterator( + streaming_response=iter([]), + sync_stream=True, + logging_obj=logging_obj, + ) + + # Chunk 1: first tool call + chunk_tool_1 = { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "get_weather", + "args": {"location": "NYC"}, + } + }, + { + "functionCall": { + "name": "get_time", + "args": {"timezone": "EST"}, + } + }, + ], + "role": "model", + }, + "index": 0, + } + ], + } + + # Chunk 2: finishReason="STOP" with no content + chunk_finish = { + "candidates": [ + { + "finishReason": "STOP", + "index": 0, + } + ], + "usageMetadata": { + "promptTokenCount": 50, + "candidatesTokenCount": 30, + "totalTokenCount": 80, + }, + } + + response1 = iterator.chunk_parser(chunk_tool_1) + assert response1 is not None + assert iterator.has_seen_tool_calls is True + + response2 = iterator.chunk_parser(chunk_finish) + assert response2 is not None + assert len(response2.choices) == 1 + assert response2.choices[0].finish_reason == "tool_calls" + + +def test_streaming_content_filter_finish_reason_preserved(): + """ + When Gemini returns finishReason due to content filtering (not STOP), + and no tool calls were seen, the content_filter reason should be preserved. + """ + logging_obj = _make_logging_obj() + iterator = ModelResponseIterator( + streaming_response=iter([]), + sync_stream=True, + logging_obj=logging_obj, + ) + + # Chunk with finishReason="SAFETY" and no content + chunk_safety = { + "candidates": [ + { + "finishReason": "SAFETY", + "index": 0, + } + ], + } + + response = iterator.chunk_parser(chunk_safety) + assert response is not None + assert len(response.choices) == 1 + assert response.choices[0].finish_reason == "content_filter" From 34d09a314ca5a60d7cd64a7f1e531213c01c5d90 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Mon, 23 Feb 2026 14:47:07 -0800 Subject: [PATCH 005/171] virtual-keys-team-table --- .../src/components/team/TeamInfo.test.tsx | 59 +- .../src/components/team/TeamInfo.tsx | 12 + .../team/TeamVirtualKeysTable.test.tsx | 192 +++++ .../components/team/TeamVirtualKeysTable.tsx | 757 ++++++++++++++++++ .../team/tabVisibilityUtils.test.ts | 19 +- .../src/components/team/tabVisibilityUtils.ts | 6 +- 6 files changed, 1040 insertions(+), 5 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx create mode 100644 ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx index cc0483aafd6..75fe76902bd 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx @@ -100,12 +100,28 @@ vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroups", () => ({ }), })); +vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({ + useKeys: vi.fn().mockReturnValue({ + data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 }, + isPending: false, + isFetching: false, + refetch: vi.fn(), + }), +})); + +vi.mock("../key_team_helpers/filter_helpers", () => ({ + fetchAllKeyAliases: vi.fn().mockResolvedValue([]), + fetchAllOrganizations: vi.fn().mockResolvedValue([]), +})); + import { useAllProxyModels } from "@/app/(dashboard)/hooks/models/useModels"; +import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; const mockUseAllProxyModels = vi.mocked(useAllProxyModels); +const mockUseKeys = vi.mocked(useKeys); const mockUseTeam = vi.mocked(useTeam); const mockUseOrganization = vi.mocked(useOrganization); const mockUseCurrentUser = vi.mocked(useCurrentUser); @@ -180,6 +196,12 @@ describe("TeamInfoView", () => { data: { models: [] }, isLoading: false, } as any); + mockUseKeys.mockReturnValue({ + data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 }, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); vi.mocked(networking.getPoliciesList).mockResolvedValue({ policies: [] }); @@ -558,7 +580,42 @@ describe("TeamInfoView", () => { renderWithProviders(); await waitFor(() => { - expect(screen.getByText("Virtual Keys")).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Virtual Keys" })).toBeInTheDocument(); + }); + }); + + it("should show Virtual Keys tab when user cannot edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Virtual Keys" })).toBeInTheDocument(); + }); + }); + + it("should display X Members in Virtual Keys tab when navigated to", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + mockUseKeys.mockReturnValue({ + data: { keys: [], total_count: 5, current_page: 1, total_pages: 1 }, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const virtualKeysTab = screen.getByRole("tab", { name: "Virtual Keys" }); + await user.click(virtualKeysTab); + + await waitFor(() => { + expect(screen.getByText("5 Members")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index ea7a9a1c460..62208b4186d 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -48,6 +48,7 @@ import { TEAM_INFO_TAB_LABELS, } from "./tabVisibilityUtils"; import TeamMembersComponent from "./TeamMemberTab"; +import { TeamVirtualKeysTable } from "./TeamVirtualKeysTable"; export interface TeamMembership { user_id: string; @@ -726,6 +727,17 @@ const TeamInfoView: React.FC = ({ ), }, + { + key: TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS, + label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS], + children: ( + + ), + }, { key: TEAM_INFO_TAB_KEYS.MEMBERS, label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBERS], diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx new file mode 100644 index 00000000000..e4b7b00fb58 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx @@ -0,0 +1,192 @@ +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi, MockedFunction } from "vitest"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { TeamVirtualKeysTable } from "./TeamVirtualKeysTable"; +import { KeysResponse, useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { KeyResponse } from "../key_team_helpers/key_list"; +import { Organization } from "../networking"; + +vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({ + useKeys: vi.fn(), +})); + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: vi.fn(), +})); + +vi.mock("../key_team_helpers/filter_helpers", () => ({ + fetchAllKeyAliases: vi.fn().mockResolvedValue([]), + fetchAllOrganizations: vi.fn().mockResolvedValue([]), +})); + +vi.mock("../key_team_helpers/fetch_available_models_team_key", () => ({ + getModelDisplayName: vi.fn((model: string) => model), +})); + +vi.mock("../templates/key_info_view", () => ({ + default: vi.fn(({ onClose }: { onClose: () => void }) => ( +
+ Key Info View + +
+ )), +})); + +const mockUseKeys = useKeys as MockedFunction; +const mockUseAuthorized = useAuthorized as MockedFunction; + +const createMockKey = (overrides: Partial = {}): KeyResponse => + ({ + token: "sk-test123", + token_id: "key-1", + key_alias: "alice_key_team1", + key_name: "sk-...abc", + user_id: "user-1", + organization_id: null, + user: { user_id: "user-1", user_email: "alice@example.com" }, + created_at: "2024-01-01T00:00:00Z", + team_id: "team-1", + spend: 0, + max_budget: 100, + models: ["gpt-4"], + ...overrides, + } as KeyResponse); + +const mockOrganization: Organization = { + organization_id: "org-123", + organization_alias: "Test Org", + budget_id: "budget-1", + metadata: {}, + models: [], + spend: 0, + model_spend: {}, + created_at: "", + created_by: "", + updated_at: "", + updated_by: "", + litellm_budget_table: {}, + teams: [], + users: [], + members: [], +}; + +describe("TeamVirtualKeysTable", () => { + const defaultProps = { + teamId: "team-1", + teamAlias: "team1", + organization: null as Organization | null, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" } as any); + mockUseKeys.mockReturnValue({ + data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + }); + + it("should render successfully", async () => { + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("0 Members")).toBeInTheDocument(); + }); + }); + + it("should display X Members instead of Showing X of Y results", async () => { + mockUseKeys.mockReturnValue({ + data: { + keys: [createMockKey(), createMockKey({ token: "sk-2", token_id: "key-2" })], + total_count: 2, + current_page: 1, + total_pages: 1, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("2 Members")).toBeInTheDocument(); + }); + expect(screen.queryByText(/Showing.*results/)).not.toBeInTheDocument(); + }); + + it("should display 1 Member when singular", async () => { + mockUseKeys.mockReturnValue({ + data: { + keys: [createMockKey()], + total_count: 1, + current_page: 1, + total_pages: 1, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("1 Member")).toBeInTheDocument(); + }); + }); + + it("should call useKeys with expand user to fetch user email", async () => { + renderWithProviders(); + + await waitFor(() => { + expect(mockUseKeys).toHaveBeenCalledWith( + 1, + 50, + expect.objectContaining({ + teamID: "team-1", + expand: "user", + }) + ); + }); + }); + + it("should enrich keys with organization_id when organization is provided", async () => { + const keyWithoutOrg = createMockKey({ organization_id: null }); + mockUseKeys.mockReturnValue({ + data: { + keys: [keyWithoutOrg], + total_count: 1, + current_page: 1, + total_pages: 1, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByText("1 Member")).toBeInTheDocument(); + }); + // Key with org_id should display in table - org-123 from organization + await waitFor(() => { + expect(screen.getByText("org-123")).toBeInTheDocument(); + }); + }); + + it("should show table with Key ID column header", async () => { + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("0 Members")).toBeInTheDocument(); + }); + expect(screen.getByText("Key ID")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx new file mode 100644 index 00000000000..ce998d18677 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -0,0 +1,757 @@ +// TO-DO: Standardize tables eventually + +"use client"; +import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + PaginationState, + SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { + Badge, + Button, + Icon, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + Text, +} from "@tremor/react"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Popover, Skeleton, Tooltip } from "antd"; +import React, { useEffect, useMemo, useState } from "react"; +import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; +import { KeyResponse, Team } from "../key_team_helpers/key_list"; +import FilterComponent, { FilterOption } from "../molecules/filter"; +import { Organization } from "../networking"; +import KeyInfoView from "../templates/key_info_view"; +import { useQuery } from "@tanstack/react-query"; +import { fetchAllKeyAliases, fetchAllOrganizations } from "../key_team_helpers/filter_helpers"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +interface TeamVirtualKeysTableProps { + teamId: string; + teamAlias?: string; + organization: Organization | null; +} + +/** + * TeamVirtualKeysTable – variant of VirtualKeysTable scoped to a single team. + * Displays all virtual keys belonging to the team with same format and styling. + */ +export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVirtualKeysTableProps) { + const { accessToken } = useAuthorized(); + const [selectedKey, setSelectedKey] = useState(null); + const [sorting, setSorting] = useState([ + { id: "created_at", desc: true }, + ]); + const [tablePagination, setTablePagination] = useState({ + pageIndex: 0, + pageSize: 50, + }); + const [filters, setFilters] = useState>({ + "Organization ID": "", + "Key Alias": "", + "User ID": "", + "Sort By": "created_at", + "Sort Order": "desc", + }); + + const sortBy = sorting.length > 0 ? sorting[0].id : "created_at"; + const sortOrder = sorting.length > 0 ? (sorting[0].desc ? "desc" : "asc") : "desc"; + + const { + data: keys, + isPending: isLoading, + isFetching, + refetch, + } = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize, { + teamID: teamId, + sortBy: sortBy || undefined, + sortOrder: sortOrder || undefined, + organizationID: filters["Organization ID"] || undefined, + selectedKeyAlias: filters["Key Alias"] || undefined, + userID: filters["User ID"] || undefined, + expand: "user", + }); + + const totalCount = keys?.total_count || 0; + const displayKeys = useMemo(() => { + const kList = keys?.keys || []; + const orgId = organization?.organization_id; + if (!orgId) return kList; + return kList.map((k: KeyResponse) => ({ + ...k, + organization_id: k.organization_id || orgId, + })); + }, [keys?.keys, organization?.organization_id]); + const [expandedAccordions, setExpandedAccordions] = useState>({}); + + const currentTeam: Team = useMemo( + () => ({ + team_id: teamId, + team_alias: teamAlias || teamId, + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: organization?.organization_id || "", + created_at: "", + keys: [], + members_with_roles: [], + spend: 0, + }), + [teamId, teamAlias, organization], + ); + + const allKeyAliasesQuery = useQuery({ + queryKey: ["allKeyAliases"], + queryFn: async () => fetchAllKeyAliases(accessToken), + enabled: !!accessToken, + }); + const allKeyAliases = allKeyAliasesQuery.data || []; + + const allOrganizationsQuery = useQuery({ + queryKey: ["allOrganizations"], + queryFn: async () => fetchAllOrganizations(accessToken), + enabled: !!accessToken, + }); + const allOrganizations = allOrganizationsQuery.data || []; + + useEffect(() => { + if (refetch) { + const handleStorageChange = () => refetch(); + window.addEventListener("storage", handleStorageChange); + return () => window.removeEventListener("storage", handleStorageChange); + } + }, [refetch]); + + const handleFilterChange = (newFilters: Record, skipDebounce = false) => { + setFilters((prev) => ({ + ...prev, + "Organization ID": newFilters["Organization ID"] ?? prev["Organization ID"], + "Key Alias": newFilters["Key Alias"] ?? prev["Key Alias"], + "User ID": newFilters["User ID"] ?? prev["User ID"], + "Sort By": newFilters["Sort By"] ?? prev["Sort By"] ?? "created_at", + "Sort Order": newFilters["Sort Order"] ?? prev["Sort Order"] ?? "desc", + })); + if (!skipDebounce) { + setTablePagination((prev) => ({ ...prev, pageIndex: 0 })); + } + }; + + const handleFilterReset = () => { + setFilters({ + "Organization ID": "", + "Key Alias": "", + "User ID": "", + "Sort By": "created_at", + "Sort Order": "desc", + }); + setTablePagination((prev) => ({ ...prev, pageIndex: 0 })); + }; + + const filterOptions: FilterOption[] = useMemo( + () => [ + { + name: "Organization ID", + label: "Organization ID", + isSearchable: true, + searchFn: async (searchText: string) => { + if (!allOrganizations.length) return []; + const filtered = allOrganizations.filter( + (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false, + ); + return filtered + .filter((org) => org.organization_id != null) + .map((org) => ({ + label: `${org.organization_id || "Unknown"} (${org.organization_id})`, + value: org.organization_id as string, + })); + }, + }, + { + name: "Key Alias", + label: "Key Alias", + isSearchable: true, + searchFn: async (searchText: string) => { + const filtered = allKeyAliases.filter((alias) => + alias.toLowerCase().includes(searchText.toLowerCase()), + ); + return filtered.map((alias) => ({ label: alias, value: alias })); + }, + }, + { name: "User ID", label: "User ID", isSearchable: false }, + ], + [allOrganizations, allKeyAliases], + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + id: "token", + accessorKey: "token", + header: "Key ID", + size: 100, + enableSorting: true, + cell: (info) => { + const value = info.getValue() as string; + const width = info.cell.column.getSize(); + return ( + + + + ); + }, + }, + { + id: "key_alias", + accessorKey: "key_alias", + header: "Key Alias", + size: 150, + enableSorting: true, + cell: (info) => { + const value = info.getValue() as string; + const width = info.cell.column.getSize(); + return ( + + + {value ?? "-"} + + + ); + }, + }, + { + id: "key_name", + accessorKey: "key_name", + header: "Secret Key", + size: 120, + enableSorting: false, + cell: (info) => {info.getValue() as string}, + }, + { + id: "organization_id", + accessorKey: "organization_id", + header: "Organization ID", + size: 140, + enableSorting: false, + cell: (info) => (info.getValue() ? info.renderValue() : "-"), + }, + { + id: "user_email", + accessorKey: "user", + header: "User Email", + size: 160, + enableSorting: false, + cell: (info) => { + const user = info.getValue() as { user_email?: string } | undefined; + const value = user?.user_email; + const width = info.cell.column.getSize(); + return ( + + + {value ?? "-"} + + + ); + }, + }, + { + id: "user_id", + accessorKey: "user_id", + header: "User ID", + size: 70, + enableSorting: false, + cell: (info) => { + const userId = info.getValue() as string | null; + const displayValue = userId === "default_user_id" ? "Default Proxy Admin" : userId; + const width = info.cell.column.getSize(); + return ( + + + {displayValue ?? "-"} + + + ); + }, + }, + { + id: "created_at", + accessorKey: "created_at", + header: "Created At", + size: 120, + enableSorting: true, + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "-"; + }, + }, + { + id: "created_by", + accessorKey: "created_by", + header: "Created By", + size: 70, + enableSorting: false, + cell: (info) => { + const value = info.getValue() as string | null; + const displayValue = value === "default_user_id" ? "Default Proxy Admin" : value; + const width = info.cell.column.getSize(); + return ( + + + {displayValue ?? "-"} + + + ); + }, + }, + { + id: "updated_at", + accessorKey: "updated_at", + header: "Updated At", + size: 120, + enableSorting: true, + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "Never"; + }, + }, + { + id: "last_active", + accessorKey: "last_active", + header: () => ( + + Last Active + + + + + ), + size: 130, + enableSorting: false, + cell: (info) => { + const value = info.getValue(); + if (!value) return "Unknown"; + const date = new Date(value as string); + return ( + + {date.toLocaleDateString()} + + ); + }, + }, + { + id: "expires", + accessorKey: "expires", + header: "Expires", + size: 120, + enableSorting: false, + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "Never"; + }, + }, + { + id: "spend", + accessorKey: "spend", + header: "Spend (USD)", + size: 100, + enableSorting: true, + cell: (info) => formatNumberWithCommas(info.getValue() as number, 4), + }, + { + id: "max_budget", + accessorKey: "max_budget", + header: "Budget (USD)", + size: 110, + enableSorting: true, + cell: (info) => { + const maxBudget = info.getValue() as number | null; + if (maxBudget === null) return "Unlimited"; + return `$${formatNumberWithCommas(maxBudget)}`; + }, + }, + { + id: "budget_reset_at", + accessorKey: "budget_reset_at", + header: "Budget Reset", + size: 130, + enableSorting: false, + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleString() : "Never"; + }, + }, + { + id: "models", + accessorKey: "models", + header: "Models", + size: 200, + enableSorting: false, + cell: (info) => { + const models = info.getValue() as string[]; + return ( +
+ {Array.isArray(models) ? ( +
+ {models.length === 0 ? ( + + All Proxy Models + + ) : ( + <> +
+ {models.length > 3 && ( +
+ + setExpandedAccordions((prev) => ({ + ...prev, + [info.row.id]: !prev[info.row.id], + })) + } + /> +
+ )} +
+ {models.slice(0, 3).map((model, index) => + model === "all-proxy-models" ? ( + + All Proxy Models + + ) : ( + + + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} + + + ), + )} + {models.length > 3 && !expandedAccordions[info.row.id] && ( + + + +{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"} + + + )} + {expandedAccordions[info.row.id] && ( +
+ {models.slice(3).map((model, index) => + model === "all-proxy-models" ? ( + + All Proxy Models + + ) : ( + + + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} + + + ), + )} +
+ )} +
+
+ + )} +
+ ) : null} +
+ ); + }, + }, + { + id: "rate_limits", + header: "Rate Limits", + size: 140, + enableSorting: false, + cell: ({ row }) => { + const key = row.original; + return ( +
+
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
+
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
+
+ ); + }, + }, + ], + [expandedAccordions], + ); + + const table = useReactTable({ + data: displayKeys, + columns, + columnResizeMode: "onChange", + columnResizeDirection: "ltr", + state: { sorting, pagination: tablePagination }, + onSortingChange: (updaterOrValue) => { + const newSorting = + typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; + setSorting(newSorting); + if (newSorting?.length > 0) { + const sortState = newSorting[0]; + handleFilterChange( + { + ...filters, + "Sort By": sortState.id, + "Sort Order": sortState.desc ? "desc" : "asc", + }, + true, + ); + } + }, + onPaginationChange: setTablePagination, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableSorting: true, + manualSorting: false, + manualPagination: true, + pageCount: Math.ceil(totalCount / tablePagination.pageSize), + }); + + const { pageIndex, pageSize } = table.getState().pagination; + return ( +
+ {selectedKey ? ( + setSelectedKey(null)} + keyData={selectedKey} + teams={[currentTeam]} + onDelete={refetch} + /> + ) : ( +
+
+ +
+ +
+ {isLoading || isFetching ? ( + + ) : ( + + {totalCount} Member{totalCount !== 1 ? "s" : ""} + + )} + +
+ {isLoading || isFetching ? ( + + ) : ( + + Page {pageIndex + 1} of {table.getPageCount()} + + )} + + {isLoading || isFetching ? ( + + ) : ( + + )} + + {isLoading || isFetching ? ( + + ) : ( + + )} +
+
+
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + { + const resizer = document.querySelector( + `[data-header-id="${header.id}"] .resizer`, + ); + if (resizer) (resizer as HTMLElement).style.opacity = "0.5"; + }} + onMouseLeave={() => { + const resizer = document.querySelector( + `[data-header-id="${header.id}"] .resizer`, + ); + if (resizer && !header.column.getIsResizing()) + (resizer as HTMLElement).style.opacity = "0"; + }} + onClick={ + header.column.getCanSort() + ? header.column.getToggleSortingHandler() + : undefined + } + > +
+
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ {header.id !== "actions" && header.column.getCanSort() && ( +
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: , + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+ )} +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${table.options.columnResizeDirection} ${ + header.column.getIsResizing() ? "isResizing" : "" + }`} + style={{ + position: "absolute", + right: 0, + top: 0, + height: "100%", + width: "5px", + background: header.column.getIsResizing() ? "#3b82f6" : "transparent", + cursor: "col-resize", + userSelect: "none", + touchAction: "none", + opacity: header.column.getIsResizing() ? 1 : 0, + }} + /> +
+ + ))} + + ))} + + + {isLoading || isFetching ? ( + + +
+

Loading keys...

+
+
+
+ ) : displayKeys.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + 3 + ? "px-0" + : "" + }`} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + +
+

No keys found

+
+
+
+ )} +
+
+
+
+
+
+ )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts index 5a09b4fa36c..8b9a0402c9a 100644 --- a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts +++ b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts @@ -11,6 +11,7 @@ describe("team_info_tabs", () => { describe("TEAM_INFO_TAB_LABELS", () => { it("should have label for every tab key", () => { expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.OVERVIEW]).toBe("Overview"); + expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS]).toBe("Virtual Keys"); expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBERS]).toBe("Members"); expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS]).toBe("Member Permissions"); expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.SETTINGS]).toBe("Settings"); @@ -18,15 +19,16 @@ describe("team_info_tabs", () => { }); describe("getTeamInfoVisibleTabs", () => { - it("returns only overview when user cannot edit team", () => { + it("returns overview and virtual keys when user cannot edit team", () => { const tabs = getTeamInfoVisibleTabs(false); - expect(tabs).toEqual([TEAM_INFO_TAB_KEYS.OVERVIEW]); + expect(tabs).toEqual([TEAM_INFO_TAB_KEYS.OVERVIEW, TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS]); }); it("returns all tabs when user can edit team", () => { const tabs = getTeamInfoVisibleTabs(true); expect(tabs).toEqual([ TEAM_INFO_TAB_KEYS.OVERVIEW, + TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS, TEAM_INFO_TAB_KEYS.MEMBERS, TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS, TEAM_INFO_TAB_KEYS.SETTINGS, @@ -55,6 +57,19 @@ describe("team_info_tabs", () => { expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.OVERVIEW, true)).toBe(true); }); + it("always returns true for virtual keys tab regardless of edit permission", () => { + expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS, false)).toBe(true); + expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS, true)).toBe(true); + }); + + it("returns false for member permissions tab when user cannot edit", () => { + expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS, false)).toBe(false); + }); + + it("returns true for member permissions tab when user can edit", () => { + expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS, true)).toBe(true); + }); + it("returns false for members tab when user cannot edit", () => { expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.MEMBERS, false)).toBe(false); }); diff --git a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts index d77230ea09b..dd0e54baf36 100644 --- a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts +++ b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts @@ -5,6 +5,7 @@ export const TEAM_INFO_TAB_KEYS = { OVERVIEW: "overview", + VIRTUAL_KEYS: "virtual-keys", MEMBERS: "members", MEMBER_PERMISSIONS: "member-permissions", SETTINGS: "settings", @@ -12,6 +13,7 @@ export const TEAM_INFO_TAB_KEYS = { export const TEAM_INFO_TAB_LABELS: Record = { [TEAM_INFO_TAB_KEYS.OVERVIEW]: "Overview", + [TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS]: "Virtual Keys", [TEAM_INFO_TAB_KEYS.MEMBERS]: "Members", [TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS]: "Member Permissions", [TEAM_INFO_TAB_KEYS.SETTINGS]: "Settings", @@ -19,11 +21,11 @@ export const TEAM_INFO_TAB_LABELS: Record = { /** * Returns the list of tab keys that should be visible based on permissions. - * - Overview: always visible + * - Overview, Virtual Keys: always visible * - Members, Member Permissions, Settings: only when canEditTeam is true */ export function getTeamInfoVisibleTabs(canEditTeam: boolean): readonly string[] { - const baseTabs = [TEAM_INFO_TAB_KEYS.OVERVIEW]; + const baseTabs = [TEAM_INFO_TAB_KEYS.OVERVIEW, TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS]; if (canEditTeam) { return [ ...baseTabs, From 6d98622923f95c6e047c910637cdf92b3f055f77 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Mon, 23 Feb 2026 15:21:49 -0800 Subject: [PATCH 006/171] filters working properly for team virtual keys --- .../key_team_helpers/filter_helpers.ts | 77 +++++++- .../src/components/team/TeamInfo.test.tsx | 71 +++++++- .../team/TeamVirtualKeysTable.test.tsx | 170 +++++++++++++++++- .../components/team/TeamVirtualKeysTable.tsx | 133 ++++++++------ 4 files changed, 390 insertions(+), 61 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index fa12a75aacd..08eccc4ed2c 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -1,7 +1,82 @@ -import { teamListCall, organizationListCall, keyAliasesCall } from "../networking" +import { teamListCall, organizationListCall, keyAliasesCall, keyListCall } from "../networking"; import { Team } from "./key_list"; import { Organization } from "../networking"; +export interface TeamFilterOptions { + keyAliases: string[]; + organizationIds: string[]; + userIds: Array<{ id: string; email: string }>; +} + +/** + * Fetches filter options (key aliases, org IDs, user IDs) scoped to a team's keys. + * Used by TeamVirtualKeysTable to show only relevant filter options. + */ +export const fetchTeamFilterOptions = async ( + accessToken: string | null, + teamId: string, +): Promise => { + if (!accessToken || !teamId) { + return { keyAliases: [], organizationIds: [], userIds: [] }; + } + + try { + const keyAliases = new Set(); + const organizationIds = new Set(); + const userMap = new Map(); // user_id -> user_email + + let page = 1; + let totalPages = 1; + + do { + const response = await keyListCall( + accessToken, + null, + teamId, + null, + null, + null, + page, + 100, + null, + null, + "user", + null, + ); + + const keys = response?.keys || []; + totalPages = response?.total_pages ?? 1; + + for (const key of keys) { + const alias = key?.key_alias; + if (alias && typeof alias === "string") { + keyAliases.add(alias.trim()); + } + const orgId = key?.organization_id; + if (orgId && typeof orgId === "string") { + organizationIds.add(orgId.trim()); + } + const userId = key?.user_id; + if (userId && typeof userId === "string") { + const email = key?.user?.user_email || userId; + userMap.set(userId, email); + } + } + + page++; + } while (page <= totalPages); + + return { + keyAliases: Array.from(keyAliases).sort(), + organizationIds: Array.from(organizationIds).sort(), + userIds: Array.from(userMap.entries()).map(([id, email]) => ({ id, email })), + }; + } catch (error) { + console.error("Error fetching team filter options:", error); + return { keyAliases: [], organizationIds: [], userIds: [] }; + } +}; + /** * Fetches all key aliases via the dedicated /key/aliases endpoint * @param accessToken The access token for API authentication diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx index 75fe76902bd..317db7b37fd 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx @@ -110,6 +110,11 @@ vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({ })); vi.mock("../key_team_helpers/filter_helpers", () => ({ + fetchTeamFilterOptions: vi.fn().mockResolvedValue({ + keyAliases: [], + organizationIds: [], + userIds: [], + }), fetchAllKeyAliases: vi.fn().mockResolvedValue([]), fetchAllOrganizations: vi.fn().mockResolvedValue([]), })); @@ -597,8 +602,22 @@ describe("TeamInfoView", () => { it("should display X Members in Virtual Keys tab when navigated to", async () => { const user = userEvent.setup(); vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + const fiveKeys = Array.from({ length: 5 }, (_, i) => ({ + token: `sk-${i}`, + token_id: `key-${i}`, + key_alias: `key_${i}`, + key_name: `sk-...${i}`, + user_id: `user-${i}`, + organization_id: null, + user: { user_id: `user-${i}`, user_email: `user${i}@test.com` }, + created_at: "2024-01-01T00:00:00Z", + team_id: "123", + spend: 0, + max_budget: 100, + models: ["gpt-4"], + })); mockUseKeys.mockReturnValue({ - data: { keys: [], total_count: 5, current_page: 1, total_pages: 1 }, + data: { keys: fiveKeys, total_count: 5, current_page: 1, total_pages: 1 }, isPending: false, isFetching: false, refetch: vi.fn(), @@ -619,6 +638,56 @@ describe("TeamInfoView", () => { }); }); + it("should show Filters and pagination controls in Virtual Keys tab", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + mockUseKeys.mockReturnValue({ + data: { + keys: [ + { + token: "sk-1", + token_id: "key-1", + key_alias: "key1", + key_name: "sk-...1", + user_id: "user-1", + organization_id: null, + user: { user_id: "user-1", user_email: "user1@test.com" }, + created_at: "2024-01-01T00:00:00Z", + team_id: "123", + spend: 0, + max_budget: 100, + models: ["gpt-4"], + }, + ], + total_count: 1, + current_page: 1, + total_pages: 1, + }, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const virtualKeysTab = screen.getByRole("tab", { name: "Virtual Keys" }); + await user.click(virtualKeysTab); + + await waitFor(() => { + expect(screen.getByText("1 Member")).toBeInTheDocument(); + }); + expect(screen.getByRole("button", { name: "Filters" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Reset Filters" })).toBeInTheDocument(); + expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Previous" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Next" })).toBeInTheDocument(); + }); + it("should display object permissions when present", async () => { vi.mocked(networking.teamInfoCall).mockResolvedValue( createMockTeamData({ diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx index e4b7b00fb58..41df5611e07 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi, MockedFunction } from "vitest"; import { renderWithProviders } from "../../../tests/test-utils"; import { TeamVirtualKeysTable } from "./TeamVirtualKeysTable"; import { KeysResponse, useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; +import { fetchTeamFilterOptions } from "../key_team_helpers/filter_helpers"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { KeyResponse } from "../key_team_helpers/key_list"; import { Organization } from "../networking"; @@ -17,8 +18,11 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ })); vi.mock("../key_team_helpers/filter_helpers", () => ({ - fetchAllKeyAliases: vi.fn().mockResolvedValue([]), - fetchAllOrganizations: vi.fn().mockResolvedValue([]), + fetchTeamFilterOptions: vi.fn().mockResolvedValue({ + keyAliases: [], + organizationIds: [], + userIds: [], + }), })); vi.mock("../key_team_helpers/fetch_available_models_team_key", () => ({ @@ -139,7 +143,7 @@ describe("TeamVirtualKeysTable", () => { }); }); - it("should call useKeys with expand user to fetch user email", async () => { + it("should call useKeys with page, pageSize, and expand user for server-side pagination", async () => { renderWithProviders(); await waitFor(() => { @@ -189,4 +193,164 @@ describe("TeamVirtualKeysTable", () => { }); expect(screen.getByText("Key ID")).toBeInTheDocument(); }); + + it("should display keys in table when data is loaded", async () => { + mockUseKeys.mockReturnValue({ + data: { + keys: [ + createMockKey({ key_alias: "alice_key_team1" }), + createMockKey({ token: "sk-2", token_id: "key-2", key_alias: "bob_key_team1" }), + ], + total_count: 2, + current_page: 1, + total_pages: 1, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("2 Members")).toBeInTheDocument(); + }); + expect(screen.getByText("alice_key_team1")).toBeInTheDocument(); + expect(screen.getByText("bob_key_team1")).toBeInTheDocument(); + }); + + it("should show Page X of Y when multiple pages exist", async () => { + mockUseKeys.mockReturnValue({ + data: { + keys: [createMockKey()], + total_count: 100, + current_page: 1, + total_pages: 3, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Page 1 of 3")).toBeInTheDocument(); + }); + expect(screen.getByText("100 Members")).toBeInTheDocument(); + }); + + it("should fetch page 2 when Next is clicked", async () => { + const user = userEvent.setup(); + mockUseKeys.mockImplementation((page: number) => ({ + data: { + keys: page === 1 ? [createMockKey()] : [createMockKey({ token: "sk-page2", key_alias: "page2_key" })], + total_count: 100, + current_page: page, + total_pages: 3, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any)); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Page 1 of 3")).toBeInTheDocument(); + }); + + const nextButton = screen.getByRole("button", { name: "Next" }); + await user.click(nextButton); + + await waitFor(() => { + expect(mockUseKeys).toHaveBeenLastCalledWith( + 2, + 50, + expect.objectContaining({ teamID: "team-1" }) + ); + }); + }); + + it("should show Loading keys when isPending", async () => { + mockUseKeys.mockReturnValue({ + data: undefined, + isPending: true, + isFetching: true, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Loading keys...")).toBeInTheDocument(); + }); + }); + + it("should show No keys found when keys array is empty", async () => { + mockUseKeys.mockReturnValue({ + data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("0 Members")).toBeInTheDocument(); + }); + expect(screen.getByText("No keys found")).toBeInTheDocument(); + }); + + it("should fetch team-scoped filter options for Key Alias, Organization ID, and User ID", async () => { + const mockFetchTeamFilterOptions = vi.mocked(fetchTeamFilterOptions); + mockFetchTeamFilterOptions.mockResolvedValue({ + keyAliases: ["alice_key_team1", "charlie_key_team1"], + organizationIds: ["org-123"], + userIds: [ + { id: "user-1", email: "alice@example.com" }, + { id: "user-2", email: "charlie@example.com" }, + ], + }); + + // Use unique teamId to avoid cache hit from previous tests (refetchOnMount: false) + renderWithProviders( + + ); + + await waitFor(() => { + expect(mockFetchTeamFilterOptions).toHaveBeenCalledWith( + "test-token", + "team-filter-options-test" + ); + }); + }); + + it("should open Key Info View when key is clicked", async () => { + mockUseKeys.mockReturnValue({ + data: { + keys: [createMockKey({ token: "sk-click-me", key_alias: "clickable_key" })], + total_count: 1, + current_page: 1, + total_pages: 1, + } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("clickable_key")).toBeInTheDocument(); + }); + + const keyButton = screen.getByRole("button", { name: /sk-click-me|clickable_key/ }); + await userEvent.click(keyButton); + + await waitFor(() => { + expect(screen.getByText("Key Info View")).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index ce998d18677..ddbffa118b1 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -8,7 +8,6 @@ import { ColumnDef, flexRender, getCoreRowModel, - getPaginationRowModel, getSortedRowModel, PaginationState, SortingState, @@ -28,14 +27,14 @@ import { } from "@tremor/react"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Popover, Skeleton, Tooltip } from "antd"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; import { KeyResponse, Team } from "../key_team_helpers/key_list"; import FilterComponent, { FilterOption } from "../molecules/filter"; import { Organization } from "../networking"; import KeyInfoView from "../templates/key_info_view"; import { useQuery } from "@tanstack/react-query"; -import { fetchAllKeyAliases, fetchAllOrganizations } from "../key_team_helpers/filter_helpers"; +import { fetchTeamFilterOptions } from "../key_team_helpers/filter_helpers"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface TeamVirtualKeysTableProps { @@ -69,22 +68,24 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi const sortBy = sorting.length > 0 ? sorting[0].id : "created_at"; const sortOrder = sorting.length > 0 ? (sorting[0].desc ? "desc" : "asc") : "desc"; + const pageIndex = tablePagination.pageIndex; + const pageSize = tablePagination.pageSize; + const { data: keys, isPending: isLoading, isFetching, refetch, - } = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize, { + } = useKeys(pageIndex + 1, pageSize, { teamID: teamId, + organizationID: filters["Organization ID"]?.trim() || undefined, + selectedKeyAlias: filters["Key Alias"]?.trim() || undefined, + userID: filters["User ID"]?.trim() || undefined, sortBy: sortBy || undefined, sortOrder: sortOrder || undefined, - organizationID: filters["Organization ID"] || undefined, - selectedKeyAlias: filters["Key Alias"] || undefined, - userID: filters["User ID"] || undefined, expand: "user", }); - const totalCount = keys?.total_count || 0; const displayKeys = useMemo(() => { const kList = keys?.keys || []; const orgId = organization?.organization_id; @@ -94,6 +95,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi organization_id: k.organization_id || orgId, })); }, [keys?.keys, organization?.organization_id]); + + const totalCount = keys?.total_count ?? 0; + const pageCount = keys?.total_pages ?? 0; const [expandedAccordions, setExpandedAccordions] = useState>({}); const currentTeam: Team = useMemo( @@ -114,29 +118,27 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi [teamId, teamAlias, organization], ); - const allKeyAliasesQuery = useQuery({ - queryKey: ["allKeyAliases"], - queryFn: async () => fetchAllKeyAliases(accessToken), - enabled: !!accessToken, + const teamFilterOptionsQuery = useQuery({ + queryKey: ["teamFilterOptions", teamId], + queryFn: async () => fetchTeamFilterOptions(accessToken, teamId), + enabled: !!accessToken && !!teamId, }); - const allKeyAliases = allKeyAliasesQuery.data || []; + const teamFilterOptions = teamFilterOptionsQuery.data || { + keyAliases: [], + organizationIds: [], + userIds: [], + }; - const allOrganizationsQuery = useQuery({ - queryKey: ["allOrganizations"], - queryFn: async () => fetchAllOrganizations(accessToken), - enabled: !!accessToken, - }); - const allOrganizations = allOrganizationsQuery.data || []; - - useEffect(() => { - if (refetch) { - const handleStorageChange = () => refetch(); - window.addEventListener("storage", handleStorageChange); - return () => window.removeEventListener("storage", handleStorageChange); - } + const handleStorageChange = useCallback(() => { + refetch?.(); }, [refetch]); - const handleFilterChange = (newFilters: Record, skipDebounce = false) => { + useEffect(() => { + window.addEventListener("storage", handleStorageChange); + return () => window.removeEventListener("storage", handleStorageChange); + }, [handleStorageChange]); + + const handleFilterChange = useCallback((newFilters: Record, skipDebounce = false) => { setFilters((prev) => ({ ...prev, "Organization ID": newFilters["Organization ID"] ?? prev["Organization ID"], @@ -148,9 +150,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi if (!skipDebounce) { setTablePagination((prev) => ({ ...prev, pageIndex: 0 })); } - }; + }, []); - const handleFilterReset = () => { + const handleFilterReset = useCallback(() => { setFilters({ "Organization ID": "", "Key Alias": "", @@ -159,7 +161,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi "Sort Order": "desc", }); setTablePagination((prev) => ({ ...prev, pageIndex: 0 })); - }; + }, []); const filterOptions: FilterOption[] = useMemo( () => [ @@ -168,16 +170,13 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi label: "Organization ID", isSearchable: true, searchFn: async (searchText: string) => { - if (!allOrganizations.length) return []; - const filtered = allOrganizations.filter( - (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false, - ); - return filtered - .filter((org) => org.organization_id != null) - .map((org) => ({ - label: `${org.organization_id || "Unknown"} (${org.organization_id})`, - value: org.organization_id as string, - })); + const { organizationIds } = teamFilterOptions; + if (!organizationIds.length) return []; + const lower = searchText.toLowerCase(); + const filtered = lower + ? organizationIds.filter((id) => id.toLowerCase().includes(lower)) + : organizationIds; + return filtered.map((id) => ({ label: id, value: id })); }, }, { @@ -185,15 +184,35 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi label: "Key Alias", isSearchable: true, searchFn: async (searchText: string) => { - const filtered = allKeyAliases.filter((alias) => - alias.toLowerCase().includes(searchText.toLowerCase()), - ); + const { keyAliases } = teamFilterOptions; + const lower = searchText.toLowerCase(); + const filtered = lower + ? keyAliases.filter((alias) => alias.toLowerCase().includes(lower)) + : keyAliases; return filtered.map((alias) => ({ label: alias, value: alias })); }, }, - { name: "User ID", label: "User ID", isSearchable: false }, + { + name: "User ID", + label: "User ID", + isSearchable: true, + searchFn: async (searchText: string) => { + const { userIds } = teamFilterOptions; + const lower = searchText.toLowerCase(); + const filtered = lower + ? userIds.filter( + (u) => + u.id.toLowerCase().includes(lower) || u.email.toLowerCase().includes(lower), + ) + : userIds; + return filtered.map((u) => ({ + label: u.email ? `${u.id} (${u.email})` : u.id, + value: u.id, + })); + }, + }, ], - [allOrganizations, allKeyAliases], + [teamFilterOptions], ); const columns: ColumnDef[] = useMemo( @@ -521,13 +540,8 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi [expandedAccordions], ); - const table = useReactTable({ - data: displayKeys, - columns, - columnResizeMode: "onChange", - columnResizeDirection: "ltr", - state: { sorting, pagination: tablePagination }, - onSortingChange: (updaterOrValue) => { + const handleSortingChange = useCallback( + (updaterOrValue: React.SetStateAction) => { const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; setSorting(newSorting); @@ -535,7 +549,6 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi const sortState = newSorting[0]; handleFilterChange( { - ...filters, "Sort By": sortState.id, "Sort Order": sortState.desc ? "desc" : "asc", }, @@ -543,17 +556,25 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi ); } }, + [sorting, handleFilterChange], + ); + + const table = useReactTable({ + data: displayKeys, + columns, + columnResizeMode: "onChange", + columnResizeDirection: "ltr", + state: { sorting, pagination: tablePagination }, + onSortingChange: handleSortingChange, onPaginationChange: setTablePagination, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), enableSorting: true, manualSorting: false, manualPagination: true, - pageCount: Math.ceil(totalCount / tablePagination.pageSize), + pageCount: pageCount, }); - const { pageIndex, pageSize } = table.getState().pagination; return (
{selectedKey ? ( From f44c36f9802bc46bbd4b646924aed8ddeea82f2a Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Mon, 23 Feb 2026 15:43:04 -0800 Subject: [PATCH 007/171] Fix Unbounded pagination --- .../src/components/key_team_helpers/filter_helpers.ts | 6 ++++-- .../src/components/team/TeamVirtualKeysTable.tsx | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index 08eccc4ed2c..dd3766592bf 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -25,6 +25,8 @@ export const fetchTeamFilterOptions = async ( const organizationIds = new Set(); const userMap = new Map(); // user_id -> user_email + const MAX_PAGES = 1; // Cap at 20 keys on load to avoid heavy fetches + const PAGE_SIZE = 20; let page = 1; let totalPages = 1; @@ -37,7 +39,7 @@ export const fetchTeamFilterOptions = async ( null, null, page, - 100, + PAGE_SIZE, null, null, "user", @@ -64,7 +66,7 @@ export const fetchTeamFilterOptions = async ( } page++; - } while (page <= totalPages); + } while (page <= totalPages && page <= MAX_PAGES); return { keyAliases: Array.from(keyAliases).sort(), diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index ddbffa118b1..13d7885d672 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -122,6 +122,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi queryKey: ["teamFilterOptions", teamId], queryFn: async () => fetchTeamFilterOptions(accessToken, teamId), enabled: !!accessToken && !!teamId, + staleTime: 30000, // 30 seconds - align with useKeys }); const teamFilterOptions = teamFilterOptionsQuery.data || { keyAliases: [], From bc99e38e55348aaab5db5fcef3adb1f9ba44faa3 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Mon, 23 Feb 2026 15:44:51 -0800 Subject: [PATCH 008/171] Fix: accessToken not included in queryKey --- .../src/components/team/TeamVirtualKeysTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index 13d7885d672..74100069779 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -119,7 +119,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi ); const teamFilterOptionsQuery = useQuery({ - queryKey: ["teamFilterOptions", teamId], + queryKey: ["teamFilterOptions", teamId, accessToken], queryFn: async () => fetchTeamFilterOptions(accessToken, teamId), enabled: !!accessToken && !!teamId, staleTime: 30000, // 30 seconds - align with useKeys From a1f5450b4001555b04ce51044004580ced8e3cb1 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Mon, 23 Feb 2026 16:01:04 -0800 Subject: [PATCH 009/171] manualSorting fix. Filtering fetching fix --- .../components/key_team_helpers/filter_helpers.ts | 14 ++++++++------ .../src/components/team/TeamVirtualKeysTable.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index dd3766592bf..0a5590d78ce 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -8,9 +8,13 @@ export interface TeamFilterOptions { userIds: Array<{ id: string; email: string }>; } +const FILTER_OPTIONS_PAGE_SIZE = 100; // API max per page +const MAX_PAGES = 50; // Cap at 5000 keys to avoid unbounded fetches + /** - * Fetches filter options (key aliases, org IDs, user IDs) scoped to a team's keys. - * Used by TeamVirtualKeysTable to show only relevant filter options. + * Fetches filter options (key aliases, org IDs, user IDs) from all team keys. + * Paginates through pages to build complete dropdowns. Capped at 50 pages + * (5000 keys) to limit load for very large teams. */ export const fetchTeamFilterOptions = async ( accessToken: string | null, @@ -23,10 +27,8 @@ export const fetchTeamFilterOptions = async ( try { const keyAliases = new Set(); const organizationIds = new Set(); - const userMap = new Map(); // user_id -> user_email + const userMap = new Map(); - const MAX_PAGES = 1; // Cap at 20 keys on load to avoid heavy fetches - const PAGE_SIZE = 20; let page = 1; let totalPages = 1; @@ -39,7 +41,7 @@ export const fetchTeamFilterOptions = async ( null, null, page, - PAGE_SIZE, + FILTER_OPTIONS_PAGE_SIZE, null, null, "user", diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index 74100069779..de3061a003d 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -571,7 +571,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), enableSorting: true, - manualSorting: false, + manualSorting: true, // Server sorts via useKeys. Avoid redundant client-side sort manualPagination: true, pageCount: pageCount, }); From 18bc0d4ec94f04bc3b284003f43a90f6d15a39e4 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Fri, 27 Feb 2026 13:37:23 -0800 Subject: [PATCH 010/171] reusable-credentials --- .../handle_add_model_submit.test.tsx | 24 +++++++ .../add_model/handle_add_model_submit.tsx | 3 + .../src/components/model_info_view.test.tsx | 63 +++++++++++++++++++ .../src/components/model_info_view.tsx | 57 ++++++++++++++++- 4 files changed, 146 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx index 5cf7a18634e..c261e505684 100644 --- a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx @@ -55,4 +55,28 @@ describe("prepareModelAddRequest", () => { const [deployment] = deployments!; expect(deployment.litellmParamsObj.custom_llm_provider).toBe("petals"); }); + + it("ignores litellm_credential_name inside LiteLLM Params JSON", async () => { + const formValues = { + model_mappings: [ + { + public_name: "Public Model", + litellm_model: "litellm/public", + }, + ], + model_name: "custom-model-name", + litellm_credential_name: "selected-credential", + litellm_extra_params: JSON.stringify({ + litellm_credential_name: "from-json", + timeout: 5, + }), + }; + + const deployments = await prepareModelAddRequest({ ...formValues }, "token", null); + + expect(deployments).toHaveLength(1); + const [deployment] = deployments!; + expect(deployment.litellmParamsObj.litellm_credential_name).toBe("selected-credential"); + expect(deployment.litellmParamsObj.timeout).toBe(5); + }); }); diff --git a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx index 1d8c980c5ae..5137a302dd5 100644 --- a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx +++ b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx @@ -91,6 +91,9 @@ export const prepareModelAddRequest = async (formValues: Record, ac if (value && value != undefined) { try { litellmExtraParams = JSON.parse(value); + if ("litellm_credential_name" in litellmExtraParams) { + delete litellmExtraParams.litellm_credential_name; + } } catch (error) { NotificationManager.fromBackend("Failed to parse LiteLLM Extra Params: " + error); throw new Error("Failed to parse litellm_extra_params: " + error); diff --git a/ui/litellm-dashboard/src/components/model_info_view.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.test.tsx index 7158c452d94..b2cbbda34aa 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -23,6 +23,7 @@ vi.mock("./molecules/notifications_manager", () => ({ vi.mock("./networking", () => ({ modelInfoV1Call: vi.fn(), credentialGetCall: vi.fn(), + credentialListCall: vi.fn(), getGuardrailsList: vi.fn(), tagListCall: vi.fn(), testConnectionRequest: vi.fn(), @@ -47,6 +48,7 @@ vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ const mockNotificationsManager = vi.mocked(NotificationsManager); const mockModelInfoV1Call = vi.mocked(networking.modelInfoV1Call); const mockCredentialGetCall = vi.mocked(networking.credentialGetCall); +const mockCredentialListCall = vi.mocked(networking.credentialListCall); const mockGetGuardrailsList = vi.mocked(networking.getGuardrailsList); const mockTagListCall = vi.mocked(networking.tagListCall); const mockTestConnectionRequest = vi.mocked(networking.testConnectionRequest); @@ -63,6 +65,7 @@ describe("ModelInfoView", () => { model: "gpt-4", api_base: "https://api.openai.com/v1", custom_llm_provider: "openai", + litellm_credential_name: "selected-credential", }, model_info: { id: "123", @@ -125,6 +128,15 @@ describe("ModelInfoView", () => { credential_values: {}, credential_info: {}, }); + mockCredentialListCall.mockResolvedValue({ + credentials: [ + { + credential_name: "selected-credential", + credential_values: {}, + credential_info: {}, + }, + ], + }); mockGetGuardrailsList.mockResolvedValue({ guardrails: [{ guardrail_name: "content_filter" }, { guardrail_name: "toxicity_filter" }], @@ -489,6 +501,57 @@ describe("ModelInfoView", () => { }); }); + it("should show existing credentials field in edit mode", async () => { + const user = userEvent.setup(); + render(, { wrapper }); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + await user.click(screen.getByRole("button", { name: /edit settings/i })); + + await waitFor(() => { + expect(screen.getByText("Existing Credentials")).toBeInTheDocument(); + }); + }); + + it("should keep selector credential and ignore litellm_credential_name from LiteLLM Params json", async () => { + const user = userEvent.setup(); + render(, { wrapper }); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + await user.click(screen.getByRole("button", { name: /edit settings/i })); + + const litellmParamsInput = screen + .getAllByRole("textbox") + .find( + (input) => + input.tagName === "TEXTAREA" && + (input as HTMLTextAreaElement).value.includes('"custom_llm_provider"'), + ); + expect(litellmParamsInput).toBeDefined(); + if (!litellmParamsInput) { + return; + } + expect((litellmParamsInput as HTMLTextAreaElement).value).not.toContain("litellm_credential_name"); + await user.clear(litellmParamsInput); + await user.paste(`{"litellm_credential_name":"from-json","timeout":42}`); + + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => { + expect(mockModelPatchUpdateCall).toHaveBeenCalled(); + }); + + const updatePayload = mockModelPatchUpdateCall.mock.calls[0][1]; + expect(updatePayload.litellm_params.litellm_credential_name).toBe("selected-credential"); + expect(updatePayload.litellm_params.litellm_credential_name).not.toBe("from-json"); + }); + it("should display health check model field for wildcard models", async () => { const wildcardModelData = { ...defaultModelData, diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index e2fc8caa21c..0ba86e9f243 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -30,6 +30,7 @@ import { CredentialItem, credentialCreateCall, credentialGetCall, + credentialListCall, getGuardrailsList, modelDeleteCall, modelInfoV1Call, @@ -75,6 +76,7 @@ export default function ModelInfoView({ const [isAutoRouterModalOpen, setIsAutoRouterModalOpen] = useState(false); const [guardrailsList, setGuardrailsList] = useState([]); const [tagsList, setTagsList] = useState>({}); + const [credentialsList, setCredentialsList] = useState([]); // Fetch model data using hook const { data: rawModelDataResponse, isLoading: isLoadingModel } = useModelsInfo(1, 50, undefined, modelId); @@ -191,10 +193,21 @@ export default function ModelInfoView({ } }; + const fetchCredentials = async () => { + if (!accessToken) return; + try { + const response = await credentialListCall(accessToken); + setCredentialsList(response.credentials || []); + } catch (error) { + console.error("Failed to fetch credentials:", error); + } + }; + getExistingCredential(); getModelInfo(); fetchGuardrails(); fetchTags(); + fetchCredentials(); }, [accessToken, modelId]); const handleReuseCredential = async (values: any) => { @@ -220,6 +233,7 @@ export default function ModelInfoView({ let parsedExtraParams: Record = {}; try { parsedExtraParams = values.litellm_extra_params ? JSON.parse(values.litellm_extra_params) : {}; + delete parsedExtraParams.litellm_credential_name; } catch (e) { NotificationsManager.fromBackend("Invalid JSON in LiteLLM Params"); setIsSaving(false); @@ -242,6 +256,11 @@ export default function ModelInfoView({ output_cost_per_token: values.output_cost / 1_000_000, tags: values.tags, }; + if (values.litellm_credential_name) { + updatedLitellmParams.litellm_credential_name = values.litellm_credential_name; + } else { + delete updatedLitellmParams.litellm_credential_name; + } if (values.guardrails) { updatedLitellmParams.guardrails = values.guardrails; } @@ -608,7 +627,16 @@ export default function ModelInfoView({ : [], tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [], health_check_model: isWildcardModel ? localModelData.model_info?.health_check_model : null, - litellm_extra_params: JSON.stringify(localModelData.litellm_params || {}, null, 2), + litellm_credential_name: localModelData.litellm_params?.litellm_credential_name || undefined, + litellm_extra_params: JSON.stringify( + Object.fromEntries( + Object.entries(localModelData.litellm_params || {}).filter( + ([key]) => key !== "litellm_credential_name", + ), + ), + null, + 2, + ), }} layout="vertical" onValuesChange={() => setIsDirty(true)} @@ -930,6 +958,33 @@ export default function ModelInfoView({
)} +
+ Existing Credentials + {isEditing ? ( + + setObjectIdSearch(e.target.value)} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- - - - - -
- {/* Custom Action Filter Dropdown */} -
- - - {actionFilterOpen && ( -
-
- {[ - { label: "All Actions", value: "all" }, - { label: "Created", value: "created" }, - { label: "Updated", value: "updated" }, - { label: "Deleted", value: "deleted" }, - { label: "Rotated", value: "rotated" }, - ].map((option) => ( - - ))} -
-
- )} -
- - {/* Custom Table Filter Dropdown */} -
- - - {tableFilterOpen && ( -
-
- {[ - { label: "All Tables", value: "all" }, - { label: "Keys", value: "keys" }, - { label: "Teams", value: "teams" }, - { label: "Users", value: "users" }, - ].map((option) => ( - - ))} -
-
- )} -
- - - Showing {allLogsQuery.isLoading ? "..." : currentDisplayItemsStart} -{" "} - {allLogsQuery.isLoading ? "..." : currentDisplayItemsEnd} of{" "} - {allLogsQuery.isLoading ? "..." : totalFilteredItems} results - -
- - Page {allLogsQuery.isLoading ? "..." : clientCurrentPage} of{" "} - {allLogsQuery.isLoading ? "..." : totalFilteredPages} - - - -
-
+ {/* Filters */} +
+ { + setObjectId(val); + handleFilterChange(); + }} + onChange={(e) => { + if (!e.target.value) { + setObjectId(""); + handleFilterChange(); + } + }} + /> + { + setChangedBy(val); + handleFilterChange(); + }} + onChange={(e) => { + if (!e.target.value) { + setChangedBy(""); + handleFilterChange(); + } + }} + /> + { + setTeamId(val); + handleFilterChange(); + }} + onChange={(e) => { + if (!e.target.value) { + setTeamId(""); + handleFilterChange(); + } + }} + /> + { + setKeyHash(val); + handleFilterChange(); + }} + onChange={(e) => { + if (!e.target.value) { + setKeyHash(""); + handleFilterChange(); + } + }} + /> + { + setTableName(val); + handleFilterChange(); + }} + />
- true} + + {/* Table */} + + columns={columns} + dataSource={auditLogs} + rowKey="id" + loading={query.isLoading} + size="small" + onRow={(record) => ({ + onClick: () => handleRowClick(record), + style: { cursor: "pointer" }, + })} + pagination={{ + current: page, + pageSize: PAGE_SIZE, + total, + showTotal: (t) => `${t} total`, + showSizeChanger: false, + onChange: (p) => setPage(p), + }} + onChange={handleTableChange} /> + + setDrawerOpen(false)} + log={selectedLog} + /> ); } From 963390928d3be222170178848b20703d8f861391 Mon Sep 17 00:00:00 2001 From: Harshit Jain <48647625+Harshit28j@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:08:40 +0530 Subject: [PATCH 017/171] Update litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../proxy/management_endpoints/jwt_key_mapping_endpoints.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py index 08ca00e66d4..6c19f8c5dd4 100644 --- a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py +++ b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py @@ -55,7 +55,9 @@ async def update_jwt_key_mapping( if prisma_client is None: raise HTTPException(status_code=500, detail="Database not connected") - update_data = data.model_dump(exclude_unset=True, exclude={"id"}) + update_data = data.model_dump(exclude_unset=True, exclude={"id", "key"}) + if data.key is not None: + update_data["token"] = hash_token(data.key) try: # Get old mapping for cache invalidation From 0f9d3808748b74a35539c4ad1bf6dcfb1bc395cf Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Sun, 1 Mar 2026 13:28:06 +0530 Subject: [PATCH 018/171] fix: add pagination to jwt key mapping list endpoint Add page/size query params with take/skip to prevent unbounded queries. Returns paginated response with total_count, current_page, total_pages. Co-Authored-By: Claude Opus 4.6 --- .../jwt_key_mapping_endpoints.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py index 6c19f8c5dd4..770b9a48dd5 100644 --- a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py +++ b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from litellm.proxy._types import * from litellm.proxy._types import hash_token from litellm.proxy.auth.user_api_key_auth import user_api_key_auth @@ -118,6 +118,8 @@ async def delete_jwt_key_mapping( @router.get("/jwt/key/mapping/list", tags=["JWT Key Mapping"]) async def list_jwt_key_mappings( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + page: int = Query(1, description="Page number", ge=1), + size: int = Query(50, description="Page size", ge=1, le=100), ): from litellm.proxy.proxy_server import prisma_client @@ -130,8 +132,19 @@ async def list_jwt_key_mappings( raise HTTPException(status_code=500, detail="Database not connected") try: - mappings = await prisma_client.db.litellm_jwtkeymapping.find_many() - return mappings + skip = (page - 1) * size + mappings = await prisma_client.db.litellm_jwtkeymapping.find_many( + skip=skip, + take=size, + order={"created_at": "desc"}, + ) + total_count = await prisma_client.db.litellm_jwtkeymapping.count() + return { + "mappings": mappings, + "total_count": total_count, + "current_page": page, + "total_pages": -(-total_count // size), # ceiling division + } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) From 0e2dd4aac1a71623a72af4b2199731c27f5eaccc Mon Sep 17 00:00:00 2001 From: Harshit Jain <48647625+Harshit28j@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:43:06 +0530 Subject: [PATCH 019/171] Update litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../proxy/management_endpoints/jwt_key_mapping_endpoints.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py index 770b9a48dd5..9680a92ae67 100644 --- a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py +++ b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py @@ -22,12 +22,13 @@ async def create_jwt_key_mapping( raise HTTPException(status_code=500, detail="Database not connected") try: - hashed_key = hash_token(data.key) + try: new_mapping = await prisma_client.db.litellm_jwtkeymapping.create( data={ "jwt_claim_name": data.jwt_claim_name, "jwt_claim_value": data.jwt_claim_value, - "token": hashed_key, + "token": data.key, + "is_active": True, } ) From 911ba14e45509a250b19ca7c480dad188ac1ccca Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Sun, 1 Mar 2026 14:28:50 +0530 Subject: [PATCH 020/171] fix: address remaining greptile feedback for jwt key mapping - Persist description field on create (was silently dropped) - Remove phantom key_alias from JWTKeyMappingResponse (not in schema) - Populate created_by/updated_by audit fields from authenticated user - Pass actual jwt_valid_token in admin path instead of empty dict - Restore hash_token on create and fix duplicate try block Co-Authored-By: Claude Opus 4.6 --- litellm/proxy/_types.py | 3 ++- litellm/proxy/auth/handle_jwt.py | 5 +++-- .../jwt_key_mapping_endpoints.py | 20 ++++++++++++------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 6440bf0ed81..6b51df709f7 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3692,11 +3692,12 @@ class JWTKeyMappingResponse(LiteLLMPydanticObjectBase): jwt_claim_name: str jwt_claim_value: str token: str - key_alias: Optional[str] = None description: Optional[str] = None is_active: bool created_at: datetime updated_at: datetime + created_by: Optional[str] = None + updated_by: Optional[str] = None class SpecialHeaders(enum.Enum): diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index d3b028f63f9..6ca7b290a07 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -831,6 +831,7 @@ class JWTAuthManager: user_id: Optional[str], org_id: Optional[str], api_key: str, + jwt_valid_token: Optional[dict] = None, ) -> Optional[JWTAuthBuilderResult]: """Check admin status and route access permissions""" if not jwt_handler.is_admin(scopes=scopes): @@ -860,7 +861,7 @@ class JWTAuthManager: end_user_id=None, org_id=org_id, team_membership=None, - jwt_claims={}, + jwt_claims=jwt_valid_token or {}, ) @staticmethod @@ -1368,7 +1369,7 @@ class JWTAuthManager: # Check admin access admin_result = await JWTAuthManager.check_admin_access( - jwt_handler, scopes, route, user_id, org_id, api_key + jwt_handler, scopes, route, user_id, org_id, api_key, jwt_valid_token ) if admin_result: return admin_result diff --git a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py index 9680a92ae67..c5d91d3699b 100644 --- a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py +++ b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py @@ -22,14 +22,19 @@ async def create_jwt_key_mapping( raise HTTPException(status_code=500, detail="Database not connected") try: - try: + hashed_key = hash_token(data.key) + create_data = { + "jwt_claim_name": data.jwt_claim_name, + "jwt_claim_value": data.jwt_claim_value, + "token": hashed_key, + "created_by": user_api_key_dict.user_id, + "updated_by": user_api_key_dict.user_id, + } + if data.description is not None: + create_data["description"] = data.description + new_mapping = await prisma_client.db.litellm_jwtkeymapping.create( - data={ - "jwt_claim_name": data.jwt_claim_name, - "jwt_claim_value": data.jwt_claim_value, - "token": data.key, - "is_active": True, - } + data=create_data ) # Invalidate cache @@ -59,6 +64,7 @@ async def update_jwt_key_mapping( update_data = data.model_dump(exclude_unset=True, exclude={"id", "key"}) if data.key is not None: update_data["token"] = hash_token(data.key) + update_data["updated_by"] = user_api_key_dict.user_id try: # Get old mapping for cache invalidation From 28a48acce645deaee4b53b485f1ad9eb022cfc70 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Sun, 1 Mar 2026 16:46:49 +0530 Subject: [PATCH 021/171] fix: add @log_db_metrics and move jwt mapping before auth_builder - Add @log_db_metrics decorator to get_jwt_key_mapping_object for consistent DB latency/error tracking with other helpers - Move virtual key mapping lookup before auth_builder() to avoid unnecessary team/user/org DB queries when mapping resolves - JWT is decoded early; auth_builder only runs when no mapping found Co-Authored-By: Claude Opus 4.6 --- litellm/proxy/auth/auth_checks.py | 1 + litellm/proxy/auth/user_api_key_auth.py | 37 +++++++++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index a7867fa08c7..e3776f2bfb7 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -2028,6 +2028,7 @@ async def _fetch_key_object_from_db_with_reconnect( raise +@log_db_metrics async def get_jwt_key_mapping_object( jwt_claim_name: str, jwt_claim_value: str, diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 7098b5e18d6..d6dad5167ca 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -660,24 +660,18 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 is_jwt = jwt_handler.is_jwt(token=api_key) verbose_proxy_logger.debug("is_jwt: %s", is_jwt) if is_jwt: - result = await JWTAuthManager.auth_builder( - request_data=request_data, - general_settings=general_settings, - api_key=api_key, - jwt_handler=jwt_handler, - route=route, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, - parent_otel_span=parent_otel_span, - request_headers=_safe_get_request_headers(request), - ) - - # JWT-to-Virtual-Key Mapping lookup + # Try JWT-to-Virtual-Key mapping first to avoid + # unnecessary DB queries in auth_builder do_standard_jwt_auth = True if jwt_handler.litellm_jwtauth.virtual_key_claim_field is not None: + # Decode JWT to get claims without running full auth_builder + if jwt_handler.litellm_jwtauth.oidc_userinfo_enabled: + jwt_claims = await jwt_handler.get_oidc_userinfo(token=api_key) + else: + jwt_claims = await jwt_handler.auth_jwt(token=api_key) + valid_token = await _resolve_jwt_to_virtual_key( - jwt_claims=result["jwt_claims"], + jwt_claims=jwt_claims, jwt_handler=jwt_handler, prisma_client=prisma_client, user_api_key_cache=user_api_key_cache, @@ -690,6 +684,19 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 # Fall through to virtual key checks if do_standard_jwt_auth: + result = await JWTAuthManager.auth_builder( + request_data=request_data, + general_settings=general_settings, + api_key=api_key, + jwt_handler=jwt_handler, + route=route, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + parent_otel_span=parent_otel_span, + request_headers=_safe_get_request_headers(request), + ) + is_proxy_admin = result["is_proxy_admin"] team_id = result["team_id"] team_object = result["team_object"] From 5655cb87fc7e95b68e70f4018fab5ce4b0b5d0a8 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 2 Mar 2026 08:11:07 +0000 Subject: [PATCH 022/171] fix: pass all custom pricing fields to register_model in completion() and embedding() Previously, register_model() was called with only input_cost_per_token, output_cost_per_token, and litellm_provider. This dropped ~40+ other pricing fields from CustomPricingLiteLLMParams (cache_read_input_token_cost, cache_creation_input_token_cost, output_cost_per_reasoning_token, etc.) as well as model_info metadata (mode, supports_prompt_caching, max_tokens). For DB-sourced custom-priced models, the first request after a pod restart would register a partial entry in litellm.model_cost, causing cost calculations to miss cache token discounts and other extended pricing until the entry was later enriched by deployment_callback_on_success mutating the lru_cache. Changes: - Add _build_custom_pricing_entry() helper that iterates over all CustomPricingLiteLLMParams.model_fields and merges model_info metadata - Replace hardcoded 3-field dicts in both completion() and embedding() with the new helper - Add 7 tests covering field collection, model_info merging, precedence, None skipping, and end-to-end register_model behavior Co-authored-by: openhands --- litellm/main.py | 80 ++++---- .../test_register_model_custom_pricing.py | 180 ++++++++++++++++++ 2 files changed, 223 insertions(+), 37 deletions(-) create mode 100644 tests/test_litellm/test_register_model_custom_pricing.py diff --git a/litellm/main.py b/litellm/main.py index cb3ddc2f401..737cf50ad24 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -107,6 +107,7 @@ from litellm.realtime_api.main import _realtime_health_check from litellm.secret_managers.main import get_secret_bool, get_secret_str from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import ( + CustomPricingLiteLLMParams, ModelResponseStream, RawRequestTypedDict, StreamingChoices, @@ -996,6 +997,32 @@ def _drop_input_examples_from_tools( return cleaned_tools +def _build_custom_pricing_entry( + custom_llm_provider: str, + kwargs: dict, + model_info: Optional[dict] = None, +) -> dict: + """Build a complete model cost entry from kwargs and model_info. + + Collects all CustomPricingLiteLLMParams fields present in kwargs and + merges metadata from model_info (mode, supports_prompt_caching, max_tokens) + so that register_model() receives the full pricing configuration. + """ + entry: dict = {"litellm_provider": custom_llm_provider} + + for field_name in CustomPricingLiteLLMParams.model_fields: + value = kwargs.get(field_name) + if value is not None: + entry[field_name] = value + + if model_info and isinstance(model_info, dict): + for key in ("mode", "supports_prompt_caching", "max_tokens"): + if key in model_info and model_info[key] is not None: + entry.setdefault(key, model_info[key]) + + return entry + + @tracer.wrap() @client def completion( # type: ignore # noqa: PLR0915 @@ -1351,27 +1378,16 @@ def completion( # type: ignore # noqa: PLR0915 timeout = float(timeout) # type: ignore ### REGISTER CUSTOM MODEL PRICING -- IF GIVEN ### - if input_cost_per_token is not None and output_cost_per_token is not None: + if ( + input_cost_per_token is not None and output_cost_per_token is not None + ) or input_cost_per_second is not None: litellm.register_model( { - f"{custom_llm_provider}/{model}": { - "input_cost_per_token": input_cost_per_token, - "output_cost_per_token": output_cost_per_token, - "litellm_provider": custom_llm_provider, - } - } - ) - elif ( - input_cost_per_second is not None - ): # time based pricing just needs cost in place - output_cost_per_second = output_cost_per_second - litellm.register_model( - { - f"{custom_llm_provider}/{model}": { - "input_cost_per_second": input_cost_per_second, - "output_cost_per_second": output_cost_per_second, - "litellm_provider": custom_llm_provider, - } + f"{custom_llm_provider}/{model}": _build_custom_pricing_entry( + custom_llm_provider=custom_llm_provider, + kwargs=kwargs, + model_info=model_info, + ) } ) ### BUILD CUSTOM PROMPT TEMPLATE -- IF GIVEN ### @@ -4644,7 +4660,6 @@ def embedding( # noqa: PLR0915 input_cost_per_token = kwargs.get("input_cost_per_token", None) output_cost_per_token = kwargs.get("output_cost_per_token", None) input_cost_per_second = kwargs.get("input_cost_per_second", None) - output_cost_per_second = kwargs.get("output_cost_per_second", None) openai_params = [ "user", "dimensions", @@ -4694,25 +4709,16 @@ def embedding( # noqa: PLR0915 ) ### REGISTER CUSTOM MODEL PRICING -- IF GIVEN ### - if input_cost_per_token is not None and output_cost_per_token is not None: + if ( + input_cost_per_token is not None and output_cost_per_token is not None + ) or input_cost_per_second is not None: litellm.register_model( { - f"{custom_llm_provider}/{model}": { - "input_cost_per_token": input_cost_per_token, - "output_cost_per_token": output_cost_per_token, - "litellm_provider": custom_llm_provider, - } - } - ) - if input_cost_per_second is not None: # time based pricing just needs cost in place - output_cost_per_second = output_cost_per_second or 0.0 - litellm.register_model( - { - f"{custom_llm_provider}/{model}": { - "input_cost_per_second": input_cost_per_second, - "output_cost_per_second": output_cost_per_second, - "litellm_provider": custom_llm_provider, - } + f"{custom_llm_provider}/{model}": _build_custom_pricing_entry( + custom_llm_provider=custom_llm_provider, + kwargs=kwargs, + model_info=kwargs.get("model_info"), + ) } ) diff --git a/tests/test_litellm/test_register_model_custom_pricing.py b/tests/test_litellm/test_register_model_custom_pricing.py new file mode 100644 index 00000000000..fa023d9943e --- /dev/null +++ b/tests/test_litellm/test_register_model_custom_pricing.py @@ -0,0 +1,180 @@ +""" +Test that register_model() in completion() and embedding() passes all +custom pricing fields from kwargs and model_info, not just the base +input/output costs. + +Previously, only input_cost_per_token, output_cost_per_token, and +litellm_provider were forwarded. Fields like cache_read_input_token_cost, +mode, and supports_prompt_caching were dropped, causing incorrect cost +calculations for DB-sourced models with prompt caching pricing. +""" + +import os +import sys + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.main import _build_custom_pricing_entry + + +def test_build_custom_pricing_entry_includes_all_kwargs_fields(): + """All CustomPricingLiteLLMParams fields present in kwargs should be + included in the resulting entry dict.""" + kwargs = { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "cache_read_input_token_cost": 0.00025, + "cache_creation_input_token_cost": 0.005, + "output_cost_per_reasoning_token": 0.01, + "input_cost_per_audio_token": 0.003, + "unrelated_kwarg": "should_be_ignored", + } + + entry = _build_custom_pricing_entry( + custom_llm_provider="openai", + kwargs=kwargs, + ) + + assert entry["litellm_provider"] == "openai" + assert entry["input_cost_per_token"] == 0.001 + assert entry["output_cost_per_token"] == 0.002 + assert entry["cache_read_input_token_cost"] == 0.00025 + assert entry["cache_creation_input_token_cost"] == 0.005 + assert entry["output_cost_per_reasoning_token"] == 0.01 + assert entry["input_cost_per_audio_token"] == 0.003 + assert "unrelated_kwarg" not in entry + + +def test_build_custom_pricing_entry_merges_model_info_metadata(): + """Fields from model_info (mode, supports_prompt_caching, max_tokens) + should be merged into the entry when present.""" + kwargs = { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + } + model_info = { + "id": "deployment-123", + "mode": "chat", + "supports_prompt_caching": True, + "max_tokens": 128000, + } + + entry = _build_custom_pricing_entry( + custom_llm_provider="openai", + kwargs=kwargs, + model_info=model_info, + ) + + assert entry["mode"] == "chat" + assert entry["supports_prompt_caching"] is True + assert entry["max_tokens"] == 128000 + + +def test_build_custom_pricing_entry_kwargs_take_precedence_over_model_info(): + """If a field appears in both kwargs and model_info, the kwargs value + should take precedence (setdefault behavior).""" + kwargs = { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + } + model_info = { + "mode": "chat", + "supports_prompt_caching": True, + } + + entry = _build_custom_pricing_entry( + custom_llm_provider="openai", + kwargs=kwargs, + model_info=model_info, + ) + + # model_info fields should be set via setdefault + assert entry["mode"] == "chat" + assert entry["supports_prompt_caching"] is True + + +def test_build_custom_pricing_entry_skips_none_values(): + """Fields with None values in kwargs should not be included.""" + kwargs = { + "input_cost_per_token": 0.001, + "output_cost_per_token": None, # explicitly None + "cache_read_input_token_cost": None, + } + + entry = _build_custom_pricing_entry( + custom_llm_provider="openai", + kwargs=kwargs, + ) + + assert entry["input_cost_per_token"] == 0.001 + assert "output_cost_per_token" not in entry + assert "cache_read_input_token_cost" not in entry + + +def test_build_custom_pricing_entry_handles_no_model_info(): + """Should work correctly when model_info is None.""" + kwargs = { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + } + + entry = _build_custom_pricing_entry( + custom_llm_provider="openai", + kwargs=kwargs, + model_info=None, + ) + + assert entry["litellm_provider"] == "openai" + assert entry["input_cost_per_token"] == 0.001 + assert entry["output_cost_per_token"] == 0.002 + assert "mode" not in entry + + +def test_register_model_receives_cache_pricing_fields(): + """End-to-end: when register_model is called with a full pricing entry, + the cache pricing fields should be present in litellm.model_cost.""" + model_key = "openai/test-custom-model-with-cache-pricing" + + litellm.register_model( + { + model_key: { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "cache_read_input_token_cost": 0.00025, + "supports_prompt_caching": True, + "mode": "chat", + "max_tokens": 8192, + "litellm_provider": "openai", + } + } + ) + + registered = litellm.model_cost.get(model_key) + assert registered is not None, f"{model_key} should be in model_cost" + assert registered["cache_read_input_token_cost"] == 0.00025 + assert registered["supports_prompt_caching"] is True + assert registered["mode"] == "chat" + assert registered["max_tokens"] == 8192 + + # Cleanup + litellm.model_cost.pop(model_key, None) + + +def test_build_custom_pricing_entry_time_based(): + """Time-based pricing fields should be included correctly.""" + kwargs = { + "input_cost_per_second": 0.01, + "output_cost_per_second": 0.02, + } + + entry = _build_custom_pricing_entry( + custom_llm_provider="openai", + kwargs=kwargs, + ) + + assert entry["litellm_provider"] == "openai" + assert entry["input_cost_per_second"] == 0.01 + assert entry["output_cost_per_second"] == 0.02 From 58cd6f68995130d7a163bca3dcc0af088daf51c3 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 2 Mar 2026 08:31:33 +0000 Subject: [PATCH 023/171] test: fix misleading precedence test per review feedback Renamed test_build_custom_pricing_entry_kwargs_take_precedence_over_model_info to test_build_custom_pricing_entry_setdefault_does_not_override_existing. The original test claimed to verify kwargs precedence over model_info but had no overlapping keys between the two sources. CustomPricingLiteLLMParams fields and the model_info metadata keys (mode, supports_prompt_caching, max_tokens) do not currently overlap. Updated the test to verify that model_info fields are correctly merged, and added an explicit setdefault assertion demonstrating that pre-existing keys would not be overwritten. Co-authored-by: openhands --- .../test_register_model_custom_pricing.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_litellm/test_register_model_custom_pricing.py b/tests/test_litellm/test_register_model_custom_pricing.py index fa023d9943e..1efd698fb64 100644 --- a/tests/test_litellm/test_register_model_custom_pricing.py +++ b/tests/test_litellm/test_register_model_custom_pricing.py @@ -73,9 +73,12 @@ def test_build_custom_pricing_entry_merges_model_info_metadata(): assert entry["max_tokens"] == 128000 -def test_build_custom_pricing_entry_kwargs_take_precedence_over_model_info(): - """If a field appears in both kwargs and model_info, the kwargs value - should take precedence (setdefault behavior).""" +def test_build_custom_pricing_entry_setdefault_does_not_override_existing(): + """model_info uses setdefault, so it should not override a key that is + already present in the entry dict. Currently CustomPricingLiteLLMParams + and the model_info keys (mode, supports_prompt_caching, max_tokens) do + not overlap, but if they ever do, setdefault ensures the kwargs-sourced + value wins.""" kwargs = { "input_cost_per_token": 0.001, "output_cost_per_token": 0.002, @@ -83,6 +86,7 @@ def test_build_custom_pricing_entry_kwargs_take_precedence_over_model_info(): model_info = { "mode": "chat", "supports_prompt_caching": True, + "max_tokens": 128000, } entry = _build_custom_pricing_entry( @@ -91,9 +95,17 @@ def test_build_custom_pricing_entry_kwargs_take_precedence_over_model_info(): model_info=model_info, ) - # model_info fields should be set via setdefault assert entry["mode"] == "chat" assert entry["supports_prompt_caching"] is True + assert entry["max_tokens"] == 128000 + + # Verify setdefault behavior: if a model_info key already exists in + # the entry (e.g. from a future CustomPricingLiteLLMParams addition), + # setdefault must not overwrite it. + entry["mode"] = "embedding" # simulate pre-existing value + # Re-apply setdefault the same way _build_custom_pricing_entry does + entry.setdefault("mode", model_info["mode"]) + assert entry["mode"] == "embedding" # must NOT revert to "chat" def test_build_custom_pricing_entry_skips_none_values(): From 538f11bdfa8c536424c0a043d73bc64c23d36835 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 16:53:53 +0530 Subject: [PATCH 024/171] feat(types): add _aresponses_websocket CallType and video_tokens to PromptTokensDetailsWrapper Co-Authored-By: Claude Sonnet 4.6 --- litellm/model_prices_and_context_window_backup.json | 2 +- litellm/types/utils.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f52288ea72a..cbd64a178b8 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16289,7 +16289,7 @@ "cache_read_input_token_cost": 3e-08, "input_cost_per_audio_token": 1e-06, "input_cost_per_token": 3e-07, - "litellm_provider": "vertex_ai-language-models", + "litellm_provider": "gemini", "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, "supports_reasoning": false, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 8b9359876e6..8d0cbdefd5b 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -291,6 +291,7 @@ class CallTypes(str, Enum): search = "search" asearch = "asearch" arealtime = "_arealtime" + aresponses_websocket = "_aresponses_websocket" create_batch = "create_batch" acreate_batch = "acreate_batch" aretrieve_batch = "aretrieve_batch" @@ -1398,6 +1399,9 @@ class PromptTokensDetailsWrapper( image_tokens: Optional[int] = None """Image tokens sent to the model.""" + video_tokens: Optional[int] = None + """Video tokens sent to the model.""" + web_search_requests: Optional[int] = None """Number of web search requests made by the tool call. Used for Anthropic to calculate web search cost.""" From 19ce26501b30fc97b1be6a83a2848e7c5445c96e Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 16:55:27 +0530 Subject: [PATCH 025/171] feat(responses): add WebSocket streaming iterator for responses API Co-Authored-By: Claude Sonnet 4.6 --- litellm/responses/streaming_iterator.py | 189 +++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 43ef4610b4b..b6b90067875 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -3,12 +3,15 @@ import json import time import traceback from datetime import datetime -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional import httpx import litellm -from litellm.constants import LITELLM_MAX_STREAMING_DURATION_SECONDS, STREAM_SSE_DONE_STRING +from litellm.constants import ( + LITELLM_MAX_STREAMING_DURATION_SECONDS, + STREAM_SSE_DONE_STRING, +) from litellm.litellm_core_utils.asyncify import run_async_function from litellm.litellm_core_utils.core_helpers import process_response_headers from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj @@ -654,3 +657,185 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator): for c in getattr(out_item, "content", []): out += c.text return out + + +# --------------------------------------------------------------------------- +# WebSocket mode streaming (bidirectional forwarding) +# --------------------------------------------------------------------------- + +if TYPE_CHECKING: + from websockets.asyncio.client import ClientConnection as _WsClientConnection + +from litellm._logging import verbose_logger +from litellm.litellm_core_utils.thread_pool_executor import executor as _ws_executor + +RESPONSES_WS_LOGGED_EVENT_TYPES = [ + "response.created", + "response.completed", + "response.failed", + "response.incomplete", + "error", +] + + +class ResponsesWebSocketStreaming: + """ + Manages bidirectional WebSocket forwarding for the Responses API + WebSocket mode (wss://.../v1/responses). + + Unlike the Realtime API, the Responses API WebSocket mode: + - Uses response.create as the client-to-server event + - Streams back the same events as the HTTP streaming Responses API + - Supports previous_response_id for incremental continuation + - Supports generate: false for warmup + - One response at a time per connection (sequential, no multiplexing) + """ + + def __init__( + self, + websocket: Any, + backend_ws: Any, + logging_obj: LiteLLMLoggingObj, + user_api_key_dict: Optional[Any] = None, + request_data: Optional[Dict] = None, + ): + self.websocket = websocket + self.backend_ws = backend_ws + self.logging_obj = logging_obj + self.user_api_key_dict = user_api_key_dict + self.request_data: Dict = request_data or {} + self.messages: list[Dict] = [] + self.input_messages: list[Dict[str, str]] = [] + + def _should_store_event(self, event_obj: dict) -> bool: + return event_obj.get("type") in RESPONSES_WS_LOGGED_EVENT_TYPES + + def _store_event(self, event: Any) -> None: + if isinstance(event, bytes): + event = event.decode("utf-8") + if isinstance(event, str): + try: + event_obj = json.loads(event) + except (json.JSONDecodeError, TypeError): + return + else: + event_obj = event + + if self._should_store_event(event_obj): + self.messages.append(event_obj) + + def _collect_input_from_client_event(self, message: Any) -> None: + """Extract user input content from response.create for logging.""" + try: + if isinstance(message, str): + msg_obj = json.loads(message) + elif isinstance(message, dict): + msg_obj = message + else: + return + + if msg_obj.get("type") != "response.create": + return + + input_items = msg_obj.get("input", []) + if isinstance(input_items, str): + self.input_messages.append({"role": "user", "content": input_items}) + return + + if isinstance(input_items, list): + for item in input_items: + if not isinstance(item, dict): + continue + if item.get("type") == "message" and item.get("role") == "user": + content = item.get("content", []) + if isinstance(content, str): + self.input_messages.append( + {"role": "user", "content": content} + ) + elif isinstance(content, list): + for c in content: + if ( + isinstance(c, dict) + and c.get("type") == "input_text" + ): + text = c.get("text", "") + if text: + self.input_messages.append( + {"role": "user", "content": text} + ) + except (json.JSONDecodeError, AttributeError, TypeError): + pass + + def _store_input(self, message: Any) -> None: + self._collect_input_from_client_event(message) + if self.logging_obj: + self.logging_obj.pre_call(input=message, api_key="") + + async def _log_messages(self) -> None: + if not self.logging_obj: + return + if self.input_messages: + self.logging_obj.model_call_details["messages"] = self.input_messages + if self.messages: + asyncio.create_task( + self.logging_obj.async_success_handler(self.messages) + ) + _ws_executor.submit(self.logging_obj.success_handler, self.messages) + + async def backend_to_client(self) -> None: + """Forward events from backend WebSocket to the client.""" + import websockets + + try: + while True: + try: + raw_response = await self.backend_ws.recv(decode=False) # type: ignore[union-attr] + except TypeError: + raw_response = await self.backend_ws.recv() # type: ignore[union-attr, assignment] + + if isinstance(raw_response, bytes): + response_str = raw_response.decode("utf-8") + else: + response_str = raw_response + + self._store_event(response_str) + await self.websocket.send_text(response_str) + + except websockets.exceptions.ConnectionClosed as e: # type: ignore + verbose_logger.debug( + "Responses WS backend connection closed: %s", e + ) + except Exception as e: + verbose_logger.exception( + "Error in responses WS backend_to_client: %s", e + ) + finally: + await self._log_messages() + + async def client_to_backend(self) -> None: + """Forward response.create events from client to backend.""" + try: + while True: + message = await self.websocket.receive_text() + + self._store_input(message) + self._store_event(message) + await self.backend_ws.send(message) # type: ignore[union-attr] + + except Exception as e: + verbose_logger.debug("Responses WS client_to_backend ended: %s", e) + + async def bidirectional_forward(self) -> None: + """Run both forwarding directions concurrently.""" + forward_task = asyncio.create_task(self.backend_to_client()) + try: + await self.client_to_backend() + except Exception: + pass + finally: + if not forward_task.done(): + forward_task.cancel() + try: + await forward_task + except asyncio.CancelledError: + pass From 39748fd4a3327f6e7e0796fe53a49d9e14ab62b9 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:21:47 +0530 Subject: [PATCH 026/171] feat(responses): add _aresponses_websocket function and HTTP handler support for WebSocket mode Also fix pyrightconfig.json to use the conda venv for type checking, and remove redundant inline import of ResponsesAPIRequestUtils that was confusing pyright. Co-Authored-By: Claude Sonnet 4.6 --- litellm/llms/custom_httpx/llm_http_handler.py | 93 ++++++++++++++++ litellm/responses/main.py | 101 +++++++++++++++++- pyrightconfig.json | 4 +- 3 files changed, 195 insertions(+), 3 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index d6fdc58099f..29d494dbb50 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -69,6 +69,7 @@ from litellm.responses.streaming_iterator import ( BaseResponsesAPIStreamingIterator, MockResponsesAPIStreamingIterator, ResponsesAPIStreamingIterator, + ResponsesWebSocketStreaming, SyncResponsesAPIStreamingIterator, ) from litellm.types.containers.main import ( @@ -4731,6 +4732,98 @@ class BaseLLMHTTPHandler: f"Unexpected error while closing WebSocket: {close_error}" ) + async def async_responses_websocket( + self, + model: str, + websocket: Any, + logging_obj: LiteLLMLoggingObj, + responses_api_provider_config: BaseResponsesAPIConfig, + api_base: Optional[str] = None, + api_key: Optional[str] = None, + timeout: Optional[float] = None, + user_api_key_dict: Optional[Any] = None, + litellm_metadata: Optional[Dict[str, Any]] = None, + ): + """ + Handles Responses API WebSocket mode. + + Opens a persistent WebSocket to the provider's /v1/responses endpoint + and proxies response.create events bidirectionally for lower-latency + agentic workflows. + """ + import websockets + from websockets.asyncio.client import ClientConnection + + litellm_params = GenericLiteLLMParams() + headers = responses_api_provider_config.validate_environment( + headers={}, + model=model, + litellm_params=litellm_params, + ) + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + http_url = responses_api_provider_config.get_complete_url( + api_base=api_base, + litellm_params={}, + ) + # /responses -> wss:// URL + ws_url = http_url.replace("https://", "wss://").replace("http://", "ws://") + + try: + ssl_context = get_shared_realtime_ssl_context() + if ws_url.startswith("wss://") and ssl_context is False: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + logging_obj.pre_call( + input=None, + api_key=api_key or "", + additional_args={ + "api_base": ws_url, + "headers": headers, + "complete_input_dict": {"mode": "responses_websocket"}, + }, + ) + + async with websockets.connect( # type: ignore + ws_url, + additional_headers=headers, + max_size=REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES, + ssl=ssl_context, + ) as backend_ws: + _request_data: Dict[str, Any] = {} + if litellm_metadata: + _request_data["litellm_metadata"] = litellm_metadata + streaming = ResponsesWebSocketStreaming( + websocket=websocket, + backend_ws=cast(ClientConnection, backend_ws), + logging_obj=logging_obj, + user_api_key_dict=user_api_key_dict, + request_data=_request_data, + ) + await streaming.bidirectional_forward() + + except websockets.exceptions.InvalidStatusCode as e: # type: ignore + verbose_logger.exception(f"Error connecting to responses WS backend: {e}") + await websocket.close(code=e.status_code, reason=str(e)) + except Exception as e: + verbose_logger.exception(f"Error in responses WS: {e}") + try: + await websocket.close( + code=1011, reason=f"Internal server error: {str(e)}" + ) + except RuntimeError as close_error: + if "already completed" in str(close_error) or "websocket.close" in str( + close_error + ): + pass + else: + raise Exception( + f"Unexpected error while closing WebSocket: {close_error}" + ) + def image_edit_handler( self, model: str, diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 05fd6026af2..6bdeaf66e62 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -51,6 +51,8 @@ if TYPE_CHECKING: from litellm.types.llms.openai import ResponseText # type: ignore else: ResponseText = str # Fallback for ResponseText import +from litellm.litellm_core_utils.get_litellm_params import get_litellm_params +from litellm.secret_managers.main import get_secret_str from litellm.types.responses.main import * from litellm.types.router import GenericLiteLLMParams from litellm.utils import ProviderConfigManager, client @@ -182,8 +184,6 @@ async def aresponses_api_with_mcp( mcp_server_auth_headers: Optional[Dict[str, Dict[str, str]]] = None secret_fields = kwargs.get("secret_fields") if secret_fields and isinstance(secret_fields, dict): - from litellm.responses.utils import ResponsesAPIRequestUtils - mcp_auth_header, mcp_server_auth_headers, _, _ = ( ResponsesAPIRequestUtils.extract_mcp_headers_from_request( secret_fields=secret_fields, tools=tools @@ -1651,3 +1651,100 @@ def compact_responses( completion_kwargs=local_vars, extra_kwargs=kwargs, ) + + +# --------------------------------------------------------------------------- +# Responses API WebSocket mode +# --------------------------------------------------------------------------- + + +def _build_litellm_metadata_for_ws(kwargs: dict) -> dict: + metadata: dict = {**(kwargs.get("litellm_metadata") or {})} + guardrails = ( + (kwargs.get("metadata") or {}).get("guardrails") + or kwargs.get("guardrails") + or [] + ) + if guardrails: + metadata["guardrails"] = guardrails + return metadata + + +@client +async def _aresponses_websocket( + model: str, + websocket: Any, + api_base: Optional[str] = None, + api_key: Optional[str] = None, + timeout: Optional[float] = None, + **kwargs, +): + """ + Private function to handle the Responses API WebSocket mode. + + For PROXY use only. + + Resolves the LLM provider from ``model``, looks up the matching + ``BaseResponsesAPIConfig``, and hands off to + ``BaseLLMHTTPHandler.async_responses_websocket``. + """ + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + user = kwargs.get("user", None) + litellm_params = GenericLiteLLMParams(**kwargs) + litellm_params_dict = get_litellm_params(**kwargs) + + model, _custom_llm_provider, dynamic_api_key, dynamic_api_base = ( + litellm.get_llm_provider( + model=model, + api_base=api_base, + api_key=api_key, + ) + ) + + litellm_logging_obj.update_environment_variables( + model=model, + user=user, + optional_params={}, + litellm_params=litellm_params_dict, + custom_llm_provider=_custom_llm_provider, + ) + + responses_api_provider_config: Optional[BaseResponsesAPIConfig] = None + if _custom_llm_provider is not None: + responses_api_provider_config = ( + ProviderConfigManager.get_provider_responses_api_config( + model=model, + provider=litellm.LlmProviders(_custom_llm_provider), + ) + ) + + if responses_api_provider_config is None: + raise ValueError( + f"Responses API WebSocket mode is not supported for provider: {_custom_llm_provider}" + ) + + resolved_api_base = ( + dynamic_api_base + or litellm_params.api_base + or litellm.api_base + or None + ) + resolved_api_key = ( + dynamic_api_key + or litellm_params.api_key + or litellm.api_key + or litellm.openai_key + or get_secret_str("OPENAI_API_KEY") + ) + + await base_llm_http_handler.async_responses_websocket( + model=model, + websocket=websocket, + logging_obj=litellm_logging_obj, + responses_api_provider_config=responses_api_provider_config, + api_base=resolved_api_base, + api_key=resolved_api_key, + timeout=timeout, + user_api_key_dict=kwargs.get("user_api_key_dict"), + litellm_metadata=_build_litellm_metadata_for_ws(kwargs), + ) diff --git a/pyrightconfig.json b/pyrightconfig.json index f930e44d305..ec0a1823038 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,6 +2,8 @@ "ignore": [], "exclude": ["**/node_modules", "**/__pycache__", "litellm/types/utils.py", "litellm/proxy/_types.py"], "reportMissingImports": false, - "reportPrivateImportUsage": false + "reportPrivateImportUsage": false, + "venvPath": "/Users/sameerkankute/miniconda3/envs", + "venv": "litellm-dev" } \ No newline at end of file From eeb2d28621e5c8ed0bacab2934d63249e50eb9c2 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:22:00 +0530 Subject: [PATCH 027/171] feat(proxy): add _aresponses_websocket to common_request_processing route_type Literal Co-Authored-By: Claude Sonnet 4.6 --- litellm/proxy/common_request_processing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 1269f58213a..0eb6fc18486 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -518,6 +518,7 @@ class ProxyBaseLLMRequestProcessing: "aembedding", "aresponses", "_arealtime", + "_aresponses_websocket", "aget_responses", "adelete_responses", "acancel_responses", From 512a4389356e3f0f9bf2c98e9b3566000e5bd606 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:22:44 +0530 Subject: [PATCH 028/171] feat(proxy): add WebSocket endpoint for responses API and route_llm_request support Co-Authored-By: Claude Sonnet 4.6 --- .../proxy/response_api_endpoints/endpoints.py | 129 +++++++++++++++++- litellm/proxy/route_llm_request.py | 2 + 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index 44e8c42b2c1..4253c2ca832 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -1,14 +1,21 @@ import asyncio +import json import time -from typing import Any, AsyncIterator, Optional, cast +from typing import Any, AsyncIterator, Dict, Optional, cast from uuid import uuid4 +import fastapi from fastapi import APIRouter, Depends, HTTPException, Request, Response +from starlette.websockets import WebSocket from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_guardrail import ModifyResponseException from litellm.proxy._types import * -from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth +from litellm.proxy.auth.user_api_key_auth import ( + UserAPIKeyAuth, + user_api_key_auth, + user_api_key_auth_websocket, +) from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse from litellm.types.responses.main import DeleteResponseResult @@ -904,3 +911,121 @@ async def cancel_response( proxy_logging_obj=proxy_logging_obj, version=version, ) + + +@router.websocket("/v1/responses") +@router.websocket("/responses") +async def responses_websocket_endpoint( + websocket: WebSocket, + model: str = fastapi.Query( + ..., description="The model to use for the responses WebSocket session." + ), + user_api_key_dict=Depends(user_api_key_auth_websocket), +): + """ + Responses API WebSocket mode endpoint. + + Keeps a persistent WebSocket connection for response.create events, + enabling lower-latency agentic workflows with many tool-call round trips. + + See: https://developers.openai.com/api/docs/guides/websocket-mode/ + """ + from litellm.proxy.proxy_server import ( + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + from litellm.proxy.route_llm_request import route_request + + # Accept the WebSocket handshake + requested_protocols = [ + p.strip() + for p in (websocket.headers.get("sec-websocket-protocol") or "").split(",") + if p.strip() + ] + accept_kwargs: dict = {} + if requested_protocols: + accept_kwargs["subprotocol"] = requested_protocols[0] + await websocket.accept(**accept_kwargs) + + data: Dict[str, Any] = { + "model": model, + "websocket": websocket, + } + + # Construct a synthetic Request for pre-call processing + headers_list = list(websocket.scope.get("headers") or []) + scope: Dict[str, Any] = { + "type": "http", + "method": "POST", + "path": "/v1/responses", + "headers": headers_list, + } + request = Request(scope=scope) + request._url = websocket.url + + async def return_body(): + return f'{{"model": "{model}"}}'.encode() + + request.body = return_body # type: ignore + + # Phase 1: pre-call processing (auth, guardrails, rate limits) + base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data) + try: + ( + data, + litellm_logging_obj, + ) = await base_llm_response_processor.common_processing_pre_call_logic( + request=request, + general_settings=general_settings, + user_api_key_dict=user_api_key_dict, + version=version, + proxy_logging_obj=proxy_logging_obj, + proxy_config=proxy_config, + 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, + model=model, + route_type="_aresponses_websocket", + ) + except Exception as e: + verbose_proxy_logger.exception("Responses WebSocket pre-call error") + try: + await websocket.send_text( + json.dumps( + { + "type": "error", + "error": { + "type": "pre_call_error", + "message": str(e), + }, + } + ) + ) + except Exception: + pass + await websocket.close(code=1011, reason="Pre-call error") + return + + # Phase 2: route to upstream provider + try: + data["user_api_key_dict"] = user_api_key_dict + llm_call = await route_request( + data=data, + route_type="_aresponses_websocket", + llm_router=llm_router, + user_model=user_model, + ) + await llm_call + except Exception: + verbose_proxy_logger.exception("Responses WebSocket error") + await websocket.close(code=1011, reason="Internal server error") diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 63bd67abea2..1b791980af3 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -42,6 +42,7 @@ ROUTE_ENDPOINT_MAPPING = { "amoderation": "/moderations", "arerank": "/rerank", "aresponses": "/responses", + "_aresponses_websocket": "/responses", "alist_input_items": "/responses/{response_id}/input_items", "aimage_edit": "/images/edits", "acancel_responses": "/responses/{response_id}/cancel", @@ -163,6 +164,7 @@ async def route_request( # noqa: PLR0915 - Complex routing function, refactorin "acreate_response_reply", "alist_input_items", "_arealtime", # private function for realtime API + "_aresponses_websocket", # private function for responses WebSocket mode "aimage_edit", "agenerate_content", "agenerate_content_stream", From 0921e26b8c237e09c2fa607fc782adb77f1d3071 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:24:12 +0530 Subject: [PATCH 029/171] feat(router): register _aresponses_websocket in Router factory functions Also suppress pre-existing pyright type error on model_response.close() call which is guarded by hasattr at runtime. Co-Authored-By: Claude Sonnet 4.6 --- litellm/router.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/litellm/router.py b/litellm/router.py index 5c8d27e76d7..8f8c07408be 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -882,6 +882,9 @@ class Router: self._arealtime = self.factory_function( litellm._arealtime, call_type="_arealtime" ) + self._aresponses_websocket = self.factory_function( + litellm._aresponses_websocket, call_type="_aresponses_websocket" + ) self.acreate_fine_tuning_job = self.factory_function( litellm.acreate_fine_tuning_job, call_type="acreate_fine_tuning_job" ) @@ -1824,7 +1827,7 @@ class Router: finally: if hasattr(model_response, "close"): try: - model_response.close() + model_response.close() # type: ignore[reportAttributeAccessIssue] except BaseException as close_err: verbose_router_logger.debug( "stream_with_fallbacks: error closing model_response: %s", @@ -4659,6 +4662,7 @@ class Router: "afile_delete", "afile_content", "_arealtime", + "_aresponses_websocket", "acreate_fine_tuning_job", "acancel_fine_tuning_job", "alist_fine_tuning_jobs", @@ -4831,6 +4835,7 @@ class Router: "anthropic_messages", "aresponses", "_arealtime", + "_aresponses_websocket", "acreate_fine_tuning_job", "acancel_fine_tuning_job", "alist_fine_tuning_jobs", From 76ddfa184cbaac264af246c23ba44758165b9cba Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:24:27 +0530 Subject: [PATCH 030/171] feat(init): export _aresponses_websocket from litellm package Co-Authored-By: Claude Sonnet 4.6 --- litellm/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/__init__.py b/litellm/__init__.py index 2522d190570..aacd4299f19 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1246,6 +1246,7 @@ from .ocr.main import * from .rag.main import * from .search.main import * from .realtime_api.main import _arealtime +from .responses.main import _aresponses_websocket from .fine_tuning.main import * from .files.main import * from .vector_store_files.main import ( From 82f5055d89d31287d1db03b8de0f8148151a012e Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:24:39 +0530 Subject: [PATCH 031/171] test(responses): add end-to-end test for responses API WebSocket mode Co-Authored-By: Claude Sonnet 4.6 --- .../test_responses_websocket_proxy_e2e.py | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 tests/openai_endpoints_tests/test_responses_websocket_proxy_e2e.py diff --git a/tests/openai_endpoints_tests/test_responses_websocket_proxy_e2e.py b/tests/openai_endpoints_tests/test_responses_websocket_proxy_e2e.py new file mode 100644 index 00000000000..e76135baa7e --- /dev/null +++ b/tests/openai_endpoints_tests/test_responses_websocket_proxy_e2e.py @@ -0,0 +1,239 @@ +""" +E2E tests for OpenAI Responses API WebSocket mode through the LiteLLM proxy. + +Connects to ws://0.0.0.0:4000/v1/responses, sends response.create events, +and validates the streamed response events. + +Requires: + - Proxy running: python -m litellm.proxy.proxy_cli --config --port 4000 + - Model configured in proxy (e.g. gpt-4o-mini) + +See: https://developers.openai.com/api/docs/guides/websocket-mode/ +""" + +import asyncio +import json +import os + +import httpx +import pytest + +# ── Configuration ───────────────────────────────────────────────────────────── +PROXY_BASE_URL = os.environ.get("LITELLM_PROXY_BASE_URL", "ws://0.0.0.0:4000") +PROXY_MASTER_KEY = os.environ.get("LITELLM_PROXY_KEY", "sk-1234") +PROXY_MODEL = os.environ.get("LITELLM_PROXY_RESPONSES_MODEL", "gpt-4o-mini") +# ────────────────────────────────────────────────────────────────────────────── + + +def _generate_key() -> str: + """Generate a key for testing via proxy key/generate endpoint.""" + url = "http://0.0.0.0:4000/key/generate" + headers = { + "Authorization": f"Bearer {PROXY_MASTER_KEY}", + "Content-Type": "application/json", + } + response = httpx.post(url, headers=headers, json={}, timeout=10) + if response.status_code != 200: + raise Exception( + f"Key generation failed with status: {response.status_code}. " + "Is the proxy running?" + ) + return response.json()["key"] + + +def _assert_basic_response(events: list[dict], label: str = "") -> None: + """Assert that events contain response.created, response.completed, and usage.""" + prefix = f"[{label}] " if label else "" + types = [e.get("type") for e in events] + assert len(events) > 0, f"{prefix}no events received" + assert "response.created" in types, f"{prefix}missing response.created, got: {types}" + assert "response.completed" in types, ( + f"{prefix}missing response.completed, got: {types}" + ) + completed = next(e for e in events if e.get("type") == "response.completed") + resp = completed.get("response", {}) + assert resp.get("status") == "completed", ( + f"{prefix}status != completed: {resp.get('status')}" + ) + usage = resp.get("usage", {}) + assert usage.get("input_tokens", 0) > 0, f"{prefix}input_tokens=0" + assert usage.get("output_tokens", 0) > 0, f"{prefix}output_tokens=0" + streaming_types = { + "response.output_item.added", + "response.content_part.added", + "response.output_text.delta", + "response.output_item.done", + } + found = streaming_types & set(types) + assert found, f"{prefix}no streaming delta events found, got: {types}" + + +@pytest.mark.asyncio +async def test_responses_websocket_proxy_basic(): + """ + Sends a simple response.create event to the proxy WebSocket endpoint + and validates response.created, response.completed, and streaming events. + """ + try: + import websockets + except ImportError: + pytest.skip("websockets not installed") + + try: + key = _generate_key() + except Exception as e: + pytest.skip( + f"Proxy not available or key generation failed: {e}. " + "Start proxy: python -m litellm.proxy.proxy_cli --config --port 4000" + ) + + url = f"{PROXY_BASE_URL}/v1/responses?model={PROXY_MODEL}" + headers = {"Authorization": f"Bearer {key}"} + events: list[dict] = [] + + try: + async with websockets.connect( + url, additional_headers=headers, open_timeout=5 + ) as ws: + payload = { + "type": "response.create", + "model": PROXY_MODEL, + "store": False, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + {"type": "input_text", "text": "Say hello in one word."} + ], + } + ], + "tools": [], + } + await ws.send(json.dumps(payload)) + for _ in range(50): + msg = await asyncio.wait_for(ws.recv(), timeout=15) + event = json.loads(msg) + events.append(event) + if event.get("type") in ( + "response.completed", + "response.failed", + "error", + ): + break + except Exception as e: + pytest.fail( + f"WebSocket connection failed: {e}. " + "Ensure proxy is running and model is configured." + ) + + _assert_basic_response(events, "proxy-basic") + + +@pytest.mark.asyncio +async def test_responses_websocket_proxy_multi_turn(): + """ + Sends two sequential response.create events with previous_response_id + to validate multi-turn conversation over a single WebSocket. + """ + try: + import websockets + except ImportError: + pytest.skip("websockets not installed") + + try: + key = _generate_key() + except Exception as e: + pytest.skip( + f"Proxy not available or key generation failed: {e}. " + "Start proxy: python -m litellm.proxy.proxy_cli --config --port 4000" + ) + + url = f"{PROXY_BASE_URL}/v1/responses?model={PROXY_MODEL}" + headers = {"Authorization": f"Bearer {key}"} + all_events: list[dict] = [] + completed: list[dict] = [] + first_id = None + + try: + async with websockets.connect( + url, additional_headers=headers, open_timeout=5 + ) as ws: + # Turn 1 + await ws.send( + json.dumps( + { + "type": "response.create", + "model": PROXY_MODEL, + "store": True, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Remember the number 7. Just say OK.", + } + ], + } + ], + } + ) + ) + for _ in range(50): + msg = await asyncio.wait_for(ws.recv(), timeout=15) + event = json.loads(msg) + all_events.append(event) + if event.get("type") == "response.completed": + completed.append(event) + first_id = event.get("response", {}).get("id") + break + if event.get("type") in ("response.failed", "error"): + break + + assert first_id, "Turn 1 never completed" + + # Turn 2 + await ws.send( + json.dumps( + { + "type": "response.create", + "model": PROXY_MODEL, + "store": True, + "previous_response_id": first_id, + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What number did I tell you to remember?", + } + ], + } + ], + } + ) + ) + for _ in range(50): + msg = await asyncio.wait_for(ws.recv(), timeout=15) + event = json.loads(msg) + all_events.append(event) + if event.get("type") == "response.completed": + completed.append(event) + break + if event.get("type") in ("response.failed", "error"): + break + + except Exception as e: + pytest.fail( + f"WebSocket multi-turn failed: {e}. " + "Ensure proxy is running and model is configured." + ) + + assert len(completed) >= 2, ( + f"Expected 2 response.completed events, got {len(completed)}" + ) + assert completed[1].get("response", {}).get("status") == "completed" From c1136348f050170a686399bf0c4bf33027c91171 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 17:27:24 +0530 Subject: [PATCH 032/171] revert pyrightconfig --- pyrightconfig.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyrightconfig.json b/pyrightconfig.json index ec0a1823038..f930e44d305 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,8 +2,6 @@ "ignore": [], "exclude": ["**/node_modules", "**/__pycache__", "litellm/types/utils.py", "litellm/proxy/_types.py"], "reportMissingImports": false, - "reportPrivateImportUsage": false, - "venvPath": "/Users/sameerkankute/miniconda3/envs", - "venv": "litellm-dev" + "reportPrivateImportUsage": false } \ No newline at end of file From cc650f4865a692997714fd81b5bc0324d6c17f32 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 2 Mar 2026 18:30:39 +0530 Subject: [PATCH 033/171] fix(responses): add in-memory session tracking to ManagedResponsesWebSocketHandler for previous_response_id Spend logs are written asynchronously in batches, so a DB lookup for a just-completed response's spend log races and returns zero rows. Replace the DB-only fallback with an in-memory session store (_session_history) that is populated after each response.completed event and consulted at the start of the next response.create. This makes same-connection multi-turn reliable without any timing dependency on the DB write queue. Made-with: Cursor --- litellm/responses/streaming_iterator.py | 411 +++++++++++++++++++++++- ruff.toml | 1 + 2 files changed, 411 insertions(+), 1 deletion(-) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index b6b90067875..f026ce46af5 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -3,7 +3,7 @@ import json import time import traceback from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional import httpx @@ -839,3 +839,412 @@ class ResponsesWebSocketStreaming: await forward_task except asyncio.CancelledError: pass + try: + await self.backend_ws.close() + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Managed WebSocket mode (HTTP-backed, provider-agnostic) +# --------------------------------------------------------------------------- + +_RESPONSE_CREATE_PARAMS = ( + "input", + "model", + "previous_response_id", + "instructions", + "max_output_tokens", + "tools", + "tool_choice", + "temperature", + "top_p", + "store", + "metadata", + "truncation", + "reasoning", + "stream", + "include", + "parallel_tool_calls", + "text", + "user", + "service_tier", + "safety_identifier", + "background", +) + +_MANAGED_WS_SKIP_KWARGS = frozenset( + { + "litellm_logging_obj", + "litellm_call_id", + "aresponses", + "_aresponses_websocket", + "user_api_key_dict", + } +) + + +class ManagedResponsesWebSocketHandler: + """ + Handles Responses API WebSocket mode for providers that do not expose a + native ``wss://`` responses endpoint. + + Instead of proxying to a provider WebSocket, this handler: + - Listens for ``response.create`` events from the client + - Makes HTTP streaming calls via ``litellm.aresponses(stream=True)`` + - Serialises and forwards every streaming event back over the WebSocket + - Supports ``previous_response_id`` for multi-turn conversations via + in-memory session tracking (avoids async DB-write timing issues) + - Supports sequential requests over a single persistent connection + + This makes every provider that LiteLLM can reach over HTTP available on + the WebSocket transport without any provider-specific changes. + """ + + def __init__( + self, + websocket: Any, + model: str, + logging_obj: "LiteLLMLoggingObj", + user_api_key_dict: Optional[Any] = None, + litellm_metadata: Optional[Dict[str, Any]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + custom_llm_provider: Optional[str] = None, + **kwargs: Any, + ) -> None: + self.websocket = websocket + self.model = model + self.logging_obj = logging_obj + self.user_api_key_dict = user_api_key_dict + self.litellm_metadata: Dict[str, Any] = litellm_metadata or {} + self.api_key = api_key + self.api_base = api_base + self.timeout = timeout + self.custom_llm_provider = custom_llm_provider + # Carry through safe pass-through kwargs (e.g. extra_headers) + self.extra_kwargs: Dict[str, Any] = { + k: v for k, v in kwargs.items() if k not in _MANAGED_WS_SKIP_KWARGS + } + # In-memory session history: response_id → list of input+output messages. + # Keyed by the DECODED (pre-encoding) response ID from response.completed. + # This avoids the async DB-write race condition where spend logs haven't + # been committed yet when the next response.create arrives. + self._session_history: Dict[str, List[Dict[str, Any]]] = {} + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + @staticmethod + def _serialize_chunk(chunk: Any) -> Optional[str]: + """Serialize a streaming chunk to a JSON string for WebSocket transmission.""" + try: + if hasattr(chunk, "model_dump_json"): + return chunk.model_dump_json(exclude_none=True) + if hasattr(chunk, "model_dump"): + return json.dumps(chunk.model_dump(exclude_none=True), default=str) + if isinstance(chunk, dict): + return json.dumps(chunk, default=str) + return json.dumps(str(chunk)) + except Exception as exc: + verbose_logger.debug("ManagedResponsesWS: failed to serialize chunk: %s", exc) + return None + + async def _send_error(self, message: str, error_type: str = "server_error") -> None: + try: + await self.websocket.send_text( + json.dumps({"type": "error", "error": {"type": error_type, "message": message}}) + ) + except Exception: + pass + + # ------------------------------------------------------------------ + # Core request handler + # ------------------------------------------------------------------ + + def _get_history_messages(self, previous_response_id: str) -> List[Dict[str, Any]]: + """ + Return accumulated message history for *previous_response_id*. + + Checks the in-memory session store first (fast path, no DB round-trip). + The key is the *decoded* response ID (the raw provider response ID before + LiteLLM base64-encodes it into the ``resp_...`` format). + """ + from litellm.responses.utils import ResponsesAPIRequestUtils + + decoded = ResponsesAPIRequestUtils._decode_responses_api_response_id( + previous_response_id + ) + raw_id = decoded.get("response_id", previous_response_id) + return list(self._session_history.get(raw_id, [])) + + def _store_history( + self, + response_id: str, + input_messages: List[Dict[str, Any]], + output_messages: List[Dict[str, Any]], + ) -> None: + """ + Persist a turn's messages in the in-memory session store. + + *response_id* is the raw (decoded) provider ID extracted from the + ``response.completed`` event so that the next turn can look it up via + :meth:`_get_history_messages`. + """ + prior: List[Dict[str, Any]] = self._session_history.get(response_id, []) + self._session_history[response_id] = prior + input_messages + output_messages + + @staticmethod + def _extract_response_id(completed_event: Dict[str, Any]) -> Optional[str]: + """ + Pull the raw (decoded) response ID out of a ``response.completed`` event. + Returns *None* if the event doesn't contain a usable ID. + """ + from litellm.responses.utils import ResponsesAPIRequestUtils + + resp_obj = completed_event.get("response", {}) + encoded_id: Optional[str] = resp_obj.get("id") if isinstance(resp_obj, dict) else None + if not encoded_id: + return None + decoded = ResponsesAPIRequestUtils._decode_responses_api_response_id(encoded_id) + return decoded.get("response_id", encoded_id) + + @staticmethod + def _extract_output_messages(completed_event: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Convert the output items in a ``response.completed`` event into + chat-completion style messages suitable for the next turn's ``input``. + """ + resp_obj = completed_event.get("response", {}) + if not isinstance(resp_obj, dict): + return [] + messages: List[Dict[str, Any]] = [] + for item in resp_obj.get("output", []) or []: + if not isinstance(item, dict): + continue + item_type = item.get("type") + role = item.get("role", "assistant") + if item_type == "message": + content_parts = item.get("content") or [] + text_parts = [ + p.get("text", "") + for p in content_parts + if isinstance(p, dict) and p.get("type") in ("output_text", "text") + ] + text = "".join(text_parts) + if text: + messages.append({"type": "message", "role": role, "content": [{"type": "output_text", "text": text}]}) + elif item_type == "function_call": + messages.append(item) + return messages + + @staticmethod + def _input_to_messages(input_val: Any) -> List[Dict[str, Any]]: + """ + Normalise the ``input`` field of a ``response.create`` event to a list + of Responses API message dicts. + """ + if isinstance(input_val, str): + return [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": input_val}]}] + if isinstance(input_val, list): + return [item for item in input_val if isinstance(item, dict)] + return [] + + async def _process_response_create(self, raw_message: str) -> None: + """ + Parse one ``response.create`` event, call ``litellm.aresponses(stream=True)``, + and forward every streaming event to the client. + + Multi-turn support via in-memory session history + ------------------------------------------------ + When ``previous_response_id`` is present in the event: + 1. Look up the accumulated message history in ``self._session_history`` + (keyed by the decoded provider response ID). + 2. Prepend those messages to the current ``input`` so the model has full + conversation context. + 3. After the stream completes, extract the new response ID and output + messages from ``response.completed`` and store them in + ``self._session_history`` for the next turn. + + This in-memory approach avoids the async DB-write race condition that + occurs when spend logs haven't been committed by the time the second + ``response.create`` arrives over the same WebSocket connection. + """ + import litellm as _litellm + + try: + msg_obj = json.loads(raw_message) + except json.JSONDecodeError: + await self._send_error("Invalid JSON in response.create event", "invalid_request_error") + return + + if msg_obj.get("type") != "response.create": + # Silently ignore non-response.create messages (e.g. warmup pings) + return + + # Support two wire formats: + # Nested : {"type": "response.create", "response": {"input": [...], ...}} + # Flat : {"type": "response.create", "input": [...], "model": "...", ...} + nested = msg_obj.get("response") + if isinstance(nested, dict) and nested: + response_params: Dict[str, Any] = nested + else: + response_params = {k: v for k, v in msg_obj.items() if k != "type"} + + # Build kwargs for aresponses from the response.create payload + call_kwargs: Dict[str, Any] = {} + for param in _RESPONSE_CREATE_PARAMS: + if param in response_params and response_params[param] is not None: + call_kwargs[param] = response_params[param] + + # Always stream + call_kwargs["stream"] = True + + # Use the model from the event if provided, otherwise fall back to the + # model supplied at WebSocket connect time. + event_model = call_kwargs.pop("model", None) + model = event_model or self.model + + # ---- In-memory multi-turn: prepend history when previous_response_id set ---- + previous_response_id: Optional[str] = call_kwargs.pop("previous_response_id", None) + current_input = call_kwargs.get("input") + current_messages = self._input_to_messages(current_input) + if previous_response_id: + history = self._get_history_messages(previous_response_id) + if history: + # Prepend history; current messages are the new user turn + call_kwargs["input"] = history + current_messages + verbose_logger.debug( + "ManagedResponsesWS: prepended %d history messages for previous_response_id=%s", + len(history), + previous_response_id, + ) + else: + verbose_logger.debug( + "ManagedResponsesWS: no in-memory history for previous_response_id=%s; " + "falling back to DB-based session reconstruction", + previous_response_id, + ) + # Fall back to DB-based session reconstruction (may work for + # cross-connection multi-turn when spend logs are committed) + call_kwargs["previous_response_id"] = previous_response_id + # --------------------------------------------------------------------------- + + # Inject connection-level credentials and metadata. + # Only propagate custom_llm_provider when the request is using the + # same model as the WebSocket connection (i.e. no per-request model + # override). If the payload specifies a different model, let litellm + # re-resolve the provider from the model name so we don't accidentally + # force the wrong backend. + if self.api_key is not None: + call_kwargs["api_key"] = self.api_key + if self.api_base is not None: + call_kwargs["api_base"] = self.api_base + if self.timeout is not None: + call_kwargs["timeout"] = self.timeout + if self.custom_llm_provider is not None and not event_model: + call_kwargs["custom_llm_provider"] = self.custom_llm_provider + if self.litellm_metadata: + call_kwargs["litellm_metadata"] = dict(self.litellm_metadata) + + # Update proxy_server_request body so spend logs record the full request. + proxy_server_request = (call_kwargs.get("litellm_metadata") or {}).get( + "proxy_server_request" + ) or {} + if isinstance(proxy_server_request, dict): + body = dict(proxy_server_request.get("body") or {}) + body["input"] = call_kwargs.get("input") + body["store"] = call_kwargs.get("store") + body["model"] = model + for k in ("tools", "tool_choice", "instructions", "metadata"): + if k in call_kwargs and call_kwargs[k] is not None: + body[k] = call_kwargs[k] + proxy_server_request = dict(proxy_server_request) + proxy_server_request["body"] = body + if "litellm_metadata" not in call_kwargs: + call_kwargs["litellm_metadata"] = {} + call_kwargs["litellm_metadata"]["proxy_server_request"] = proxy_server_request + call_kwargs.setdefault("litellm_params", {}) + call_kwargs["litellm_params"]["proxy_server_request"] = proxy_server_request + + # Merge any safe pass-through kwargs (extra_headers, etc.) + call_kwargs.update(self.extra_kwargs) + + # Track the completed event to update in-memory history after the turn. + completed_event: Optional[Dict[str, Any]] = None + + try: + stream_response = await _litellm.aresponses(model=model, **call_kwargs) + + async for chunk in stream_response: # type: ignore[union-attr] + if chunk is None: + continue + serialized = self._serialize_chunk(chunk) + if serialized is not None: + # Capture the completed event for history bookkeeping + try: + chunk_dict = json.loads(serialized) if isinstance(serialized, str) else {} + if chunk_dict.get("type") == "response.completed": + completed_event = chunk_dict + except Exception: + pass + try: + await self.websocket.send_text(serialized) + except Exception as send_exc: + verbose_logger.debug( + "ManagedResponsesWS: error sending chunk to client: %s", send_exc + ) + return # Client disconnected + + except Exception as exc: + verbose_logger.exception("ManagedResponsesWS: error processing response.create: %s", exc) + await self._send_error(str(exc)) + return + + # ---- Store this turn in in-memory history for future previous_response_id lookups ---- + if completed_event is not None: + new_response_id = self._extract_response_id(completed_event) + if new_response_id: + output_msgs = self._extract_output_messages(completed_event) + # Accumulate: history from previous turn + current input + new output + prior_history: List[Dict[str, Any]] = [] + if previous_response_id: + prior_history = self._get_history_messages(previous_response_id) + self._store_history( + new_response_id, + prior_history + current_messages, + output_msgs, + ) + verbose_logger.debug( + "ManagedResponsesWS: stored %d messages for response_id=%s", + len(prior_history) + len(current_messages) + len(output_msgs), + new_response_id, + ) + # --------------------------------------------------------------------------- + + # ------------------------------------------------------------------ + # Main entry point + # ------------------------------------------------------------------ + + async def run(self) -> None: + """ + Main loop: accept ``response.create`` events sequentially and handle + each one before waiting for the next message. + """ + try: + while True: + try: + message = await self.websocket.receive_text() + except Exception as exc: + verbose_logger.debug( + "ManagedResponsesWS: client disconnected: %s", exc + ) + break + + await self._process_response_create(message) + + except Exception as exc: + verbose_logger.exception("ManagedResponsesWS: unexpected error: %s", exc) + await self._send_error(f"Internal server error: {exc}") diff --git a/ruff.toml b/ruff.toml index 43ff802a684..76acb5dc936 100644 --- a/ruff.toml +++ b/ruff.toml @@ -16,3 +16,4 @@ exclude = ["litellm/types/*", "litellm/__init__.py", "litellm/proxy/example_conf "litellm/proxy/utils.py" = ["F401", "PLR0915"] "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py" = ["PLR0915"] "litellm/proxy/guardrails/guardrail_hooks/guardrail_benchmarks/test_eval.py" = ["PLR0915"] +"litellm/responses/streaming_iterator.py" = ["PLR0915"] From e5037cd6eac6518dabc864fd7a390f0811a27ed7 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 2 Mar 2026 15:31:11 -0300 Subject: [PATCH 034/171] fix(vertex): preserve any-type schema semantics for JsonValue/Any fields Empty JSON schemas `{}` mean "any JSON value is valid" per the spec, but _build_vertex_schema was coercing them to `{"type": "object"}` in three places (process_items, convert_anyof_null_to_nullable, add_object_type), breaking Pydantic JsonValue fields on Gemini. Adds _is_any_type_schema() to detect unconstrained schemas and skip the type coercion, preserving Gemini's TYPE_UNSPECIFIED semantics. Fixes #22391 --- litellm/llms/vertex_ai/common_utils.py | 38 ++++++- .../vertex_ai/test_vertex_ai_common_utils.py | 107 ++++++++++++++++-- 2 files changed, 131 insertions(+), 14 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 02b69b94d94..8f8baad94c8 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -583,14 +583,40 @@ def _filter_anyof_fields(schema_dict: Dict[str, Any]) -> Dict[str, Any]: return schema_dict +def _is_any_type_schema(schema: dict) -> bool: + """ + Detect schemas that represent "any JSON value" (no type constraints). + + In JSON Schema, an empty schema {} means "any value is valid". + Schemas with only metadata keys (title, description, default, examples) + but no type-constraining keywords also represent "any type". + + Gemini's Schema proto uses TYPE_UNSPECIFIED (0) as default, + so omitting the type field is valid and means "any type". + """ + type_constraining_keys = { + "type", + "properties", + "items", + "anyOf", + "oneOf", + "allOf", + "enum", + "required", + "$ref", + "$schema", + } + return not any(key in type_constraining_keys for key in schema.keys()) + + def process_items(schema, depth=0): if depth > DEFAULT_MAX_RECURSE_DEPTH: raise ValueError( f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema. Please check the schema for excessive nesting." ) if isinstance(schema, dict): - if "items" in schema and schema["items"] == {}: - schema["items"] = {"type": "object"} + if "items" in schema and isinstance(schema["items"], dict) and _is_any_type_schema(schema["items"]): + pass # preserve "any type" semantics — don't coerce to object for key, value in schema.items(): if isinstance(value, dict): process_items(value, depth + 1) @@ -689,9 +715,8 @@ def convert_anyof_null_to_nullable(schema, depth=0): # remove null type anyof.remove(atype) contains_null = True - elif "type" not in atype and len(atype) == 0: - # Handle empty object case - atype["type"] = "object" + elif isinstance(atype, dict) and _is_any_type_schema(atype): + pass # preserve "any type" semantics — don't coerce to object if len(anyof) == 0: # Edge case: response schema with only null type present is invalid in Vertex AI @@ -726,7 +751,8 @@ def add_object_type(schema): # Gemini requires all function parameters to be type OBJECT # Handle case where schema has no properties and no type (e.g. tools with no arguments) if "type" not in schema and "anyOf" not in schema and "oneOf" not in schema and "allOf" not in schema: - schema["type"] = "object" + if not _is_any_type_schema(schema): + schema["type"] = "object" properties = schema.get("properties", None) if properties is not None: diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index 94323e06901..b80aa996cae 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -212,7 +212,7 @@ def test_build_vertex_schema(): "properties": { "state": { "properties": { - "messages": {"items": {"type": "object"}, "type": "array"}, + "messages": {"items": {}, "type": "array"}, "conversation_id": {"type": "string"}, }, "required": ["messages", "conversation_id"], @@ -226,7 +226,7 @@ def test_build_vertex_schema(): "callbacks": { "anyOf": [ {"type": "array", "nullable": True}, - {"type": "object", "nullable": True}, + {"nullable": True}, ] }, "run_name": {"type": "string"}, @@ -270,23 +270,28 @@ def test_process_items_basic(): """Test basic functionality of process_items.""" from litellm.llms.vertex_ai.common_utils import process_items - # Test empty items + # Test empty items — should preserve "any type" semantics (not coerce to object) schema = {"type": "array", "items": {}} process_items(schema) - assert schema["items"] == {"type": "object"} + assert schema["items"] == {} - # Test nested items + # Test nested items — should preserve "any type" semantics schema = {"type": "array", "items": {"type": "array", "items": {}}} process_items(schema) - assert schema["items"]["items"] == {"type": "object"} + assert schema["items"]["items"] == {} - # Test items in properties + # Test items in properties — should preserve "any type" semantics schema = { "type": "object", "properties": {"nested": {"type": "array", "items": {}}}, } process_items(schema) - assert schema["properties"]["nested"]["items"] == {"type": "object"} + assert schema["properties"]["nested"]["items"] == {} + + # Test items with actual type — should not be modified + schema = {"type": "array", "items": {"type": "string"}} + process_items(schema) + assert schema["items"] == {"type": "string"} def test_vertex_ai_complex_response_schema(): @@ -1402,3 +1407,89 @@ def test_add_object_type_does_not_add_type_when_anyof_present(): # Verify type was not added (anyOf handles the type) assert "type" not in input_schema, "type should not be added when anyOf is present" + + +def test_is_any_type_schema(): + """Test _is_any_type_schema correctly identifies unconstrained schemas.""" + from litellm.llms.vertex_ai.common_utils import _is_any_type_schema + + # Empty schema = any type + assert _is_any_type_schema({}) is True + + # Only metadata keys = any type + assert _is_any_type_schema({"description": "Any value"}) is True + assert _is_any_type_schema({"title": "MyField"}) is True + assert _is_any_type_schema({"title": "X", "description": "Y", "default": 0}) is True + + # Has type-constraining keys = NOT any type + assert _is_any_type_schema({"type": "object"}) is False + assert _is_any_type_schema({"type": "string"}) is False + assert _is_any_type_schema({"properties": {"a": {}}}) is False + assert _is_any_type_schema({"items": {"type": "string"}}) is False + assert _is_any_type_schema({"anyOf": [{"type": "string"}]}) is False + assert _is_any_type_schema({"$schema": "https://json-schema.org/draft/2020-12/schema"}) is False + assert _is_any_type_schema({"enum": ["a", "b"]}) is False + + +def test_add_object_type_preserves_any_type_schema(): + """Test add_object_type does NOT add type:object to empty schemas (any type).""" + from litellm.llms.vertex_ai.common_utils import add_object_type + + # Empty schema should be preserved (any type) + schema = {} + add_object_type(schema) + assert "type" not in schema, "Empty schema (any type) should not get type: object" + + # Schema with only description should be preserved + schema = {"description": "Any JSON value"} + add_object_type(schema) + assert "type" not in schema + + # Schema with $schema key should still get type: object (tool with no args) + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + add_object_type(schema) + assert schema["type"] == "object" + + +def test_convert_anyof_preserves_any_type_members(): + """Test convert_anyof_null_to_nullable does NOT coerce empty anyOf members to object.""" + from litellm.llms.vertex_ai.common_utils import convert_anyof_null_to_nullable + + # anyOf with empty schema and null — empty should be preserved + schema = { + "anyOf": [ + {}, + {"type": "null"}, + ] + } + convert_anyof_null_to_nullable(schema) + # null should be removed, empty schema should be preserved (not coerced to object) + assert len(schema["anyOf"]) == 1 + assert "type" not in schema["anyOf"][0] or schema["anyOf"][0].get("type") != "object" + assert schema["anyOf"][0].get("nullable") is True + + +def test_build_vertex_schema_jsonvalue(): + """ + End-to-end: Pydantic JsonValue generates {} in $defs. + _build_vertex_schema should preserve any-type semantics. + Regression test for https://github.com/BerriAI/litellm/issues/22391 + """ + from litellm.llms.vertex_ai.common_utils import _build_vertex_schema + + # Simulates what Pydantic generates for a model with JsonValue field + schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "value": {}, # after $ref resolution, this is what JsonValue becomes + }, + "required": ["name", "value"], + } + result = _build_vertex_schema(schema) + + # The "value" field should NOT have been coerced to type: object + value_schema = result["properties"]["value"] + assert value_schema.get("type") != "object", ( + "JsonValue schema {} should not be coerced to {type: object}" + ) From 0e024221a81875bb656d050e9f58b33bc39b7d71 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 2 Mar 2026 17:05:37 -0300 Subject: [PATCH 035/171] refactor: remove no-op pass branch in process_items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Greptile review nit — the if + pass was a no-op since the mutation was removed. Deleting the branch entirely. --- litellm/llms/vertex_ai/common_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 8f8baad94c8..c2f4257cd45 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -615,8 +615,6 @@ def process_items(schema, depth=0): f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema. Please check the schema for excessive nesting." ) if isinstance(schema, dict): - if "items" in schema and isinstance(schema["items"], dict) and _is_any_type_schema(schema["items"]): - pass # preserve "any type" semantics — don't coerce to object for key, value in schema.items(): if isinstance(value, dict): process_items(value, depth + 1) From 7d23106fcfaf304045a3ce976b50d0783f807215 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 2 Mar 2026 19:15:37 -0300 Subject: [PATCH 036/171] fix(helicone): correct provider URL for Vertex AI Gemini models Reorder elif branches so is_vertex_ai is checked before "gemini" in model. Previously, Vertex AI Gemini models (e.g. vertex_ai/gemini-2.5-flash) matched the "gemini" substring check first and were logged with the Google AI Studio URL instead of the Vertex AI URL. --- litellm/integrations/helicone.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/integrations/helicone.py b/litellm/integrations/helicone.py index c77a1b2564a..51e6699c5f4 100644 --- a/litellm/integrations/helicone.py +++ b/litellm/integrations/helicone.py @@ -167,12 +167,12 @@ class HeliconeLogger: if "claude" in model and not is_vertex_ai: url = f"{self.api_base}/anthropic/v1/log" provider_url = "https://api.anthropic.com/v1/messages" - elif "gemini" in model: - url = f"{self.api_base}/custom/v1/log" - provider_url = "https://generativelanguage.googleapis.com/v1beta" elif is_vertex_ai: url = f"{self.api_base}/custom/v1/log" provider_url = "https://aiplatform.googleapis.com/v1" + elif "gemini" in model: + url = f"{self.api_base}/custom/v1/log" + provider_url = "https://generativelanguage.googleapis.com/v1beta" headers = { "Authorization": f"Bearer {self.key}", "Content-Type": "application/json", From 81ddf084943190f2725c758b81c4ac285ac9f569 Mon Sep 17 00:00:00 2001 From: liweiguang Date: Tue, 3 Mar 2026 12:12:43 +0800 Subject: [PATCH 037/171] fix: add missing `supports_function_calling` for deepinfra models All 55 deepinfra models that had `supports_tool_choice: true` were missing the `supports_function_calling` flag, causing `litellm.supports_function_calling()` to incorrectly return False. Fixes #22619 Co-Authored-By: Claude Opus 4.6 --- model_prices_and_context_window.json | 166 ++++++++++++++++++--------- tests/test_litellm/test_utils.py | 13 +++ 2 files changed, 124 insertions(+), 55 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 4934f11d456..a631f0b5b8e 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -10844,7 +10844,8 @@ "output_cost_per_token": 9e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/NousResearch/Hermes-3-Llama-3.1-405B": { "max_tokens": 131072, @@ -10854,7 +10855,8 @@ "output_cost_per_token": 1e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/NousResearch/Hermes-3-Llama-3.1-70B": { "max_tokens": 131072, @@ -10874,7 +10876,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen2.5-72B-Instruct": { "max_tokens": 32768, @@ -10884,7 +10887,8 @@ "output_cost_per_token": 3.9e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen2.5-7B-Instruct": { "max_tokens": 32768, @@ -10905,7 +10909,8 @@ "litellm_provider": "deepinfra", "mode": "chat", "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-14B": { "max_tokens": 40960, @@ -10915,7 +10920,8 @@ "output_cost_per_token": 2.4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-235B-A22B": { "max_tokens": 40960, @@ -10925,7 +10931,8 @@ "output_cost_per_token": 5.4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-235B-A22B-Instruct-2507": { "max_tokens": 262144, @@ -10935,7 +10942,8 @@ "output_cost_per_token": 6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-235B-A22B-Thinking-2507": { "max_tokens": 262144, @@ -10945,7 +10953,8 @@ "output_cost_per_token": 2.9e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-30B-A3B": { "max_tokens": 40960, @@ -10955,7 +10964,8 @@ "output_cost_per_token": 2.9e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-32B": { "max_tokens": 40960, @@ -10965,7 +10975,8 @@ "output_cost_per_token": 2.8e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct": { "max_tokens": 262144, @@ -10975,7 +10986,8 @@ "output_cost_per_token": 1.6e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo": { "max_tokens": 262144, @@ -10985,7 +10997,8 @@ "output_cost_per_token": 1.2e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Next-80B-A3B-Instruct": { "max_tokens": 262144, @@ -10995,7 +11008,8 @@ "output_cost_per_token": 1.4e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Next-80B-A3B-Thinking": { "max_tokens": 262144, @@ -11005,7 +11019,8 @@ "output_cost_per_token": 1.4e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Sao10K/L3-8B-Lunaris-v1-Turbo": { "max_tokens": 8192, @@ -11056,7 +11071,8 @@ "cache_read_input_token_cost": 3.3e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/anthropic/claude-4-opus": { "max_tokens": 200000, @@ -11066,7 +11082,8 @@ "output_cost_per_token": 8.25e-05, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/anthropic/claude-4-sonnet": { "max_tokens": 200000, @@ -11076,7 +11093,8 @@ "output_cost_per_token": 1.65e-05, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1": { "max_tokens": 163840, @@ -11086,7 +11104,8 @@ "output_cost_per_token": 2.4e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-0528": { "max_tokens": 163840, @@ -11097,7 +11116,8 @@ "cache_read_input_token_cost": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-0528-Turbo": { "max_tokens": 32768, @@ -11107,7 +11127,8 @@ "output_cost_per_token": 3e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-Distill-Llama-70B": { "max_tokens": 131072, @@ -11127,7 +11148,8 @@ "output_cost_per_token": 2.7e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-Turbo": { "max_tokens": 40960, @@ -11137,7 +11159,8 @@ "output_cost_per_token": 3e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3": { "max_tokens": 163840, @@ -11147,7 +11170,8 @@ "output_cost_per_token": 8.9e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3-0324": { "max_tokens": 163840, @@ -11157,7 +11181,8 @@ "output_cost_per_token": 8.8e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3.1": { "max_tokens": 163840, @@ -11169,7 +11194,8 @@ "litellm_provider": "deepinfra", "mode": "chat", "supports_tool_choice": true, - "supports_reasoning": true + "supports_reasoning": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3.1-Terminus": { "max_tokens": 163840, @@ -11180,7 +11206,8 @@ "cache_read_input_token_cost": 2.16e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemini-2.0-flash-001": { "deprecation_date": "2026-06-01", @@ -11191,7 +11218,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemini-2.5-flash": { "max_tokens": 1000000, @@ -11201,7 +11229,8 @@ "output_cost_per_token": 2.5e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemini-2.5-pro": { "max_tokens": 1000000, @@ -11211,7 +11240,8 @@ "output_cost_per_token": 1e-05, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemma-3-12b-it": { "max_tokens": 131072, @@ -11221,7 +11251,8 @@ "output_cost_per_token": 1e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemma-3-27b-it": { "max_tokens": 131072, @@ -11231,7 +11262,8 @@ "output_cost_per_token": 1.6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemma-3-4b-it": { "max_tokens": 131072, @@ -11241,7 +11273,8 @@ "output_cost_per_token": 8e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-3.2-11B-Vision-Instruct": { "max_tokens": 131072, @@ -11261,7 +11294,8 @@ "output_cost_per_token": 2e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-3.3-70B-Instruct": { "max_tokens": 131072, @@ -11271,7 +11305,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo": { "max_tokens": 131072, @@ -11281,6 +11316,7 @@ "output_cost_per_token": 3.9e-07, "litellm_provider": "deepinfra", "mode": "chat", + "supports_function_calling": true, "supports_tool_choice": true }, "deepinfra/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { @@ -11291,7 +11327,8 @@ "output_cost_per_token": 6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-4-Scout-17B-16E-Instruct": { "max_tokens": 327680, @@ -11301,7 +11338,8 @@ "output_cost_per_token": 3e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-Guard-3-8B": { "max_tokens": 131072, @@ -11331,7 +11369,8 @@ "output_cost_per_token": 6e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct": { "max_tokens": 131072, @@ -11341,7 +11380,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": { "max_tokens": 131072, @@ -11351,7 +11391,8 @@ "output_cost_per_token": 2.8e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct": { "max_tokens": 131072, @@ -11361,7 +11402,8 @@ "output_cost_per_token": 5e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": { "max_tokens": 131072, @@ -11371,7 +11413,8 @@ "output_cost_per_token": 3e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/microsoft/WizardLM-2-8x22B": { "max_tokens": 65536, @@ -11391,7 +11434,8 @@ "output_cost_per_token": 1.4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mistral-Nemo-Instruct-2407": { "max_tokens": 131072, @@ -11401,7 +11445,8 @@ "output_cost_per_token": 4e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mistral-Small-24B-Instruct-2501": { "max_tokens": 32768, @@ -11411,7 +11456,8 @@ "output_cost_per_token": 8e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mistral-Small-3.2-24B-Instruct-2506": { "max_tokens": 128000, @@ -11421,7 +11467,8 @@ "output_cost_per_token": 2e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mixtral-8x7B-Instruct-v0.1": { "max_tokens": 32768, @@ -11431,7 +11478,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/moonshotai/Kimi-K2-Instruct": { "max_tokens": 131072, @@ -11441,7 +11489,8 @@ "output_cost_per_token": 2e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/moonshotai/Kimi-K2-Instruct-0905": { "max_tokens": 262144, @@ -11452,7 +11501,8 @@ "cache_read_input_token_cost": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/nvidia/Llama-3.1-Nemotron-70B-Instruct": { "max_tokens": 131072, @@ -11462,7 +11512,8 @@ "output_cost_per_token": 6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/nvidia/Llama-3.3-Nemotron-Super-49B-v1.5": { "max_tokens": 131072, @@ -11472,7 +11523,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/nvidia/NVIDIA-Nemotron-Nano-9B-v2": { "max_tokens": 131072, @@ -11482,7 +11534,8 @@ "output_cost_per_token": 1.6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/openai/gpt-oss-120b": { "max_tokens": 131072, @@ -11492,7 +11545,8 @@ "output_cost_per_token": 4.5e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/openai/gpt-oss-20b": { "max_tokens": 131072, @@ -11502,7 +11556,8 @@ "output_cost_per_token": 1.5e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/zai-org/GLM-4.5": { "max_tokens": 131072, @@ -11512,7 +11567,8 @@ "output_cost_per_token": 1.6e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepseek/deepseek-chat": { "cache_creation_input_token_cost": 0.0, diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 7f0b3b5b501..c0777559584 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -96,6 +96,19 @@ def test_supports_function_calling_github_anthropic_alias(): ) +def test_supports_function_calling_deepinfra_llama(): + """Test that deepinfra Llama models correctly report function calling support. + + Regression test for https://github.com/BerriAI/litellm/issues/22619 + """ + assert ( + litellm.utils.supports_function_calling( + model="deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo" + ) + is True + ) + + def test_supports_function_calling_unknown_github_alias_returns_false(): assert ( litellm.utils.supports_function_calling( From 5ad0d0367181af342e281e11b8b6c3afc9055e08 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 10:26:03 -0300 Subject: [PATCH 038/171] fix(proxy): encode batch IDs with model info when x-litellm-model header is used When create_batch routes via x-litellm-model header, the response batch_id was returned raw without model routing info. This meant retrieve_batch could not determine which provider/credentials to use, defaulting to "openai" instead of the correct provider (e.g., VLLM). Now encodes batch_id, output_file_id, and error_file_id with model info (same pattern as the model-embedded file_id flow in Scenario 1), so retrieve_batch can decode and route back to the correct provider. --- litellm/proxy/batches_endpoints/endpoints.py | 21 +- .../test_batch_x_litellm_model_encoding.py | 362 ++++++++++++++++++ 2 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 tests/litellm/proxy/test_batch_x_litellm_model_encoding.py diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 1c9ba6cb248..7352e6e2085 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -242,7 +242,26 @@ async def create_batch( # noqa: PLR0915 custom_llm_provider=credentials["custom_llm_provider"], **_create_batch_data # type: ignore ) - + + # Encode response IDs with model info so retrieve_batch + # can route back to the correct provider/credentials. + if response and hasattr(response, "id") and response.id: + response.id = encode_file_id_with_model( + file_id=response.id, + model=model_param, + id_type="batch", + ) + + if hasattr(response, "output_file_id") and response.output_file_id: + response.output_file_id = encode_file_id_with_model( + file_id=response.output_file_id, model=model_param + ) + + if hasattr(response, "error_file_id") and response.error_file_id: + response.error_file_id = encode_file_id_with_model( + file_id=response.error_file_id, model=model_param + ) + verbose_proxy_logger.debug(f"Created batch using model: {model_param}") else: # SCENARIO 3: Fallback to custom_llm_provider (uses env variables) diff --git a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py new file mode 100644 index 00000000000..01d6a8aaacd --- /dev/null +++ b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py @@ -0,0 +1,362 @@ +""" +Unit tests for batch ID encoding when x-litellm-model header is used. + +Verifies that create_batch encodes response IDs with model info so that +retrieve_batch can route back to the correct provider/credentials. +""" + +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import litellm +from litellm.proxy.openai_files_endpoints.common_utils import ( + decode_model_from_file_id, + get_original_file_id, +) +from litellm.types.utils import LiteLLMBatch + + +def _make_mock_request(headers: dict) -> MagicMock: + """Create a mock FastAPI Request with the given headers.""" + mock_request = MagicMock() + mock_request.headers = headers + mock_request.query_params = {} + mock_request.url = MagicMock() + mock_request.url.port = 4000 + mock_request.method = "POST" + mock_request.url.path = "/v1/batches" + return mock_request + + +def _make_batch_response( + batch_id: str = "batch_abc123", + input_file_id: str = "file-input456", + output_file_id: str = None, + error_file_id: str = None, + status: str = "validating", +) -> LiteLLMBatch: + """Create a mock LiteLLMBatch response from a provider.""" + return LiteLLMBatch( + id=batch_id, + object="batch", + status=status, + endpoint="/v1/chat/completions", + input_file_id=input_file_id, + completion_window="24h", + created_at=1234567890, + output_file_id=output_file_id, + error_file_id=error_file_id, + ) + + +@pytest.mark.asyncio +async def test_create_batch_with_x_litellm_model_encodes_batch_id(): + """ + When x-litellm-model header is provided, create_batch should encode the + response batch_id with model info so retrieve_batch can route correctly. + """ + from litellm.proxy.batches_endpoints.endpoints import create_batch + + model_name = "my-vllm-model" + raw_batch_id = "batch_abc123" + + mock_response = _make_batch_response(batch_id=raw_batch_id) + mock_request = _make_mock_request(headers={"x-litellm-model": model_name}) + mock_fastapi_response = MagicMock() + mock_user_api_key_dict = MagicMock() + mock_user_api_key_dict.parent_otel_span = None + mock_user_api_key_dict.user_id = "test_user" + + mock_credentials = { + "api_key": "sk-test", + "api_base": "http://vllm:8000", + "custom_llm_provider": "openai", + } + + with ( + patch( + "litellm.proxy.batches_endpoints.endpoints._read_request_body", + new=AsyncMock( + return_value={"input_file_id": "file-input456", "endpoint": "/v1/chat/completions", "completion_window": "24h"} + ), + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.ProxyBaseLLMRequestProcessing" + ) as mock_processor_cls, + patch( + "litellm.proxy.batches_endpoints.endpoints.get_credentials_for_model", + return_value=mock_credentials, + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.prepare_data_with_credentials", + ), + patch( + "litellm.acreate_batch", + new=AsyncMock(return_value=mock_response), + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.is_known_model", + return_value=False, + ), + patch("litellm.proxy.proxy_server.general_settings", {}), + patch("litellm.proxy.proxy_server.llm_router", MagicMock()), + patch("litellm.proxy.proxy_server.proxy_config", MagicMock()), + patch("litellm.proxy.proxy_server.version", "1.0.0"), + patch( + "litellm.proxy.proxy_server.proxy_logging_obj", + MagicMock( + post_call_success_hook=AsyncMock(return_value=mock_response), + update_request_status=AsyncMock(), + ), + ), + ): + # Setup the mock processor to return data and logging obj + mock_processor = MagicMock() + mock_processor.common_processing_pre_call_logic = AsyncMock( + return_value=( + {"input_file_id": "file-input456", "endpoint": "/v1/chat/completions", "completion_window": "24h"}, + MagicMock(), + ) + ) + mock_processor_cls.return_value = mock_processor + + response = await create_batch( + request=mock_request, + fastapi_response=mock_fastapi_response, + provider=None, + user_api_key_dict=mock_user_api_key_dict, + ) + + # The batch_id should be encoded with model info + assert response.id != raw_batch_id, ( + f"Expected batch_id to be encoded, but got raw ID: {response.id}" + ) + assert response.id.startswith("batch_"), ( + f"Encoded batch_id should keep batch_ prefix, got: {response.id}" + ) + + # Should be decodable back to the original + decoded_model = decode_model_from_file_id(response.id) + assert decoded_model == model_name, ( + f"Expected model '{model_name}' from decoded batch_id, got: {decoded_model}" + ) + + original_id = get_original_file_id(response.id) + assert original_id == raw_batch_id, ( + f"Expected original ID '{raw_batch_id}', got: {original_id}" + ) + + +@pytest.mark.asyncio +async def test_create_batch_with_x_litellm_model_encodes_output_and_error_file_ids(): + """ + When a completed batch is returned with output_file_id and error_file_id, + these should also be encoded with model info. + """ + from litellm.proxy.batches_endpoints.endpoints import create_batch + + model_name = "my-vllm-model" + raw_output_file = "file-output789" + raw_error_file = "file-error012" + + mock_response = _make_batch_response( + batch_id="batch_abc123", + output_file_id=raw_output_file, + error_file_id=raw_error_file, + status="completed", + ) + mock_request = _make_mock_request(headers={"x-litellm-model": model_name}) + mock_fastapi_response = MagicMock() + mock_user_api_key_dict = MagicMock() + mock_user_api_key_dict.parent_otel_span = None + mock_user_api_key_dict.user_id = "test_user" + + mock_credentials = { + "api_key": "sk-test", + "api_base": "http://vllm:8000", + "custom_llm_provider": "openai", + } + + with ( + patch( + "litellm.proxy.batches_endpoints.endpoints._read_request_body", + new=AsyncMock( + return_value={"input_file_id": "file-input456", "endpoint": "/v1/chat/completions", "completion_window": "24h"} + ), + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.ProxyBaseLLMRequestProcessing" + ) as mock_processor_cls, + patch( + "litellm.proxy.batches_endpoints.endpoints.get_credentials_for_model", + return_value=mock_credentials, + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.prepare_data_with_credentials", + ), + patch( + "litellm.acreate_batch", + new=AsyncMock(return_value=mock_response), + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.is_known_model", + return_value=False, + ), + patch("litellm.proxy.proxy_server.general_settings", {}), + patch("litellm.proxy.proxy_server.llm_router", MagicMock()), + patch("litellm.proxy.proxy_server.proxy_config", MagicMock()), + patch("litellm.proxy.proxy_server.version", "1.0.0"), + patch( + "litellm.proxy.proxy_server.proxy_logging_obj", + MagicMock( + post_call_success_hook=AsyncMock(return_value=mock_response), + update_request_status=AsyncMock(), + ), + ), + ): + mock_processor = MagicMock() + mock_processor.common_processing_pre_call_logic = AsyncMock( + return_value=( + {"input_file_id": "file-input456", "endpoint": "/v1/chat/completions", "completion_window": "24h"}, + MagicMock(), + ) + ) + mock_processor_cls.return_value = mock_processor + + response = await create_batch( + request=mock_request, + fastapi_response=mock_fastapi_response, + provider=None, + user_api_key_dict=mock_user_api_key_dict, + ) + + # output_file_id should be encoded + assert decode_model_from_file_id(response.output_file_id) == model_name + assert get_original_file_id(response.output_file_id) == raw_output_file + + # error_file_id should be encoded + assert decode_model_from_file_id(response.error_file_id) == model_name + assert get_original_file_id(response.error_file_id) == raw_error_file + + +@pytest.mark.asyncio +async def test_create_batch_without_x_litellm_model_returns_raw_ids(): + """ + Without x-litellm-model header, create_batch should NOT encode batch IDs + (falls through to Scenario 3 / custom_llm_provider fallback). + """ + from litellm.proxy.batches_endpoints.endpoints import create_batch + + raw_batch_id = "batch_abc123" + mock_response = _make_batch_response(batch_id=raw_batch_id) + mock_request = _make_mock_request(headers={}) + mock_fastapi_response = MagicMock() + mock_user_api_key_dict = MagicMock() + mock_user_api_key_dict.parent_otel_span = None + mock_user_api_key_dict.user_id = "test_user" + + with ( + patch( + "litellm.proxy.batches_endpoints.endpoints._read_request_body", + new=AsyncMock( + return_value={"input_file_id": "file-input456", "endpoint": "/v1/chat/completions", "completion_window": "24h"} + ), + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.ProxyBaseLLMRequestProcessing" + ) as mock_processor_cls, + patch( + "litellm.acreate_batch", + new=AsyncMock(return_value=mock_response), + ), + patch( + "litellm.proxy.batches_endpoints.endpoints.is_known_model", + return_value=False, + ), + patch("litellm.proxy.proxy_server.general_settings", {}), + patch("litellm.proxy.proxy_server.llm_router", None), + patch("litellm.proxy.proxy_server.proxy_config", MagicMock()), + patch("litellm.proxy.proxy_server.version", "1.0.0"), + patch( + "litellm.proxy.proxy_server.proxy_logging_obj", + MagicMock( + post_call_success_hook=AsyncMock(return_value=mock_response), + update_request_status=AsyncMock(), + ), + ), + ): + mock_processor = MagicMock() + mock_processor.common_processing_pre_call_logic = AsyncMock( + return_value=( + {"input_file_id": "file-input456", "endpoint": "/v1/chat/completions", "completion_window": "24h"}, + MagicMock(), + ) + ) + mock_processor_cls.return_value = mock_processor + + response = await create_batch( + request=mock_request, + fastapi_response=mock_fastapi_response, + provider=None, + user_api_key_dict=mock_user_api_key_dict, + ) + + # Without x-litellm-model, the batch_id should remain raw + assert response.id == raw_batch_id + assert decode_model_from_file_id(response.id) is None + + +class TestBatchIdRoundTripWithRetrieve: + """ + Tests that batch IDs encoded during create_batch can be decoded + correctly during retrieve_batch (Scenario 1: model_from_id). + """ + + def test_encoded_batch_id_is_decoded_for_retrieve(self): + """ + Simulates the full round-trip: create encodes the ID, + retrieve decodes it to get the model and original batch_id. + """ + from litellm.proxy.openai_files_endpoints.common_utils import ( + encode_file_id_with_model, + ) + + model_name = "my-vllm-model" + raw_batch_id = "batch_vllm_12345" + + # What create_batch does: + encoded_id = encode_file_id_with_model( + file_id=raw_batch_id, model=model_name, id_type="batch" + ) + + # What retrieve_batch does: + decoded_model = decode_model_from_file_id(encoded_id) + original_id = get_original_file_id(encoded_id) + + assert decoded_model == model_name + assert original_id == raw_batch_id + + def test_vllm_style_batch_id_roundtrip(self): + """ + VLLM may return batch IDs in various formats. + Verify round-trip works for common patterns. + """ + from litellm.proxy.openai_files_endpoints.common_utils import ( + encode_file_id_with_model, + ) + + test_cases = [ + ("batch_abc123", "vllm-llama3"), + ("batch_67890", "openai/llama-3-8b"), + ("batch_some-uuid-here", "my-custom-vllm"), + ] + + for raw_id, model in test_cases: + encoded = encode_file_id_with_model( + file_id=raw_id, model=model, id_type="batch" + ) + assert encoded.startswith("batch_") + assert decode_model_from_file_id(encoded) == model + assert get_original_file_id(encoded) == raw_id From 3426b905cedd66d59506174efb04d8c53566b53b Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:44:47 -0300 Subject: [PATCH 039/171] Update tests/litellm/proxy/test_batch_x_litellm_model_encoding.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- tests/litellm/proxy/test_batch_x_litellm_model_encoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py index 01d6a8aaacd..062443d9425 100644 --- a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py +++ b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py @@ -33,8 +33,8 @@ def _make_mock_request(headers: dict) -> MagicMock: def _make_batch_response( batch_id: str = "batch_abc123", input_file_id: str = "file-input456", - output_file_id: str = None, - error_file_id: str = None, + output_file_id: Optional[str] = None, + error_file_id: Optional[str] = None, status: str = "validating", ) -> LiteLLMBatch: """Create a mock LiteLLMBatch response from a provider.""" From 7d664f0c096a7f85befc17b22d80c8cbf9097a93 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:45:00 -0300 Subject: [PATCH 040/171] Update tests/litellm/proxy/test_batch_x_litellm_model_encoding.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- tests/litellm/proxy/test_batch_x_litellm_model_encoding.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py index 062443d9425..5150b57568b 100644 --- a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py +++ b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py @@ -5,7 +5,6 @@ Verifies that create_batch encodes response IDs with model info so that retrieve_batch can route back to the correct provider/credentials. """ -import json from unittest.mock import AsyncMock, MagicMock, patch import pytest From 096edface5b7772eccf398113a57b26941c00457 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:46:14 -0300 Subject: [PATCH 041/171] Update litellm/proxy/batches_endpoints/endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/proxy/batches_endpoints/endpoints.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 7352e6e2085..58c8e2d4d0c 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -257,11 +257,16 @@ async def create_batch( # noqa: PLR0915 file_id=response.output_file_id, model=model_param ) - if hasattr(response, "error_file_id") and response.error_file_id: + if hasattr(response, "error_file_id") and response.error_file_id: response.error_file_id = encode_file_id_with_model( file_id=response.error_file_id, model=model_param ) + if hasattr(response, "input_file_id") and response.input_file_id: + response.input_file_id = encode_file_id_with_model( + file_id=response.input_file_id, model=model_param + ) + verbose_proxy_logger.debug(f"Created batch using model: {model_param}") else: # SCENARIO 3: Fallback to custom_llm_provider (uses env variables) From 9463de0c6629354dbc616b1a1beb394f27a45b80 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 10:51:48 -0300 Subject: [PATCH 042/171] fix: correct indentation from commit suggestions and add missing Optional import --- litellm/proxy/batches_endpoints/endpoints.py | 2 +- tests/litellm/proxy/test_batch_x_litellm_model_encoding.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 58c8e2d4d0c..cdee69f2b30 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -257,7 +257,7 @@ async def create_batch( # noqa: PLR0915 file_id=response.output_file_id, model=model_param ) - if hasattr(response, "error_file_id") and response.error_file_id: + if hasattr(response, "error_file_id") and response.error_file_id: response.error_file_id = encode_file_id_with_model( file_id=response.error_file_id, model=model_param ) diff --git a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py index 5150b57568b..521a3632dcb 100644 --- a/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py +++ b/tests/litellm/proxy/test_batch_x_litellm_model_encoding.py @@ -5,6 +5,7 @@ Verifies that create_batch encodes response IDs with model info so that retrieve_batch can route back to the correct provider/credentials. """ +from typing import Optional from unittest.mock import AsyncMock, MagicMock, patch import pytest From 9fd4c00b064e6358fdb5202daa417b3cfb482796 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 11:06:38 -0300 Subject: [PATCH 043/171] fix(proxy): re-encode response IDs in retrieve_batch for model-based routing The provider returns raw IDs in the retrieve response (output_file_id, error_file_id). These need to be encoded with model info so the client can use them for subsequent file download calls through the proxy. --- litellm/proxy/batches_endpoints/endpoints.py | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index cdee69f2b30..d3e983c5ad5 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -464,8 +464,30 @@ async def retrieve_batch( # noqa: PLR0915 custom_llm_provider=credentials["custom_llm_provider"], **data # type: ignore ) - - + + # Re-encode response IDs so the client always sees encoded IDs. + # The provider returns raw IDs (e.g. output_file_id, error_file_id) + # which the client needs encoded to route future file downloads. + if response and hasattr(response, "id") and response.id: + response.id = encode_file_id_with_model( + file_id=response.id, model=model_from_id, id_type="batch", + ) + + if hasattr(response, "output_file_id") and response.output_file_id: + response.output_file_id = encode_file_id_with_model( + file_id=response.output_file_id, model=model_from_id + ) + + if hasattr(response, "error_file_id") and response.error_file_id: + response.error_file_id = encode_file_id_with_model( + file_id=response.error_file_id, model=model_from_id + ) + + if hasattr(response, "input_file_id") and response.input_file_id: + response.input_file_id = encode_file_id_with_model( + file_id=response.input_file_id, model=model_from_id + ) + verbose_proxy_logger.debug( f"Retrieved batch using model: {model_from_id}, original_id: {original_batch_id}" ) From 7506fd0426a70c0387fd811d44fdd06c1fc0006b Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 11:22:43 -0300 Subject: [PATCH 044/171] fix(proxy): re-encode response IDs in cancel_batch for model-based routing --- litellm/proxy/batches_endpoints/endpoints.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index d3e983c5ad5..7ab50e321ac 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -855,7 +855,28 @@ async def cancel_batch( custom_llm_provider=credentials["custom_llm_provider"], **data # type: ignore ) - + + # Re-encode response IDs so the client always sees encoded IDs. + if response and hasattr(response, "id") and response.id: + response.id = encode_file_id_with_model( + file_id=response.id, model=model_from_id, id_type="batch", + ) + + if hasattr(response, "output_file_id") and response.output_file_id: + response.output_file_id = encode_file_id_with_model( + file_id=response.output_file_id, model=model_from_id + ) + + if hasattr(response, "error_file_id") and response.error_file_id: + response.error_file_id = encode_file_id_with_model( + file_id=response.error_file_id, model=model_from_id + ) + + if hasattr(response, "input_file_id") and response.input_file_id: + response.input_file_id = encode_file_id_with_model( + file_id=response.input_file_id, model=model_from_id + ) + verbose_proxy_logger.debug( f"Cancelled batch using model: {model_from_id}, original_id: {original_batch_id}" ) From 59bde4a81a92c9e7b8f48ca2e32311a735a51a7d Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 11:38:50 -0300 Subject: [PATCH 045/171] refactor(proxy): extract encode_batch_response_ids helper and fix list_batches encoding Extract duplicated batch ID encoding logic into a shared helper encode_batch_response_ids() in common_utils.py. Use it in create_batch, retrieve_batch, and cancel_batch. Also add encoding to list_batches when x-litellm-model is used. --- litellm/proxy/batches_endpoints/endpoints.py | 77 +++---------------- .../openai_files_endpoints/common_utils.py | 16 ++++ 2 files changed, 27 insertions(+), 66 deletions(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 7ab50e321ac..ae99d59c631 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -23,6 +23,7 @@ from litellm.proxy.common_utils.openai_endpoint_utils import ( from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, decode_model_from_file_id, + encode_batch_response_ids, encode_file_id_with_model, get_batch_from_database, get_credentials_for_model, @@ -243,29 +244,7 @@ async def create_batch( # noqa: PLR0915 **_create_batch_data # type: ignore ) - # Encode response IDs with model info so retrieve_batch - # can route back to the correct provider/credentials. - if response and hasattr(response, "id") and response.id: - response.id = encode_file_id_with_model( - file_id=response.id, - model=model_param, - id_type="batch", - ) - - if hasattr(response, "output_file_id") and response.output_file_id: - response.output_file_id = encode_file_id_with_model( - file_id=response.output_file_id, model=model_param - ) - - if hasattr(response, "error_file_id") and response.error_file_id: - response.error_file_id = encode_file_id_with_model( - file_id=response.error_file_id, model=model_param - ) - - if hasattr(response, "input_file_id") and response.input_file_id: - response.input_file_id = encode_file_id_with_model( - file_id=response.input_file_id, model=model_param - ) + encode_batch_response_ids(response, model=model_param) verbose_proxy_logger.debug(f"Created batch using model: {model_param}") else: @@ -465,28 +444,7 @@ async def retrieve_batch( # noqa: PLR0915 **data # type: ignore ) - # Re-encode response IDs so the client always sees encoded IDs. - # The provider returns raw IDs (e.g. output_file_id, error_file_id) - # which the client needs encoded to route future file downloads. - if response and hasattr(response, "id") and response.id: - response.id = encode_file_id_with_model( - file_id=response.id, model=model_from_id, id_type="batch", - ) - - if hasattr(response, "output_file_id") and response.output_file_id: - response.output_file_id = encode_file_id_with_model( - file_id=response.output_file_id, model=model_from_id - ) - - if hasattr(response, "error_file_id") and response.error_file_id: - response.error_file_id = encode_file_id_with_model( - file_id=response.error_file_id, model=model_from_id - ) - - if hasattr(response, "input_file_id") and response.input_file_id: - response.input_file_id = encode_file_id_with_model( - file_id=response.input_file_id, model=model_from_id - ) + encode_batch_response_ids(response, model=model_from_id) verbose_proxy_logger.debug( f"Retrieved batch using model: {model_from_id}, original_id: {original_batch_id}" @@ -679,7 +637,13 @@ async def list_batches( limit=limit, **data # type: ignore ) - + + # Encode batch IDs in the list response so clients can use + # them for retrieve/cancel/file downloads through the proxy. + if response and hasattr(response, "data") and response.data: + for batch in response.data: + encode_batch_response_ids(batch, model=model_param) + verbose_proxy_logger.debug(f"Listed batches using model: {model_param}") # SCENARIO 2 (alternative): target_model_names based routing @@ -856,26 +820,7 @@ async def cancel_batch( **data # type: ignore ) - # Re-encode response IDs so the client always sees encoded IDs. - if response and hasattr(response, "id") and response.id: - response.id = encode_file_id_with_model( - file_id=response.id, model=model_from_id, id_type="batch", - ) - - if hasattr(response, "output_file_id") and response.output_file_id: - response.output_file_id = encode_file_id_with_model( - file_id=response.output_file_id, model=model_from_id - ) - - if hasattr(response, "error_file_id") and response.error_file_id: - response.error_file_id = encode_file_id_with_model( - file_id=response.error_file_id, model=model_from_id - ) - - if hasattr(response, "input_file_id") and response.input_file_id: - response.input_file_id = encode_file_id_with_model( - file_id=response.input_file_id, model=model_from_id - ) + encode_batch_response_ids(response, model=model_from_id) verbose_proxy_logger.debug( f"Cancelled batch using model: {model_from_id}, original_id: {original_batch_id}" diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index ceaf3c7550e..343ea119672 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -129,6 +129,22 @@ def encode_file_id_with_model( return f"{prefix}{encoded_b64}" +def encode_batch_response_ids(response, model: str) -> None: + """Encode all IDs in a batch response with model routing info (in-place).""" + if not response or not hasattr(response, "id") or not response.id: + return + response.id = encode_file_id_with_model( + file_id=response.id, model=model, id_type="batch" + ) + for attr in ("output_file_id", "error_file_id", "input_file_id"): + if hasattr(response, attr) and getattr(response, attr): + setattr( + response, + attr, + encode_file_id_with_model(file_id=getattr(response, attr), model=model), + ) + + def decode_model_from_file_id(encoded_id: str) -> Optional[str]: """ Extract model name from an encoded file/batch ID. From b83373d29c43667d108e087b95a5cadc9262dba9 Mon Sep 17 00:00:00 2001 From: Ephrim Stanley Date: Tue, 3 Mar 2026 11:01:49 -0500 Subject: [PATCH 046/171] Managed batches - Address PR bot comments from #22464 --- litellm/files/main.py | 2 +- litellm/llms/vertex_ai/batches/handler.py | 5 +- .../llms/vertex_ai/files/transformation.py | 5 +- .../test_file_retrieve_provider_routing.py | 127 ++++++++++++++++++ .../test_vertex_ai_files_transformation.py | 48 ++++++- 5 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 tests/test_litellm/llms/vertex_ai/files/test_file_retrieve_provider_routing.py diff --git a/litellm/files/main.py b/litellm/files/main.py index 66d3a97468d..f0a8112fbdf 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -336,7 +336,7 @@ async def afile_retrieve( @client def file_retrieve( file_id: str, - custom_llm_provider: Literal["openai", "azure", "hosted_vllm", "manus"] = "openai", + custom_llm_provider: Literal["openai", "azure", "gemini", "vertex_ai", "hosted_vllm", "manus"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, diff --git a/litellm/llms/vertex_ai/batches/handler.py b/litellm/llms/vertex_ai/batches/handler.py index ba3b5fb7a2c..5f1fefca963 100644 --- a/litellm/llms/vertex_ai/batches/handler.py +++ b/litellm/llms/vertex_ai/batches/handler.py @@ -115,9 +115,10 @@ class VertexAIBatchPrediction(VertexLLM): data=json.dumps(vertex_batch_request), ) except httpx.HTTPStatusError as e: - error_body = e.response.text if hasattr(e, 'response') else "N/A" + error_body = e.response.text litellm.verbose_logger.error( - f"Vertex AI batch create failed: status={e.response.status_code}, body={error_body[:1000]}" + "Vertex AI batch create failed: status=%s, body=%s", + e.response.status_code, error_body[:1000], ) raise if response.status_code != 200: diff --git a/litellm/llms/vertex_ai/files/transformation.py b/litellm/llms/vertex_ai/files/transformation.py index f0493cd6be9..bf3ed5e6ac9 100644 --- a/litellm/llms/vertex_ai/files/transformation.py +++ b/litellm/llms/vertex_ai/files/transformation.py @@ -408,10 +408,11 @@ class VertexAIFilesConfig(VertexBase, BaseFilesConfig): file_id = "deleted" if hasattr(raw_response, "request") and raw_response.request: url = str(raw_response.request.url) - if "/o/" in url: + if "/b/" in url and "/o/" in url: import urllib.parse + bucket_part = url.split("/b/")[-1].split("/o/")[0] encoded_name = url.split("/o/")[-1].split("?")[0] - file_id = f"gs://{urllib.parse.unquote(encoded_name)}" + file_id = f"gs://{bucket_part}/{urllib.parse.unquote(encoded_name)}" return FileDeleted(id=file_id, deleted=True, object="file") def transform_list_files_request( diff --git a/tests/test_litellm/llms/vertex_ai/files/test_file_retrieve_provider_routing.py b/tests/test_litellm/llms/vertex_ai/files/test_file_retrieve_provider_routing.py new file mode 100644 index 00000000000..68d5e2035f7 --- /dev/null +++ b/tests/test_litellm/llms/vertex_ai/files/test_file_retrieve_provider_routing.py @@ -0,0 +1,127 @@ +""" +Tests for Fix 1: file_retrieve Literal type was missing 'vertex_ai' and 'gemini', +causing a type mismatch when afile_retrieve delegated to the sync function. +""" + +import pytest +from unittest.mock import MagicMock, patch + +from litellm.files.main import file_retrieve + + +class TestFileRetrieveProviderRouting: + """ + Verify that file_retrieve accepts 'vertex_ai' and 'gemini' providers and + routes them through ProviderConfigManager / base_llm_http_handler. + """ + + def _make_mock_file_object(self): + mock = MagicMock() + mock.model_dump.return_value = { + "id": "gs://my-bucket/file.jsonl", + "object": "file", + "bytes": 1024, + "created_at": 0, + "filename": "file.jsonl", + "purpose": "batch", + "status": "processed", + } + return mock + + def test_should_route_vertex_ai_through_provider_config(self): + """ + Regression: file_retrieve Literal type was missing 'vertex_ai', + so passing custom_llm_provider='vertex_ai' would fail type-checking + and potentially cause a routing failure at runtime. + """ + mock_file = self._make_mock_file_object() + + with patch( + "litellm.files.main.base_llm_http_handler.retrieve_file", + return_value=mock_file, + ) as mock_retrieve: + result = file_retrieve( + file_id="gs://my-bucket/file.jsonl", + custom_llm_provider="vertex_ai", + ) + + mock_retrieve.assert_called_once() + assert result is not None + + def test_should_route_gemini_through_provider_config(self): + """ + Regression: file_retrieve Literal type was also missing 'gemini'. + """ + mock_file = self._make_mock_file_object() + + with patch( + "litellm.files.main.base_llm_http_handler.retrieve_file", + return_value=mock_file, + ) as mock_retrieve: + result = file_retrieve( + file_id="some-gemini-file-id", + custom_llm_provider="gemini", + ) + + mock_retrieve.assert_called_once() + assert result is not None + + def test_should_pass_file_id_to_handler_for_vertex_ai(self): + """Verify the file_id is forwarded correctly to the underlying handler.""" + mock_file = self._make_mock_file_object() + expected_file_id = "gs://my-bucket/path/to/file.jsonl" + + with patch( + "litellm.files.main.base_llm_http_handler.retrieve_file", + return_value=mock_file, + ) as mock_retrieve: + file_retrieve( + file_id=expected_file_id, + custom_llm_provider="vertex_ai", + ) + + call_kwargs = mock_retrieve.call_args.kwargs + assert call_kwargs.get("file_id") == expected_file_id + + def test_should_not_raise_bad_request_for_vertex_ai(self): + """ + Before the fix, vertex_ai fell through to the else-branch which raised + BadRequestError. Verify it no longer does. + """ + import litellm + + mock_file = self._make_mock_file_object() + + with patch( + "litellm.files.main.base_llm_http_handler.retrieve_file", + return_value=mock_file, + ): + try: + file_retrieve( + file_id="gs://my-bucket/file.jsonl", + custom_llm_provider="vertex_ai", + ) + except litellm.exceptions.BadRequestError as e: + pytest.fail( + f"file_retrieve raised BadRequestError for vertex_ai: {e}" + ) + + def test_should_not_raise_bad_request_for_gemini(self): + """Same as above but for 'gemini'.""" + import litellm + + mock_file = self._make_mock_file_object() + + with patch( + "litellm.files.main.base_llm_http_handler.retrieve_file", + return_value=mock_file, + ): + try: + file_retrieve( + file_id="some-file-id", + custom_llm_provider="gemini", + ) + except litellm.exceptions.BadRequestError as e: + pytest.fail( + f"file_retrieve raised BadRequestError for gemini: {e}" + ) diff --git a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py index 6f1d753484d..598ad255aca 100644 --- a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py @@ -167,7 +167,7 @@ class TestTransformDeleteFile: assert isinstance(result, FileDeleted) assert result.deleted is True assert result.object == "file" - assert "litellm-vertex-files/publishers/google/models/gemini-2.0-flash-001/abc" in result.id + assert result.id == "gs://my-bucket/litellm-vertex-files/publishers/google/models/gemini-2.0-flash-001/abc" def test_should_fallback_to_deleted_id_when_no_request(self, config): raw_response = MagicMock(spec=httpx.Response) @@ -182,3 +182,49 @@ class TestTransformDeleteFile: assert isinstance(result, FileDeleted) assert result.id == "deleted" assert result.deleted is True + + def test_should_include_bucket_name_in_reconstructed_delete_id(self, config): + """ + Regression: the old code split on /o/ only, dropping the bucket from + the reconstructed gs:// URI. e.g. gs://path/to/file instead of + gs://my-bucket/path/to/file. + """ + raw_response = MagicMock(spec=httpx.Response) + mock_request = MagicMock() + encoded_object = urllib.parse.quote("path/to/file.jsonl", safe="") + mock_request.url = ( + f"https://storage.googleapis.com/storage/v1/b/my-bucket/o/{encoded_object}" + ) + raw_response.request = mock_request + + result = config.transform_delete_file_response( + raw_response=raw_response, + logging_obj=MagicMock(), + litellm_params={}, + ) + + assert result.id == "gs://my-bucket/path/to/file.jsonl" + + def test_should_include_bucket_in_nested_object_path(self, config): + """Verify bucket extraction works with deeply nested GCS object paths.""" + raw_response = MagicMock(spec=httpx.Response) + mock_request = MagicMock() + encoded_object = urllib.parse.quote( + "litellm-vertex-files/publishers/google/models/gemini-2.0-flash-001/abc-123", + safe="", + ) + mock_request.url = ( + f"https://storage.googleapis.com/storage/v1/b/prod-bucket/o/{encoded_object}" + ) + raw_response.request = mock_request + + result = config.transform_delete_file_response( + raw_response=raw_response, + logging_obj=MagicMock(), + litellm_params={}, + ) + + assert result.id == ( + "gs://prod-bucket/litellm-vertex-files/publishers/google/" + "models/gemini-2.0-flash-001/abc-123" + ) From b44755db96fa54c6d2e4c38960ea20cbac0a93fc Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Tue, 3 Mar 2026 22:50:34 +0530 Subject: [PATCH 047/171] fix(proxy): make common_checks opt-in for custom auth via custom_auth_run_common_checks Replaces the skip_route_check approach from PR #22662 with a configurable opt-in flag. By default, common_checks() is not run for custom auth flows, preserving backwards compatibility with pre-#22164 behavior. Users who want budget/team/route enforcement on custom auth can enable it: general_settings: custom_auth_run_common_checks: true Co-Authored-By: Claude Opus 4.6 --- litellm/proxy/auth/auth_checks.py | 28 +++---- litellm/proxy/auth/user_api_key_auth.py | 32 ++++---- .../proxy/auth/test_auth_checks.py | 77 +++++++++---------- .../auth/test_custom_auth_end_user_budget.py | 21 ++++- 4 files changed, 86 insertions(+), 72 deletions(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 91ac58215ab..ef6b0ac462c 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -234,7 +234,6 @@ async def common_checks( request: Request, skip_budget_checks: bool = False, project_object: Optional[LiteLLM_ProjectTableCachedObj] = None, - skip_route_check: bool = False, ) -> bool: """ Common checks across jwt + key-based auth. @@ -454,21 +453,18 @@ async def common_checks( user_object=user_object, route=route, request_body=request_body ) - if not skip_route_check: - token_team = getattr(valid_token, "team_id", None) - token_type: Literal["ui", "api"] = ( - "ui" - if token_team is not None and token_team == "litellm-dashboard" - else "api" - ) - _is_route_allowed = _is_allowed_route( - route=route, - token_type=token_type, - user_obj=user_object, - request=request, - request_data=request_body, - valid_token=valid_token, - ) + token_team = getattr(valid_token, "team_id", None) + token_type: Literal["ui", "api"] = ( + "ui" if token_team is not None and token_team == "litellm-dashboard" else "api" + ) + _is_route_allowed = _is_allowed_route( + route=route, + token_type=token_type, + user_obj=user_object, + request=request, + request_data=request_body, + valid_token=valid_token, + ) # 11. [OPTIONAL] Vector store checks - is the object allowed to access the vector store await vector_store_access_check( diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 1d575fb5131..7b52f6bb96d 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -1752,21 +1752,21 @@ async def _run_post_custom_auth_checks( if _project_obj is not None: valid_token.project_metadata = _project_obj.metadata - _ = await common_checks( - request=request, - request_body=request_data, - team_object=_team_obj, - user_object=user_object, - end_user_object=end_user_object, - general_settings=general_settings, - global_proxy_spend=None, - route=route, - llm_router=llm_router, - proxy_logging_obj=proxy_logging_obj, - valid_token=valid_token, - skip_budget_checks=False, - project_object=_project_obj, - skip_route_check=True, - ) + if general_settings.get("custom_auth_run_common_checks", False): + _ = await common_checks( + request=request, + request_body=request_data, + team_object=_team_obj, + user_object=user_object, + end_user_object=end_user_object, + general_settings=general_settings, + global_proxy_spend=None, + route=route, + llm_router=llm_router, + proxy_logging_obj=proxy_logging_obj, + valid_token=valid_token, + skip_budget_checks=False, + project_object=_project_obj, + ) return valid_token diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index 501c2285d1e..ff1bc5b2581 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -1522,52 +1522,51 @@ async def test_get_fuzzy_user_object_case_insensitive_email(): @pytest.mark.asyncio -async def test_common_checks_skip_route_check_for_custom_auth(): +async def test_custom_auth_common_checks_opt_in(): """ - Test that custom routes (e.g. /ldap/ngs/ready) pass common_checks when - skip_route_check=True, which is the case for custom auth flows. + Test that _run_post_custom_auth_checks only runs common_checks when + custom_auth_run_common_checks is explicitly set to True in general_settings. - Regression test for: custom user-added routes being rejected as admin-only - after _run_post_custom_auth_checks was introduced. + By default (False), common_checks is skipped for backwards compatibility + with custom auth flows that existed before PR #22164. """ - from fastapi import Request + from litellm.proxy.auth.user_api_key_auth import _run_post_custom_auth_checks - from litellm.proxy.auth.auth_checks import common_checks - - mock_request = MagicMock(spec=Request) valid_token = UserAPIKeyAuth(token="test-token") + mock_request = MagicMock() - # Without skip_route_check, a custom route with unknown user should fail - with pytest.raises(Exception): - await common_checks( - request_body={}, - team_object=None, - user_object=None, - end_user_object=None, - global_proxy_spend=None, - general_settings={}, - route="/ldap/ngs/ready", - llm_router=None, - proxy_logging_obj=MagicMock(), + # Default (no flag) — common_checks should NOT be called + with patch( + "litellm.proxy.auth.user_api_key_auth.common_checks", + new_callable=AsyncMock, + ) as mock_common, patch( + "litellm.proxy.proxy_server.general_settings", + {}, + ): + mock_common.return_value = True + result = await _run_post_custom_auth_checks( valid_token=valid_token, request=mock_request, - skip_route_check=False, + request_data={}, + route="/ldap/ngs/ready", + parent_otel_span=None, ) + mock_common.assert_not_called() - # With skip_route_check=True (custom auth path), the same route should pass - result = await common_checks( - request_body={}, - team_object=None, - user_object=None, - end_user_object=None, - global_proxy_spend=None, - general_settings={}, - route="/ldap/ngs/ready", - llm_router=None, - proxy_logging_obj=MagicMock(), - valid_token=valid_token, - request=mock_request, - skip_route_check=True, - ) - - assert result is True + # With flag=True — common_checks SHOULD be called + with patch( + "litellm.proxy.auth.user_api_key_auth.common_checks", + new_callable=AsyncMock, + ) as mock_common, patch( + "litellm.proxy.proxy_server.general_settings", + {"custom_auth_run_common_checks": True}, + ): + mock_common.return_value = True + result = await _run_post_custom_auth_checks( + valid_token=valid_token, + request=mock_request, + request_data={}, + route="/chat/completions", + parent_otel_span=None, + ) + mock_common.assert_called_once() diff --git a/tests/test_litellm/proxy/auth/test_custom_auth_end_user_budget.py b/tests/test_litellm/proxy/auth/test_custom_auth_end_user_budget.py index 73a97188424..18816dcec4a 100644 --- a/tests/test_litellm/proxy/auth/test_custom_auth_end_user_budget.py +++ b/tests/test_litellm/proxy/auth/test_custom_auth_end_user_budget.py @@ -10,9 +10,10 @@ from litellm.proxy._types import UserAPIKeyAuth @pytest.mark.asyncio async def test_custom_auth_run_post_custom_auth_checks_without_end_user_id(): - # Test backwards compatibility + # Test backwards compatibility — common_checks only runs when opt-in flag is set valid_token = UserAPIKeyAuth(token="test_token") + # Default: common_checks should NOT be called with patch( "litellm.proxy.auth.user_api_key_auth.common_checks", new_callable=AsyncMock ) as mock_common: @@ -26,6 +27,24 @@ async def test_custom_auth_run_post_custom_auth_checks_without_end_user_id(): ) assert result.token == "test_token" assert getattr(result, "end_user_id", None) is None + mock_common.assert_not_awaited() + + # With opt-in flag: common_checks SHOULD be called + with patch( + "litellm.proxy.auth.user_api_key_auth.common_checks", new_callable=AsyncMock + ) as mock_common, patch( + "litellm.proxy.proxy_server.general_settings", + {"custom_auth_run_common_checks": True}, + ): + mock_common.return_value = True + result = await _run_post_custom_auth_checks( + valid_token=valid_token, + request=None, + request_data={}, + route="/v1/chat/completions", + parent_otel_span=None, + ) + assert result.token == "test_token" mock_common.assert_awaited_once() From 1fe2e92d3272bcf1c7d21f401efd8b8a32bbd475 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 14:34:32 -0300 Subject: [PATCH 048/171] fix(main): forward enable_json_schema_validation to acompletion_with_mcp The parameter was declared in completion() signature but not passed to acompletion_with_mcp, causing per-request JSON schema validation to silently fall back to the global default when MCP tools are present. --- litellm/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/main.py b/litellm/main.py index 362e5b8b263..75353c1b070 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1170,6 +1170,7 @@ def completion( # type: ignore # noqa: PLR0915 thinking=thinking, web_search_options=web_search_options, shared_session=shared_session, + enable_json_schema_validation=enable_json_schema_validation, **kwargs, ) api_base = kwargs.get("api_base", None) From f6264a9c0fde055be2cdb2b84a2dee6c32c8885a Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 14:50:41 -0300 Subject: [PATCH 049/171] docs(openrouter): add image edit documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add OpenRouter image edit docs to both the provider page and the main image_edits reference page, including supported models, parameter mappings (size→aspect_ratio, quality→image_size), usage examples, proxy configuration, and a note about 4K quality model support. --- docs/my-website/docs/image_edits.md | 71 +++++++++++++++- docs/my-website/docs/providers/openrouter.md | 87 ++++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/image_edits.md b/docs/my-website/docs/image_edits.md index a8438334542..f1cfc0ed8e9 100644 --- a/docs/my-website/docs/image_edits.md +++ b/docs/my-website/docs/image_edits.md @@ -16,7 +16,7 @@ LiteLLM provides image editing functionality that maps to OpenAI's `/images/edit | Supported operations | Create image edits | Single and multiple images supported | | Supported LiteLLM SDK Versions | 1.63.8+ | Gemini support requires 1.79.3+ | | Supported LiteLLM Proxy Versions | 1.71.1+ | Gemini support requires 1.79.3+ | -| Supported LLM providers | **OpenAI**, **Gemini (Google AI Studio)**, **Vertex AI**, **Stability AI**, **AWS Bedrock (Stability)** | Gemini supports the new `gemini-2.5-flash-image` family. Vertex AI supports both Gemini and Imagen models. Stability AI and Bedrock Stability support various image editing operations. | +| Supported LLM providers | **OpenAI**, **Gemini (Google AI Studio)**, **Vertex AI**, **OpenRouter**, **Stability AI**, **AWS Bedrock (Stability)** | Gemini supports the new `gemini-2.5-flash-image` family. Vertex AI supports both Gemini and Imagen models. OpenRouter routes image edits through chat completions. Stability AI and Bedrock Stability support various image editing operations. | #### ⚡️See all supported models and providers at [models.litellm.ai](https://models.litellm.ai/) @@ -244,6 +244,47 @@ response = litellm.image_edit( print(response) ``` + + + + +#### Basic Image Edit +```python showLineNumbers title="OpenRouter Image Edit" +import os +from litellm import image_edit + +os.environ["OPENROUTER_API_KEY"] = "your-api-key" + +response = image_edit( + model="openrouter/google/gemini-2.5-flash-image", + image=open("original_image.png", "rb"), + prompt="Add aurora borealis to the night sky", +) + +print(response) +``` + +#### Multiple Images Edit +```python showLineNumbers title="OpenRouter Multiple Images Edit" +import os +from litellm import image_edit + +os.environ["OPENROUTER_API_KEY"] = "your-api-key" + +response = image_edit( + model="openrouter/google/gemini-2.5-flash-image", + image=[ + open("scene.png", "rb"), + open("style_reference.png", "rb"), + ], + prompt="Blend the reference style into the scene", + size="1536x1024", # mapped to aspect_ratio 3:2 + quality="high", # mapped to image_size 4K +) + +print(response) +``` + @@ -398,6 +439,34 @@ curl -X POST "http://0.0.0.0:4000/v1/images/edits" \ -F "size=1024x1024" ``` + + + + +1. Add the OpenRouter image edit model to your `config.yaml`: +```yaml showLineNumbers title="OpenRouter Proxy Configuration" +model_list: + - model_name: openrouter-image-edit + litellm_params: + model: openrouter/google/gemini-2.5-flash-image + api_key: os.environ/OPENROUTER_API_KEY +``` + +2. Start the LiteLLM proxy server: +```bash showLineNumbers title="Start LiteLLM Proxy Server" +litellm --config /path/to/config.yaml +``` + +3. Make an image edit request: +```bash showLineNumbers title="OpenRouter Proxy Image Edit" +curl -X POST "http://0.0.0.0:4000/v1/images/edits" \ + -H "Authorization: Bearer " \ + -F "model=openrouter-image-edit" \ + -F "image=@original_image.png" \ + -F "prompt=Make the sky a vibrant purple sunset" \ + -F "size=1024x1024" +``` + diff --git a/docs/my-website/docs/providers/openrouter.md b/docs/my-website/docs/providers/openrouter.md index 38eb998c98b..4c79c41cfd5 100644 --- a/docs/my-website/docs/providers/openrouter.md +++ b/docs/my-website/docs/providers/openrouter.md @@ -210,3 +210,90 @@ response = image_generation( # Cost is available in the response metadata print(f"Request cost: ${response._hidden_params['additional_headers']['llm_provider-x-litellm-response-cost']}") ``` + +## Image Edit + +OpenRouter supports image editing through select models like Google Gemini image models. LiteLLM routes image edit requests to OpenRouter's chat completions endpoint with the source image sent as a base64 data URL and `modalities: ["image", "text"]`. + +### Supported Models + +| Model | Description | +|-------|-------------| +| `openrouter/google/gemini-2.5-flash-image` | Gemini 2.5 Flash with image editing | + +See all available image models on [OpenRouter's model list](https://openrouter.ai/models?modality=image). + +### Supported Parameters + +| Parameter | OpenRouter Mapping | Notes | +|-----------|--------------------|-------| +| `size` | `image_config.aspect_ratio` | `1024x1024` → `1:1`, `1536x1024` → `3:2`, `1024x1536` → `2:3`, `1792x1024` → `16:9`, `1024x1792` → `9:16` | +| `quality` | `image_config.image_size` | `low`/`standard` → `1K`, `medium` → `2K`, `high`/`hd` → `4K` | +| `n` | `n` | Number of images | + +:::note +`quality=high` (4K) is only supported by `google/gemini-3-pro-image-preview` and `google/gemini-3.1-flash-image-preview`. The `google/gemini-2.5-flash-image` model supports up to `medium` (2K). +::: + +### Usage + +```python +from litellm import image_edit +import os + +os.environ["OPENROUTER_API_KEY"] = "your-api-key" + +# Basic image edit +response = image_edit( + model="openrouter/google/gemini-2.5-flash-image", + image=open("original_image.png", "rb"), + prompt="Make the sky a vibrant purple sunset", +) + +print(response) +``` + +### Advanced Usage with Parameters + +```python +from litellm import image_edit +import os + +os.environ["OPENROUTER_API_KEY"] = "your-api-key" + +# Edit with size and quality parameters +response = image_edit( + model="openrouter/google/gemini-2.5-flash-image", + image=open("photo.png", "rb"), + prompt="Add northern lights to the sky", + size="1536x1024", # Maps to aspect_ratio 3:2 + quality="high", # Maps to image_size 4K +) + +# Access the edited image +image_data = response.data[0] +if image_data.b64_json: + import base64 + with open("edited.png", "wb") as f: + f.write(base64.b64decode(image_data.b64_json)) +``` + +### Multiple Images Edit + +```python +from litellm import image_edit +import os + +os.environ["OPENROUTER_API_KEY"] = "your-api-key" + +response = image_edit( + model="openrouter/google/gemini-2.5-flash-image", + image=[ + open("scene.png", "rb"), + open("style_reference.png", "rb"), + ], + prompt="Blend the reference style into the scene", +) + +print(response) +``` From 8665e92aa8757de19e5268d9426ca81be12a1aa3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:52:45 +0000 Subject: [PATCH 050/171] chore: regenerate poetry.lock to match pyproject.toml (#3) Co-authored-by: github-actions[bot] --- poetry.lock | 65 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3062c5fdaea..2e5af5d5fc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "a2a-sdk" @@ -7,11 +7,11 @@ description = "A2A Python SDK" optional = false python-versions = ">=3.10" groups = ["main", "proxy-dev"] -markers = "python_version >= \"3.10\"" files = [ {file = "a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385"}, {file = "a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d"}, ] +markers = {main = "python_version >= \"3.10\" and extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] google-api-core = ">=1.26.0" @@ -385,6 +385,7 @@ files = [ {file = "azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b"}, {file = "azure_core-1.36.0.tar.gz", hash = "sha256:22e5605e6d0bf1d229726af56d9e92bc37b6e726b141a18be0b4d424131741b7"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] requests = ">=2.21.0" @@ -405,6 +406,7 @@ files = [ {file = "azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651"}, {file = "azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] azure-core = ">=1.31.0" @@ -598,7 +600,7 @@ files = [ {file = "cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace"}, {file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "certifi" @@ -705,7 +707,7 @@ files = [ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" or extra == \"proxy\"", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} +markers = {main = "(platform_python_implementation != \"PyPy\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\")", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} @@ -1055,6 +1057,7 @@ files = [ {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] +markers = {main = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\") or extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} @@ -1837,11 +1840,11 @@ description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main", "proxy-dev"] -markers = "python_version >= \"3.14\"" files = [ {file = "google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7"}, {file = "google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300"}, ] +markers = {main = "python_version >= \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")", proxy-dev = "python_version >= \"3.14\""} [package.dependencies] google-auth = ">=2.14.1,<3.0.0" @@ -1869,7 +1872,7 @@ files = [ {file = "google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c"}, {file = "google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8"}, ] -markers = {main = "(python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\") and python_version < \"3.14\"", proxy-dev = "python_version >= \"3.10\" and python_version < \"3.14\""} +markers = {main = "python_version < \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")", proxy-dev = "python_version >= \"3.10\" and python_version < \"3.14\""} [package.dependencies] google-auth = ">=2.14.1,<3.0.0" @@ -1906,7 +1909,7 @@ files = [ {file = "google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16"}, {file = "google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] cachetools = ">=2.0.0,<7.0" @@ -2078,11 +2081,11 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0.dev0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0.dev0" +proto-plus = ">=1.22.3,<2.0.0.dev0" +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" [[package]] name = "google-cloud-resource-manager" @@ -2264,7 +2267,7 @@ files = [ {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"}, {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\") or extra == \"google\" or extra == \"extra-proxy\""} [package.dependencies] grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} @@ -2673,11 +2676,11 @@ description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.9" groups = ["main", "proxy-dev"] -markers = "python_version >= \"3.10\"" files = [ {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"}, {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"}, ] +markers = {main = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\")", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "huey" @@ -3042,7 +3045,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -3713,6 +3716,7 @@ files = [ {file = "msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1"}, {file = "msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] cryptography = ">=2.5,<49" @@ -3733,6 +3737,7 @@ files = [ {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] msal = ">=1.29,<2" @@ -3983,6 +3988,7 @@ files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +markers = {main = "extra == \"extra-proxy\""} [[package]] name = "numpy" @@ -4105,7 +4111,7 @@ files = [ {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, ] -markers = {main = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} [package.dependencies] importlib-metadata = ">=6.0,<8.8.0" @@ -4220,7 +4226,7 @@ files = [ {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, ] -markers = {main = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} [package.dependencies] opentelemetry-api = "1.39.1" @@ -4238,7 +4244,7 @@ files = [ {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, ] -markers = {main = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} [package.dependencies] opentelemetry-api = "1.39.1" @@ -4722,6 +4728,7 @@ files = [ {file = "prisma-0.11.0-py3-none-any.whl", hash = "sha256:22bb869e59a2968b99f3483bb417717273ffbc569fd1e9ceed95e5614cbaf53a"}, {file = "prisma-0.11.0.tar.gz", hash = "sha256:3f2f2fd2361e1ec5ff655f2a04c7860c2f2a5bc4c91f78ca9c5c6349735bf693"}, ] +markers = {main = "extra == \"extra-proxy\""} [package.dependencies] click = ">=7.1.2" @@ -4895,7 +4902,7 @@ files = [ {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] protobuf = ">=3.19.0,<7.0.0" @@ -4923,7 +4930,7 @@ files = [ {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\""} [[package]] name = "psutil" @@ -5083,7 +5090,7 @@ files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "pyasn1-modules" @@ -5096,7 +5103,7 @@ files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] pyasn1 = ">=0.6.1,<0.7.0" @@ -5124,7 +5131,7 @@ files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] -markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} +markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} [[package]] name = "pydantic" @@ -5347,6 +5354,7 @@ files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] +markers = {main = "(python_version <= \"3.13\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"extra-proxy\" or extra == \"proxy\")"} [package.dependencies] cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} @@ -6276,7 +6284,7 @@ files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] pyasn1 = ">=0.1.3" @@ -6322,10 +6330,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "scikit-learn" @@ -6478,9 +6486,9 @@ tornado = ">=6.4.2,<7" urllib3 = ">=1.26,<3" [package.extras] -all = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)", "cohere (>=5.9.4,<6.00)", "dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\"", "google-cloud-aiplatform (>=1.45.0,<2)", "ipykernel (>=6.25.0,<7)", "llama-cpp-python (>=0.2.28,<0.2.86) ; python_version < \"3.13\"", "mistralai (>=0.0.12,<0.1.0)", "mypy (>=1.7.1,<2)", "ollama (>=0.1.7)", "pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "pinecone[asyncio] (>=7.0.0,<8.0.0)", "psycopg[binary] (>=3.1.0,<4)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "qdrant-client (>=1.11.1,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "sentence-transformers (>=5.0.0) ; python_version < \"3.13\"", "tokenizers (>=0.19) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\"", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] +all = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)", "cohere (>=5.9.4,<6.0)", "dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\"", "google-cloud-aiplatform (>=1.45.0,<2)", "ipykernel (>=6.25.0,<7)", "llama-cpp-python (>=0.2.28,<0.2.86) ; python_version < \"3.13\"", "mistralai (>=0.0.12,<0.1.0)", "mypy (>=1.7.1,<2)", "ollama (>=0.1.7)", "pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "pinecone[asyncio] (>=7.0.0,<8.0.0)", "psycopg[binary] (>=3.1.0,<4)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "qdrant-client (>=1.11.1,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "sentence-transformers (>=5.0.0) ; python_version < \"3.13\"", "tokenizers (>=0.19) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\"", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] bedrock = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)"] -cohere = ["cohere (>=5.9.4,<6.00)"] +cohere = ["cohere (>=5.9.4,<6.0)"] dev = ["dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "ipykernel (>=6.25.0,<7)", "mypy (>=1.7.1,<2)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] docs = ["pydoc-markdown (>=4.8.2) ; python_version < \"3.12\""] fastembed = ["fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\""] @@ -7208,6 +7216,7 @@ files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] +markers = {main = "extra == \"extra-proxy\""} [[package]] name = "tornado" From 909e3ce6c9c0bcc3bbf0ef320766fd3e5d712726 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 14:54:11 -0300 Subject: [PATCH 051/171] test: create fresh ModelResponse per test to avoid shared mutable state --- .../test_json_schema_validation.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/litellm/litellm_core_utils/test_json_schema_validation.py b/tests/litellm/litellm_core_utils/test_json_schema_validation.py index 859a1238a61..f798db6fb43 100644 --- a/tests/litellm/litellm_core_utils/test_json_schema_validation.py +++ b/tests/litellm/litellm_core_utils/test_json_schema_validation.py @@ -46,11 +46,8 @@ STRICT_SCHEMA = { }, } -# Response that does NOT match the schema (wrong field names) -INVALID_RESPONSE = _make_response({"name": "test", "age": 25}) - -# Response that matches the schema -VALID_RESPONSE = _make_response({"title": "Inception", "rating": 9}) +INVALID_CONTENT = {"name": "test", "age": 25} # Does NOT match the schema +VALID_CONTENT = {"title": "Inception", "rating": 9} # Matches the schema @pytest.fixture(autouse=True) @@ -70,7 +67,7 @@ class TestPerRequestJsonSchemaValidation: litellm.enable_json_schema_validation = False # Should NOT raise even though response doesn't match schema post_call_processing( - INVALID_RESPONSE, + _make_response(INVALID_CONTENT), "test-model", {"response_format": STRICT_SCHEMA}, _mock_completion, @@ -82,7 +79,7 @@ class TestPerRequestJsonSchemaValidation: litellm.enable_json_schema_validation = False with pytest.raises(litellm.JSONSchemaValidationError): post_call_processing( - INVALID_RESPONSE, + _make_response(INVALID_CONTENT), "test-model", { "response_format": STRICT_SCHEMA, @@ -97,7 +94,7 @@ class TestPerRequestJsonSchemaValidation: litellm.enable_json_schema_validation = True # Should NOT raise because per-request says False post_call_processing( - INVALID_RESPONSE, + _make_response(INVALID_CONTENT), "test-model", { "response_format": STRICT_SCHEMA, @@ -112,7 +109,7 @@ class TestPerRequestJsonSchemaValidation: litellm.enable_json_schema_validation = True with pytest.raises(litellm.JSONSchemaValidationError): post_call_processing( - INVALID_RESPONSE, + _make_response(INVALID_CONTENT), "test-model", {"response_format": STRICT_SCHEMA}, _mock_completion, @@ -122,7 +119,7 @@ class TestPerRequestJsonSchemaValidation: def test_valid_response_passes_with_per_request_on(self): """Per-request ON + valid response -> no error raised.""" post_call_processing( - VALID_RESPONSE, + _make_response(VALID_CONTENT), "test-model", { "response_format": STRICT_SCHEMA, From fba19f089a1950c957f6b68b8ddf7d7a252af479 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Feb 2026 23:33:54 -0300 Subject: [PATCH 052/171] refactor: reduce code duplication in files/main.py with credential helpers Extract repeated OpenAI and Azure credential resolution logic into _get_openai_credentials() and _get_azure_credentials() helper functions, reducing ~270 lines of duplicated code across 5 file operations. Also removes dead Vertex AI code path in create_file that was unreachable since ProviderConfigManager.get_provider_files_config() handles it first. --- litellm/files/main.py | 374 ++++++++++++------------------------------ 1 file changed, 103 insertions(+), 271 deletions(-) diff --git a/litellm/files/main.py b/litellm/files/main.py index 78e41bb5a68..879e6fe69c0 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -11,7 +11,7 @@ import os import time import uuid as uuid_module from functools import partial -from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast +from typing import Any, Coroutine, Dict, Literal, NamedTuple, Optional, Union, cast import httpx @@ -57,6 +57,68 @@ anthropic_files_instance = AnthropicFilesHandler() ################################################# +class OpenAICredentials(NamedTuple): + api_base: Optional[str] + api_key: Optional[str] + organization: Optional[str] + + +class AzureCredentials(NamedTuple): + api_base: Optional[str] + api_key: Optional[str] + api_version: Optional[str] + + +def _get_openai_credentials( + optional_params: GenericLiteLLMParams, +) -> OpenAICredentials: + """Resolve OpenAI credentials from optional_params, litellm globals, and env vars.""" + api_base = ( + optional_params.api_base + or litellm.api_base + or os.getenv("OPENAI_BASE_URL") + or os.getenv("OPENAI_API_BASE") + or "https://api.openai.com/v1" + ) + organization = ( + optional_params.organization + or litellm.organization + or os.getenv("OPENAI_ORGANIZATION", None) + or None + ) + api_key = ( + optional_params.api_key + or litellm.api_key + or litellm.openai_key + or os.getenv("OPENAI_API_KEY") + ) + return OpenAICredentials(api_base=api_base, api_key=api_key, organization=organization) + + +def _get_azure_credentials( + optional_params: GenericLiteLLMParams, +) -> AzureCredentials: + """Resolve Azure credentials from optional_params, litellm globals, and env vars.""" + api_base = ( + optional_params.api_base + or litellm.api_base + or get_secret_str("AZURE_API_BASE") + ) + api_version = ( + optional_params.api_version + or litellm.api_version + or get_secret_str("AZURE_API_VERSION") + ) + api_key = ( + optional_params.api_key + or litellm.api_key + or litellm.azure_key + or get_secret_str("AZURE_OPENAI_API_KEY") + or get_secret_str("AZURE_API_KEY") + ) + return AzureCredentials(api_base=api_base, api_key=api_key, api_version=api_version) + + @client async def acreate_file( file: FileTypes, @@ -185,95 +247,28 @@ def create_file( timeout=timeout, ) elif custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there - api_base = ( - optional_params.api_base - or litellm.api_base - or os.getenv("OPENAI_BASE_URL") - or os.getenv("OPENAI_API_BASE") - or "https://api.openai.com/v1" - ) - organization = ( - optional_params.organization - or litellm.organization - or os.getenv("OPENAI_ORGANIZATION", None) - or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 - ) - # set API KEY - api_key = ( - optional_params.api_key - or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there - or litellm.openai_key - or os.getenv("OPENAI_API_KEY") - ) - + openai_creds = _get_openai_credentials(optional_params) response = openai_files_instance.create_file( _is_async=_is_async, - api_base=api_base, - api_key=api_key, + api_base=openai_creds.api_base, + api_key=openai_creds.api_key, timeout=timeout, max_retries=optional_params.max_retries, - organization=organization, + organization=openai_creds.organization, create_file_data=_create_file_request, ) elif custom_llm_provider == "azure": - api_base = optional_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") # type: ignore - api_version = ( - optional_params.api_version - or litellm.api_version - or get_secret_str("AZURE_API_VERSION") - ) # type: ignore - - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_OPENAI_API_KEY") - or get_secret_str("AZURE_API_KEY") - ) # type: ignore - - extra_body = optional_params.get("extra_body", {}) - if extra_body is not None: - extra_body.pop("azure_ad_token", None) - else: - get_secret_str("AZURE_AD_TOKEN") # type: ignore - + azure_creds = _get_azure_credentials(optional_params) response = azure_files_instance.create_file( _is_async=_is_async, - api_base=api_base, - api_key=api_key, - api_version=api_version, + api_base=azure_creds.api_base, + api_key=azure_creds.api_key, + api_version=azure_creds.api_version, timeout=timeout, max_retries=optional_params.max_retries, create_file_data=_create_file_request, litellm_params=litellm_params_dict, ) - elif custom_llm_provider == "vertex_ai": - api_base = optional_params.api_base or "" - vertex_ai_project = ( - optional_params.vertex_project - or litellm.vertex_project - or get_secret_str("VERTEXAI_PROJECT") - ) - vertex_ai_location = ( - optional_params.vertex_location - or litellm.vertex_location - or get_secret_str("VERTEXAI_LOCATION") - ) - vertex_credentials = optional_params.vertex_credentials or get_secret_str( - "VERTEXAI_CREDENTIALS" - ) - - response = vertex_ai_files_instance.create_file( - _is_async=_is_async, - api_base=api_base, - vertex_project=vertex_ai_project, - vertex_location=vertex_ai_location, - vertex_credentials=vertex_credentials, - timeout=timeout, - max_retries=optional_params.max_retries, - create_file_data=_create_file_request, - ) else: raise litellm.exceptions.BadRequestError( message="LiteLLM doesn't support {} for 'create_file'. Only ['openai', 'azure', 'vertex_ai', 'manus'] are supported.".format( @@ -367,64 +362,23 @@ def file_retrieve( _is_async = kwargs.pop("is_async", False) is True if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there - api_base = ( - optional_params.api_base - or litellm.api_base - or os.getenv("OPENAI_BASE_URL") - or os.getenv("OPENAI_API_BASE") - or "https://api.openai.com/v1" - ) - organization = ( - optional_params.organization - or litellm.organization - or os.getenv("OPENAI_ORGANIZATION", None) - or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 - ) - # set API KEY - api_key = ( - optional_params.api_key - or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there - or litellm.openai_key - or os.getenv("OPENAI_API_KEY") - ) - + openai_creds = _get_openai_credentials(optional_params) response = openai_files_instance.retrieve_file( file_id=file_id, _is_async=_is_async, - api_base=api_base, - api_key=api_key, + api_base=openai_creds.api_base, + api_key=openai_creds.api_key, timeout=timeout, max_retries=optional_params.max_retries, - organization=organization, + organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - api_base = optional_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") # type: ignore - api_version = ( - optional_params.api_version - or litellm.api_version - or get_secret_str("AZURE_API_VERSION") - ) # type: ignore - - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_OPENAI_API_KEY") - or get_secret_str("AZURE_API_KEY") - ) # type: ignore - - extra_body = optional_params.get("extra_body", {}) - if extra_body is not None: - extra_body.pop("azure_ad_token", None) - else: - get_secret_str("AZURE_AD_TOKEN") # type: ignore - + azure_creds = _get_azure_credentials(optional_params) response = azure_files_instance.retrieve_file( _is_async=_is_async, - api_base=api_base, - api_key=api_key, - api_version=api_version, + api_base=azure_creds.api_base, + api_key=azure_creds.api_key, + api_version=azure_creds.api_version, timeout=timeout, max_retries=optional_params.max_retries, file_id=file_id, @@ -576,63 +530,23 @@ def file_delete( timeout = 600.0 _is_async = kwargs.pop("is_async", False) is True if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there - api_base = ( - optional_params.api_base - or litellm.api_base - or os.getenv("OPENAI_BASE_URL") - or os.getenv("OPENAI_API_BASE") - or "https://api.openai.com/v1" - ) - organization = ( - optional_params.organization - or litellm.organization - or os.getenv("OPENAI_ORGANIZATION", None) - or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 - ) - # set API KEY - api_key = ( - optional_params.api_key - or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there - or litellm.openai_key - or os.getenv("OPENAI_API_KEY") - ) + openai_creds = _get_openai_credentials(optional_params) response = openai_files_instance.delete_file( file_id=file_id, _is_async=_is_async, - api_base=api_base, - api_key=api_key, + api_base=openai_creds.api_base, + api_key=openai_creds.api_key, timeout=timeout, max_retries=optional_params.max_retries, - organization=organization, + organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - api_base = optional_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") # type: ignore - api_version = ( - optional_params.api_version - or litellm.api_version - or get_secret_str("AZURE_API_VERSION") - ) # type: ignore - - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_OPENAI_API_KEY") - or get_secret_str("AZURE_API_KEY") - ) # type: ignore - - extra_body = optional_params.get("extra_body", {}) - if extra_body is not None: - extra_body.pop("azure_ad_token", None) - else: - get_secret_str("AZURE_AD_TOKEN") # type: ignore - + azure_creds = _get_azure_credentials(optional_params) response = azure_files_instance.delete_file( _is_async=_is_async, - api_base=api_base, - api_key=api_key, - api_version=api_version, + api_base=azure_creds.api_base, + api_key=azure_creds.api_key, + api_version=azure_creds.api_version, timeout=timeout, max_retries=optional_params.max_retries, file_id=file_id, @@ -815,64 +729,23 @@ def file_list( ) return response elif custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there - api_base = ( - optional_params.api_base - or litellm.api_base - or os.getenv("OPENAI_BASE_URL") - or os.getenv("OPENAI_API_BASE") - or "https://api.openai.com/v1" - ) - organization = ( - optional_params.organization - or litellm.organization - or os.getenv("OPENAI_ORGANIZATION", None) - or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 - ) - # set API KEY - api_key = ( - optional_params.api_key - or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there - or litellm.openai_key - or os.getenv("OPENAI_API_KEY") - ) - + openai_creds = _get_openai_credentials(optional_params) response = openai_files_instance.list_files( purpose=purpose, _is_async=_is_async, - api_base=api_base, - api_key=api_key, + api_base=openai_creds.api_base, + api_key=openai_creds.api_key, timeout=timeout, max_retries=optional_params.max_retries, - organization=organization, + organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - api_base = optional_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") # type: ignore - api_version = ( - optional_params.api_version - or litellm.api_version - or get_secret_str("AZURE_API_VERSION") - ) # type: ignore - - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_OPENAI_API_KEY") - or get_secret_str("AZURE_API_KEY") - ) # type: ignore - - extra_body = optional_params.get("extra_body", {}) - if extra_body is not None: - extra_body.pop("azure_ad_token", None) - else: - get_secret_str("AZURE_AD_TOKEN") # type: ignore - + azure_creds = _get_azure_credentials(optional_params) response = azure_files_instance.list_files( _is_async=_is_async, - api_base=api_base, - api_key=api_key, - api_version=api_version, + api_base=azure_creds.api_base, + api_key=azure_creds.api_key, + api_version=azure_creds.api_version, timeout=timeout, max_retries=optional_params.max_retries, purpose=purpose, @@ -1003,64 +876,23 @@ def file_content( return response if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there - api_base = ( - optional_params.api_base - or litellm.api_base - or os.getenv("OPENAI_BASE_URL") - or os.getenv("OPENAI_API_BASE") - or "https://api.openai.com/v1" - ) - organization = ( - optional_params.organization - or litellm.organization - or os.getenv("OPENAI_ORGANIZATION", None) - or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 - ) - # set API KEY - api_key = ( - optional_params.api_key - or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there - or litellm.openai_key - or os.getenv("OPENAI_API_KEY") - ) - + openai_creds = _get_openai_credentials(optional_params) response = openai_files_instance.file_content( _is_async=_is_async, file_content_request=_file_content_request, - api_base=api_base, - api_key=api_key, + api_base=openai_creds.api_base, + api_key=openai_creds.api_key, timeout=timeout, max_retries=optional_params.max_retries, - organization=organization, + organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - api_base = optional_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") # type: ignore - api_version = ( - optional_params.api_version - or litellm.api_version - or get_secret_str("AZURE_API_VERSION") - ) # type: ignore - - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_OPENAI_API_KEY") - or get_secret_str("AZURE_API_KEY") - ) # type: ignore - - extra_body = optional_params.get("extra_body", {}) - if extra_body is not None: - extra_body.pop("azure_ad_token", None) - else: - get_secret_str("AZURE_AD_TOKEN") # type: ignore - + azure_creds = _get_azure_credentials(optional_params) response = azure_files_instance.file_content( _is_async=_is_async, - api_base=api_base, - api_key=api_key, - api_version=api_version, + api_base=azure_creds.api_base, + api_key=azure_creds.api_key, + api_version=azure_creds.api_version, timeout=timeout, max_retries=optional_params.max_retries, file_content_request=_file_content_request, From ead74dff114454ca5eba36c33bd09e36fedc3e4c Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Feb 2026 23:41:53 -0300 Subject: [PATCH 053/171] refactor: move credential helpers to provider common_utils modules Move get_openai_credentials() to litellm/llms/openai/common_utils.py and get_azure_credentials() to litellm/llms/azure/common_utils.py so they can be reused by batches/main.py and other modules. Signatures now take individual params instead of GenericLiteLLMParams. --- litellm/files/main.py | 153 ++++++++++++++-------------- litellm/llms/azure/common_utils.py | 38 ++++++- litellm/llms/openai/common_utils.py | 39 ++++++- 3 files changed, 154 insertions(+), 76 deletions(-) diff --git a/litellm/files/main.py b/litellm/files/main.py index 879e6fe69c0..f626f9b3466 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -7,11 +7,10 @@ https://platform.openai.com/docs/api-reference/files import asyncio import contextvars -import os import time import uuid as uuid_module from functools import partial -from typing import Any, Coroutine, Dict, Literal, NamedTuple, Optional, Union, cast +from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast import httpx @@ -20,10 +19,12 @@ from litellm import get_secret_str 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.anthropic.files.handler import AnthropicFilesHandler +from litellm.llms.azure.common_utils import get_azure_credentials from litellm.llms.azure.files.handler import AzureOpenAIFilesAPI from litellm.llms.bedrock.files.handler import BedrockFilesHandler from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler +from litellm.llms.openai.common_utils import get_openai_credentials from litellm.llms.openai.openai import FileDeleted, FileObject, OpenAIFilesAPI from litellm.llms.vertex_ai.files.handler import VertexAIFilesHandler from litellm.types.llms.openai import ( @@ -57,68 +58,6 @@ anthropic_files_instance = AnthropicFilesHandler() ################################################# -class OpenAICredentials(NamedTuple): - api_base: Optional[str] - api_key: Optional[str] - organization: Optional[str] - - -class AzureCredentials(NamedTuple): - api_base: Optional[str] - api_key: Optional[str] - api_version: Optional[str] - - -def _get_openai_credentials( - optional_params: GenericLiteLLMParams, -) -> OpenAICredentials: - """Resolve OpenAI credentials from optional_params, litellm globals, and env vars.""" - api_base = ( - optional_params.api_base - or litellm.api_base - or os.getenv("OPENAI_BASE_URL") - or os.getenv("OPENAI_API_BASE") - or "https://api.openai.com/v1" - ) - organization = ( - optional_params.organization - or litellm.organization - or os.getenv("OPENAI_ORGANIZATION", None) - or None - ) - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.openai_key - or os.getenv("OPENAI_API_KEY") - ) - return OpenAICredentials(api_base=api_base, api_key=api_key, organization=organization) - - -def _get_azure_credentials( - optional_params: GenericLiteLLMParams, -) -> AzureCredentials: - """Resolve Azure credentials from optional_params, litellm globals, and env vars.""" - api_base = ( - optional_params.api_base - or litellm.api_base - or get_secret_str("AZURE_API_BASE") - ) - api_version = ( - optional_params.api_version - or litellm.api_version - or get_secret_str("AZURE_API_VERSION") - ) - api_key = ( - optional_params.api_key - or litellm.api_key - or litellm.azure_key - or get_secret_str("AZURE_OPENAI_API_KEY") - or get_secret_str("AZURE_API_KEY") - ) - return AzureCredentials(api_base=api_base, api_key=api_key, api_version=api_version) - - @client async def acreate_file( file: FileTypes, @@ -247,7 +186,11 @@ def create_file( timeout=timeout, ) elif custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - openai_creds = _get_openai_credentials(optional_params) + openai_creds = get_openai_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + organization=optional_params.organization, + ) response = openai_files_instance.create_file( _is_async=_is_async, api_base=openai_creds.api_base, @@ -258,7 +201,11 @@ def create_file( create_file_data=_create_file_request, ) elif custom_llm_provider == "azure": - azure_creds = _get_azure_credentials(optional_params) + azure_creds = get_azure_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + api_version=optional_params.api_version, + ) response = azure_files_instance.create_file( _is_async=_is_async, api_base=azure_creds.api_base, @@ -269,6 +216,32 @@ def create_file( create_file_data=_create_file_request, litellm_params=litellm_params_dict, ) + elif custom_llm_provider == "vertex_ai": + api_base = optional_params.api_base or "" + vertex_ai_project = ( + optional_params.vertex_project + or litellm.vertex_project + or get_secret_str("VERTEXAI_PROJECT") + ) + vertex_ai_location = ( + optional_params.vertex_location + or litellm.vertex_location + or get_secret_str("VERTEXAI_LOCATION") + ) + vertex_credentials = optional_params.vertex_credentials or get_secret_str( + "VERTEXAI_CREDENTIALS" + ) + + response = vertex_ai_files_instance.create_file( + _is_async=_is_async, + api_base=api_base, + vertex_project=vertex_ai_project, + vertex_location=vertex_ai_location, + vertex_credentials=vertex_credentials, + timeout=timeout, + max_retries=optional_params.max_retries, + create_file_data=_create_file_request, + ) else: raise litellm.exceptions.BadRequestError( message="LiteLLM doesn't support {} for 'create_file'. Only ['openai', 'azure', 'vertex_ai', 'manus'] are supported.".format( @@ -362,7 +335,11 @@ def file_retrieve( _is_async = kwargs.pop("is_async", False) is True if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - openai_creds = _get_openai_credentials(optional_params) + openai_creds = get_openai_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + organization=optional_params.organization, + ) response = openai_files_instance.retrieve_file( file_id=file_id, _is_async=_is_async, @@ -373,7 +350,11 @@ def file_retrieve( organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - azure_creds = _get_azure_credentials(optional_params) + azure_creds = get_azure_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + api_version=optional_params.api_version, + ) response = azure_files_instance.retrieve_file( _is_async=_is_async, api_base=azure_creds.api_base, @@ -530,7 +511,11 @@ def file_delete( timeout = 600.0 _is_async = kwargs.pop("is_async", False) is True if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - openai_creds = _get_openai_credentials(optional_params) + openai_creds = get_openai_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + organization=optional_params.organization, + ) response = openai_files_instance.delete_file( file_id=file_id, _is_async=_is_async, @@ -541,7 +526,11 @@ def file_delete( organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - azure_creds = _get_azure_credentials(optional_params) + azure_creds = get_azure_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + api_version=optional_params.api_version, + ) response = azure_files_instance.delete_file( _is_async=_is_async, api_base=azure_creds.api_base, @@ -729,7 +718,11 @@ def file_list( ) return response elif custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - openai_creds = _get_openai_credentials(optional_params) + openai_creds = get_openai_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + organization=optional_params.organization, + ) response = openai_files_instance.list_files( purpose=purpose, _is_async=_is_async, @@ -740,7 +733,11 @@ def file_list( organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - azure_creds = _get_azure_credentials(optional_params) + azure_creds = get_azure_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + api_version=optional_params.api_version, + ) response = azure_files_instance.list_files( _is_async=_is_async, api_base=azure_creds.api_base, @@ -876,7 +873,11 @@ def file_content( return response if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: - openai_creds = _get_openai_credentials(optional_params) + openai_creds = get_openai_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + organization=optional_params.organization, + ) response = openai_files_instance.file_content( _is_async=_is_async, file_content_request=_file_content_request, @@ -887,7 +888,11 @@ def file_content( organization=openai_creds.organization, ) elif custom_llm_provider == "azure": - azure_creds = _get_azure_credentials(optional_params) + azure_creds = get_azure_credentials( + api_base=optional_params.api_base, + api_key=optional_params.api_key, + api_version=optional_params.api_version, + ) response = azure_files_instance.file_content( _is_async=_is_async, api_base=azure_creds.api_base, diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 25b218fca8c..7ed4306e299 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -1,6 +1,6 @@ import json import os -from typing import Any, Callable, Dict, Literal, Optional, Union, cast +from typing import Any, Callable, Dict, Literal, NamedTuple, Optional, Union, cast import httpx from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI @@ -789,3 +789,39 @@ class BaseAzureLLM(BaseOpenAILLM): return param_value return os.getenv(env_var_key) + +class AzureCredentials(NamedTuple): + api_base: Optional[str] + api_key: Optional[str] + api_version: Optional[str] + + +def get_azure_credentials( + api_base: Optional[str] = None, + api_key: Optional[str] = None, + api_version: Optional[str] = None, +) -> AzureCredentials: + """Resolve Azure credentials from params, litellm globals, and env vars.""" + resolved_api_base = ( + api_base + or litellm.api_base + or get_secret_str("AZURE_API_BASE") + ) + resolved_api_version = ( + api_version + or litellm.api_version + or get_secret_str("AZURE_API_VERSION") + ) + resolved_api_key = ( + api_key + or litellm.api_key + or litellm.azure_key + or get_secret_str("AZURE_OPENAI_API_KEY") + or get_secret_str("AZURE_API_KEY") + ) + return AzureCredentials( + api_base=resolved_api_base, + api_key=resolved_api_key, + api_version=resolved_api_version, + ) + diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index 61f150f1c2e..069448eba07 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -5,8 +5,9 @@ Common helpers / utils across al OpenAI endpoints import hashlib import inspect import json +import os import ssl -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, NamedTuple, Optional, Tuple, Union import httpx import openai @@ -244,3 +245,39 @@ class BaseOpenAILLM: ) +class OpenAICredentials(NamedTuple): + api_base: Optional[str] + api_key: Optional[str] + organization: Optional[str] + + +def get_openai_credentials( + api_base: Optional[str] = None, + api_key: Optional[str] = None, + organization: Optional[str] = None, +) -> OpenAICredentials: + """Resolve OpenAI credentials from params, litellm globals, and env vars.""" + resolved_api_base = ( + api_base + or litellm.api_base + or os.getenv("OPENAI_BASE_URL") + or os.getenv("OPENAI_API_BASE") + or "https://api.openai.com/v1" + ) + resolved_organization = ( + organization + or litellm.organization + or os.getenv("OPENAI_ORGANIZATION", None) + or None + ) + resolved_api_key = ( + api_key + or litellm.api_key + or litellm.openai_key + or os.getenv("OPENAI_API_KEY") + ) + return OpenAICredentials( + api_base=resolved_api_base, + api_key=resolved_api_key, + organization=resolved_organization, + ) From 5005773909e65a4ae1c87abcf86928c7052450e2 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 1 Dec 2025 11:32:55 -0300 Subject: [PATCH 054/171] fix(deps): relax python-multipart version constraint to >=0.0.22 The caret operator (^0.0.x) in zerover projects restricts to a single patch version. Changed to >= to allow future patch updates. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 577e51a0d22..395bbfccbd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ orjson = {version = "^3.9.7", optional = true} apscheduler = {version = "^3.10.4", optional = true} fastapi-sso = { version = "^0.16.0", optional = true } PyJWT = { version = "^2.10.1", optional = true, python = ">=3.9" } -python-multipart = { version = "^0.0.22", optional = true, python = ">=3.10"} +python-multipart = { version = ">=0.0.20", optional = true} cryptography = {version = "*", optional = true} prisma = {version = "0.11.0", optional = true} azure-identity = {version = "^1.15.0", optional = true, python = ">=3.9"} From dad7805b42beac2e562253a6edd8df46cfb99436 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 2 Mar 2026 19:39:13 -0300 Subject: [PATCH 055/171] fix(deps): update python-multipart version to 0.0.22 in all files Align requirements.txt, CI workflow, liccheck, and license cache with the >=0.0.22 constraint already set in pyproject.toml. --- .github/workflows/test-litellm.yml | 2 +- requirements.txt | 3 ++- tests/code_coverage_tests/liccheck.ini | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-litellm.yml b/.github/workflows/test-litellm.yml index cf6928897be..3f8369df926 100644 --- a/.github/workflows/test-litellm.yml +++ b/.github/workflows/test-litellm.yml @@ -38,7 +38,7 @@ jobs: poetry run pip install "google-genai==1.22.0" poetry run pip install "google-cloud-aiplatform>=1.38" poetry run pip install "fastapi-offline==1.7.3" - poetry run pip install "python-multipart==0.0.22" + poetry run pip install "python-multipart>=0.0.20" poetry run pip install "openapi-core" - name: Setup litellm-enterprise as local package run: | diff --git a/requirements.txt b/requirements.txt index 69aac377d8a..3655c9b0270 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,8 +41,9 @@ polars==1.31.0 # for data processing apscheduler==3.10.4 # for resetting budget in background fastapi-sso==0.19.0 # admin UI, SSO pyjwt[crypto]==2.10.1 ; python_version >= "3.9" -python-multipart==0.0.22 # admin UI +python-multipart>=0.0.20 # admin UI jaraco.context>=6.1.0 +Pillow==11.0.0 azure-ai-contentsafety==1.0.0 # for azure content safety azure-identity==1.16.1 ; python_version >= "3.9" # for azure content safety azure-keyvault==4.2.0 # for azure KMS integration diff --git a/tests/code_coverage_tests/liccheck.ini b/tests/code_coverage_tests/liccheck.ini index 376d2859ffa..edb53ef15ad 100644 --- a/tests/code_coverage_tests/liccheck.ini +++ b/tests/code_coverage_tests/liccheck.ini @@ -114,7 +114,7 @@ apscheduler: >=3.10.4 # Unknown license fastapi-sso: >=0.16.0 # Unknown license filelock: >=3.20.0 # Unlicense (public domain) - https://unlicense.org / https://github.com/tox-dev/filelock pyjwt: >=2.9.0 # Unknown license -python-multipart: >=0.0.18 # Unknown license +python-multipart: >=0.0.22 # Unknown license pillow: >=11.0.0 # Unknown license azure-ai-contentsafety: >=1.0.0 # Unknown license azure-identity: >=1.16.1 # Unknown license From dc9f5a5cc41fd8b2c95d37e6ca1d501453ec820e Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 2 Mar 2026 20:25:22 -0300 Subject: [PATCH 056/171] fix(deps): update python-multipart to >=0.0.20 in CI and test configs --- poetry.lock | 17 +++++++++++++++-- tests/code_coverage_tests/liccheck.ini | 2 +- tests/code_coverage_tests/license_cache.json | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3062c5fdaea..da511e2ebe5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "a2a-sdk" @@ -5599,6 +5599,19 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.9\" and extra == \"proxy\"" +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + [[package]] name = "python-multipart" version = "0.0.22" @@ -7980,4 +7993,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "5ae4b43dfe73be01d71f757227eb22245d18c06b5b4d5989b014500f400f1ee9" +content-hash = "70ec9abe5b06e7e81a2d76305cb950eea79692ae40321bac3285dc63fcbcf059" diff --git a/tests/code_coverage_tests/liccheck.ini b/tests/code_coverage_tests/liccheck.ini index edb53ef15ad..65ac01123d1 100644 --- a/tests/code_coverage_tests/liccheck.ini +++ b/tests/code_coverage_tests/liccheck.ini @@ -114,7 +114,7 @@ apscheduler: >=3.10.4 # Unknown license fastapi-sso: >=0.16.0 # Unknown license filelock: >=3.20.0 # Unlicense (public domain) - https://unlicense.org / https://github.com/tox-dev/filelock pyjwt: >=2.9.0 # Unknown license -python-multipart: >=0.0.22 # Unknown license +python-multipart: >=0.0.20 # Unknown license pillow: >=11.0.0 # Unknown license azure-ai-contentsafety: >=1.0.0 # Unknown license azure-identity: >=1.16.1 # Unknown license diff --git a/tests/code_coverage_tests/license_cache.json b/tests/code_coverage_tests/license_cache.json index a9c7fad2b14..e7b1157a240 100644 --- a/tests/code_coverage_tests/license_cache.json +++ b/tests/code_coverage_tests/license_cache.json @@ -20,7 +20,7 @@ "apscheduler:3.10.4": "MIT", "fastapi-sso:0.16.0": "MIT", "pyjwt:2.9.0": "MIT", - "python-multipart:0.0.22": "Apache-2.0", + "python-multipart:0.0.20": "Apache-2.0", "Pillow:11.0.0": "MIT-CMU", "azure-ai-contentsafety:1.0.0": "MIT License", "azure-identity:1.16.1": "MIT License", From 7d65df351fd852f7b544e126c763825296c8d7a5 Mon Sep 17 00:00:00 2001 From: Varad Khonde Date: Tue, 3 Mar 2026 23:40:41 +0530 Subject: [PATCH 057/171] feat(togetherai): add support for TogetherAI Qwen3.5-397B-A17B model --- litellm/model_prices_and_context_window_backup.json | 12 ++++++++++++ model_prices_and_context_window.json | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 5de764c5cec..3dfbc5cb219 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -29736,6 +29736,18 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "together_ai/Qwen/Qwen3.5-397B-A17B": { + "input_cost_per_token": 6e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3.6e-06, + "source": "https://www.together.ai/models/Qwen/Qwen3.5-397B-A17B", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "tts-1": { "input_cost_per_character": 1.5e-05, "litellm_provider": "openai", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 412f99791a0..3a2d6d1d1e2 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -29958,6 +29958,18 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "together_ai/Qwen/Qwen3.5-397B-A17B": { + "input_cost_per_token": 6e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3.6e-06, + "source": "https://www.together.ai/models/Qwen/Qwen3.5-397B-A17B", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "tts-1": { "input_cost_per_character": 1.5e-05, "litellm_provider": "openai", From 47f0390b9b1320a586c9538936ae3a3b072af3b9 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 15:14:20 -0300 Subject: [PATCH 058/171] fix: remove duplicate Pillow==11.0.0 pin (12.1.1 already on line 8) --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3655c9b0270..aef0e1d271e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,6 @@ fastapi-sso==0.19.0 # admin UI, SSO pyjwt[crypto]==2.10.1 ; python_version >= "3.9" python-multipart>=0.0.20 # admin UI jaraco.context>=6.1.0 -Pillow==11.0.0 azure-ai-contentsafety==1.0.0 # for azure content safety azure-identity==1.16.1 ; python_version >= "3.9" # for azure content safety azure-keyvault==4.2.0 # for azure KMS integration From fecb3016847d1c8faea966c21581242748801118 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:16:12 -0300 Subject: [PATCH 059/171] Update litellm/llms/openrouter/image_edit/transformation.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/llms/openrouter/image_edit/transformation.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/litellm/llms/openrouter/image_edit/transformation.py b/litellm/llms/openrouter/image_edit/transformation.py index d06536367eb..19931f00245 100644 --- a/litellm/llms/openrouter/image_edit/transformation.py +++ b/litellm/llms/openrouter/image_edit/transformation.py @@ -133,12 +133,11 @@ class OpenRouterImageEditConfig(BaseImageEditConfig): api_base: Optional[str], litellm_params: dict, ) -> str: - if api_base: - if not api_base.endswith("/chat/completions"): - api_base = api_base.rstrip("/") - return f"{api_base}/chat/completions" - return api_base - return "https://openrouter.ai/api/v1/chat/completions" + base_url = api_base or get_secret_str("OPENROUTER_API_BASE") or "https://openrouter.ai/api/v1" + base_url = base_url.rstrip("/") + if not base_url.endswith("/chat/completions"): + return f"{base_url}/chat/completions" + return base_url def transform_image_edit_request( self, From 4a88d854462be6cc1b8d7316a9df26ac1e65a487 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 15:20:51 -0300 Subject: [PATCH 060/171] test: add provider_url routing test for vertex_ai/gemini models Verifies that vertex_ai gemini models route to aiplatform.googleapis.com instead of generativelanguage.googleapis.com, preventing regressions if the branch ordering changes. --- .../helicone/test_helicone_gemini.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/litellm/integrations/helicone/test_helicone_gemini.py b/tests/litellm/integrations/helicone/test_helicone_gemini.py index f42a7016131..67c4515c1e7 100644 --- a/tests/litellm/integrations/helicone/test_helicone_gemini.py +++ b/tests/litellm/integrations/helicone/test_helicone_gemini.py @@ -62,3 +62,74 @@ def test_helicone_vertex_ai_via_custom_llm_provider(): for model, custom_llm_provider in test_cases: is_vertex_ai = custom_llm_provider == "vertex_ai" or model.startswith("vertex_ai/") assert is_vertex_ai, f"{model} with custom_llm_provider={custom_llm_provider} should be recognized as vertex_ai" + + +def test_helicone_vertex_gemini_gets_vertex_provider_url(): + """ + Test that vertex_ai/gemini-* models route to aiplatform.googleapis.com, + not generativelanguage.googleapis.com. + + This verifies the branch ordering fix: is_vertex_ai must be checked + before "gemini" in model, otherwise vertex gemini models get the wrong + provider_url. + """ + from unittest.mock import MagicMock, patch + + from litellm.integrations.helicone import HeliconeLogger + + logger = HeliconeLogger() + + captured = {} + + def mock_post(url, **kwargs): + captured["url"] = url + captured["data"] = kwargs.get("json", {}) + mock_resp = MagicMock() + mock_resp.status_code = 200 + return mock_resp + + test_cases = [ + # (model, custom_llm_provider, expected_provider_url) + ( + "vertex_ai/gemini-1.5-pro", + "", + "https://aiplatform.googleapis.com/v1", + ), + ( + "gemini-2.0-flash", + "vertex_ai", + "https://aiplatform.googleapis.com/v1", + ), + ( + "gemini-1.5-flash", + "", + "https://generativelanguage.googleapis.com/v1beta", + ), + ] + + for model, custom_llm_provider, expected_url in test_cases: + captured.clear() + mock_client = MagicMock() + mock_client.post = mock_post + with patch("litellm.module_level_client", mock_client): + logger.log_success( + model=model, + messages=[{"role": "user", "content": "test"}], + response_obj={"choices": [{"message": {"content": "hi"}}]}, + start_time=MagicMock(), + end_time=MagicMock(), + print_verbose=lambda *args, **kwargs: None, + kwargs={ + "litellm_params": { + "custom_llm_provider": custom_llm_provider, + "metadata": {}, + }, + }, + ) + + assert "data" in captured, f"No request captured for {model}" + actual_url = captured["data"]["providerRequest"]["url"] + assert actual_url == expected_url, ( + f"Model {model} (provider={custom_llm_provider!r}): " + f"expected provider_url={expected_url}, got {actual_url}" + ) From a6e18ae31b53014534b7af51bc7b7ef17f870b46 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 15:36:45 -0300 Subject: [PATCH 061/171] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20type=20OpenAICredentials.api=5Fbase=20as=20str,=20r?= =?UTF-8?q?emove=20dead=20vertex=5Fai=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OpenAICredentials.api_base is always non-None due to fallback default, so type it as str instead of Optional[str] - Remove unreachable elif vertex_ai block in create_file(); vertex_ai is already handled by ProviderConfigManager via provider_config path --- litellm/files/main.py | 26 -------------------------- litellm/llms/openai/common_utils.py | 2 +- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/litellm/files/main.py b/litellm/files/main.py index f626f9b3466..1f4d15abb81 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -216,32 +216,6 @@ def create_file( create_file_data=_create_file_request, litellm_params=litellm_params_dict, ) - elif custom_llm_provider == "vertex_ai": - api_base = optional_params.api_base or "" - vertex_ai_project = ( - optional_params.vertex_project - or litellm.vertex_project - or get_secret_str("VERTEXAI_PROJECT") - ) - vertex_ai_location = ( - optional_params.vertex_location - or litellm.vertex_location - or get_secret_str("VERTEXAI_LOCATION") - ) - vertex_credentials = optional_params.vertex_credentials or get_secret_str( - "VERTEXAI_CREDENTIALS" - ) - - response = vertex_ai_files_instance.create_file( - _is_async=_is_async, - api_base=api_base, - vertex_project=vertex_ai_project, - vertex_location=vertex_ai_location, - vertex_credentials=vertex_credentials, - timeout=timeout, - max_retries=optional_params.max_retries, - create_file_data=_create_file_request, - ) else: raise litellm.exceptions.BadRequestError( message="LiteLLM doesn't support {} for 'create_file'. Only ['openai', 'azure', 'vertex_ai', 'manus'] are supported.".format( diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index 069448eba07..b6b302782e8 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -246,7 +246,7 @@ class BaseOpenAILLM: class OpenAICredentials(NamedTuple): - api_base: Optional[str] + api_base: str api_key: Optional[str] organization: Optional[str] From 6f74d3881a4b6f459dd2f9e90c8a2f3a132b0602 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 15:42:45 -0300 Subject: [PATCH 062/171] fix(lint): remove unused imports in batch_utils.py time, httpx, uuid, and ModelResponse are no longer used after the vertex_ai batch cost calculation was rewritten upstream. --- litellm/batches/batch_utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/litellm/batches/batch_utils.py b/litellm/batches/batch_utils.py index 80351664dfe..a55e30ebeb9 100644 --- a/litellm/batches/batch_utils.py +++ b/litellm/batches/batch_utils.py @@ -1,14 +1,10 @@ import json -import time from typing import Any, List, Literal, Optional, Tuple -import httpx - import litellm from litellm._logging import verbose_logger -from litellm._uuid import uuid from litellm.types.llms.openai import Batch -from litellm.types.utils import CallTypes, ModelInfo, ModelResponse, Usage +from litellm.types.utils import CallTypes, ModelInfo, Usage from litellm.utils import token_counter From 43cec8c980abf699b3664a239393f408b9ca2f90 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 10:46:45 -0800 Subject: [PATCH 063/171] feat(batches): support output_expires_after passthrough --- litellm/batches/main.py | 5 ++ litellm/types/llms/openai.py | 1 + tests/test_litellm/proxy/test_batch_expiry.py | 74 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 tests/test_litellm/proxy/test_batch_expiry.py diff --git a/litellm/batches/main.py b/litellm/batches/main.py index 9553d2c5246..e73f73d2f33 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -112,6 +112,7 @@ async def acreate_batch( metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, + output_expires_after: Optional[Dict[str, Any]] = None, **kwargs, ) -> LiteLLMBatch: """ @@ -133,6 +134,7 @@ async def acreate_batch( metadata, extra_headers, extra_body, + output_expires_after, **kwargs, ) @@ -160,6 +162,7 @@ def create_batch( metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, + output_expires_after: Optional[Dict[str, Any]] = None, **kwargs, ) -> Union[LiteLLMBatch, Coroutine[Any, Any, LiteLLMBatch]]: """ @@ -215,6 +218,8 @@ def create_batch( extra_headers=extra_headers, extra_body=extra_body, ) + if output_expires_after is not None: + _create_batch_request["output_expires_after"] = output_expires_after if model is not None: provider_config = ProviderConfigManager.get_provider_batches_config( model=model, diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index d06d879dad1..c5d610e639b 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -424,6 +424,7 @@ class CreateBatchRequest(TypedDict, total=False): endpoint: Literal["/v1/chat/completions", "/v1/embeddings", "/v1/completions"] input_file_id: str metadata: Optional[Dict[str, str]] + output_expires_after: Optional[FileExpiresAfter] extra_headers: Optional[Dict[str, str]] extra_body: Optional[Dict[str, str]] timeout: Optional[float] diff --git a/tests/test_litellm/proxy/test_batch_expiry.py b/tests/test_litellm/proxy/test_batch_expiry.py new file mode 100644 index 00000000000..25b1631792e --- /dev/null +++ b/tests/test_litellm/proxy/test_batch_expiry.py @@ -0,0 +1,74 @@ +""" +Tests for batch output_expires_after passthrough and team-level expiry enforcement. +""" + +import os +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.types.llms.openai import CreateBatchRequest + + +class TestCreateBatchOutputExpiresAfterPassthrough: + """Verify output_expires_after flows through create_batch to the provider.""" + + def test_output_expires_after_included_in_request(self): + """When output_expires_after is provided, it reaches the openai batches instance.""" + captured = {} + + original_create = None + + def capturing_create(**kwargs): + captured.update(kwargs) + mock_response = MagicMock() + mock_response.id = "batch_123" + return mock_response + + with patch( + "litellm.batches.main.openai_batches_instance" + ) as mock_instance: + mock_instance.create_batch.side_effect = capturing_create + litellm.create_batch( + completion_window="24h", + endpoint="/v1/chat/completions", + input_file_id="file-abc123", + output_expires_after={"anchor": "created_at", "seconds": 86400}, + custom_llm_provider="openai", + ) + + create_batch_data = captured["create_batch_data"] + assert create_batch_data["output_expires_after"] == { + "anchor": "created_at", + "seconds": 86400, + } + + def test_output_expires_after_absent_when_not_provided(self): + """Backward compat: output_expires_after not in request when omitted.""" + captured = {} + + def capturing_create(**kwargs): + captured.update(kwargs) + mock_response = MagicMock() + mock_response.id = "batch_123" + return mock_response + + with patch( + "litellm.batches.main.openai_batches_instance" + ) as mock_instance: + mock_instance.create_batch.side_effect = capturing_create + litellm.create_batch( + completion_window="24h", + endpoint="/v1/chat/completions", + input_file_id="file-abc123", + custom_llm_provider="openai", + ) + + create_batch_data = captured["create_batch_data"] + assert "output_expires_after" not in create_batch_data From 4b1389501d3f1418077a63b1fac2151a37c8df89 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 3 Mar 2026 15:58:23 -0300 Subject: [PATCH 064/171] fix: move _set_usage_and_cost outside try/except in OpenRouter image edit Prevents discarding already-parsed images when usage calculation fails. --- litellm/llms/openrouter/image_edit/transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/openrouter/image_edit/transformation.py b/litellm/llms/openrouter/image_edit/transformation.py index 19931f00245..ed5e6ae67d5 100644 --- a/litellm/llms/openrouter/image_edit/transformation.py +++ b/litellm/llms/openrouter/image_edit/transformation.py @@ -242,9 +242,6 @@ class OpenRouterImageEditConfig(BaseImageEditConfig): ) ) - self._set_usage_and_cost(model_response, response_json, model) - return model_response - except Exception as e: raise OpenRouterException( message=f"Error transforming OpenRouter image edit response: {str(e)}", @@ -252,6 +249,9 @@ class OpenRouterImageEditConfig(BaseImageEditConfig): headers={}, ) + self._set_usage_and_cost(model_response, response_json, model) + return model_response + def get_error_class( self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] ) -> BaseLLMException: From 3d15bcdb115731cd0b4dd93f19be880e2128f1bb Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 10:58:31 -0800 Subject: [PATCH 065/171] feat(proxy): add team-level batch output expiry enforcement --- litellm/proxy/_types.py | 6 + litellm/proxy/batches_endpoints/endpoints.py | 9 + tests/test_litellm/proxy/test_batch_expiry.py | 188 +++++++++++++----- 3 files changed, 153 insertions(+), 50 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index cbf683d226e..5122c64ea64 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1551,6 +1551,8 @@ class NewTeamRequest(TeamBase): ] = None # allow user to set TPM limit for all team members team_member_key_duration: Optional[str] = None # e.g. "1d", "1w", "1m" allowed_vector_store_indexes: Optional[List[AllowedVectorStoreIndexItem]] = None + enforced_batch_output_expires_after: Optional[dict] = None + enforced_file_expires_after: Optional[dict] = None model_config = ConfigDict(protected_namespaces=()) @@ -1606,6 +1608,8 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase): model_rpm_limit: Optional[Dict[str, int]] = None model_tpm_limit: Optional[Dict[str, int]] = None allowed_vector_store_indexes: Optional[List[AllowedVectorStoreIndexItem]] = None + enforced_batch_output_expires_after: Optional[dict] = None + enforced_file_expires_after: Optional[dict] = None router_settings: Optional[dict] = None access_group_ids: Optional[List[str]] = None @@ -3783,6 +3787,8 @@ LiteLLM_ManagementEndpoint_MetadataFields = [ "temp_budget_increase", "temp_budget_expiry", "allowed_vector_store_indexes", + "enforced_batch_output_expires_after", + "enforced_file_expires_after", ] LiteLLM_ManagementEndpoint_MetadataFields_Premium = [ diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 1c9ba6cb248..60905243369 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -118,6 +118,15 @@ async def create_batch( # noqa: PLR0915 or "openai" ) _create_batch_data = LiteLLMBatchCreateRequest(**data) + + # Apply team-level batch output expiry enforcement + team_metadata = user_api_key_dict.team_metadata or {} + enforced_batch_expiry = team_metadata.get( + "enforced_batch_output_expires_after" + ) + if enforced_batch_expiry is not None: + _create_batch_data["output_expires_after"] = enforced_batch_expiry + input_file_id = _create_batch_data.get("input_file_id", None) unified_file_id: Union[str, Literal[False]] = False diff --git a/tests/test_litellm/proxy/test_batch_expiry.py b/tests/test_litellm/proxy/test_batch_expiry.py index 25b1631792e..1f54f190c63 100644 --- a/tests/test_litellm/proxy/test_batch_expiry.py +++ b/tests/test_litellm/proxy/test_batch_expiry.py @@ -4,7 +4,7 @@ Tests for batch output_expires_after passthrough and team-level expiry enforceme import os import sys -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest @@ -13,62 +13,150 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm -from litellm.types.llms.openai import CreateBatchRequest +from litellm.caching.caching import DualCache +from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.proxy_server import app +from litellm.proxy.utils import ProxyLogging +from litellm.router import Router +from litellm.types.utils import LiteLLMBatch + +from fastapi.testclient import TestClient + +client = TestClient(app) + +TEAM_EXPIRY = {"anchor": "created_at", "seconds": 3600} +CALLER_EXPIRY = {"anchor": "created_at", "seconds": 86400} -class TestCreateBatchOutputExpiresAfterPassthrough: - """Verify output_expires_after flows through create_batch to the provider.""" +@pytest.fixture +def llm_router() -> Router: + return Router( + model_list=[ + { + "model_name": "gpt-3.5-turbo", + "litellm_params": { + "model": "openai/gpt-3.5-turbo", + "api_key": "test-key", + }, + "model_info": {"id": "gpt-3.5-turbo-id"}, + }, + ] + ) - def test_output_expires_after_included_in_request(self): - """When output_expires_after is provided, it reaches the openai batches instance.""" - captured = {} - original_create = None +def _setup_proxy(monkeypatch, llm_router: Router): + proxy_logging_obj = ProxyLogging( + user_api_key_cache=DualCache(default_in_memory_ttl=1) + ) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router) + monkeypatch.setattr( + "litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj + ) - def capturing_create(**kwargs): - captured.update(kwargs) - mock_response = MagicMock() - mock_response.id = "batch_123" - return mock_response - with patch( - "litellm.batches.main.openai_batches_instance" - ) as mock_instance: - mock_instance.create_batch.side_effect = capturing_create - litellm.create_batch( - completion_window="24h", - endpoint="/v1/chat/completions", - input_file_id="file-abc123", - output_expires_after={"anchor": "created_at", "seconds": 86400}, - custom_llm_provider="openai", +def _make_batch_response() -> LiteLLMBatch: + return LiteLLMBatch( + id="batch_abc123", + completion_window="24h", + created_at=1234567890, + endpoint="/v1/chat/completions", + input_file_id="file-abc123", + object="batch", + status="validating", + ) + + +def test_output_expires_after_passthrough(): + """output_expires_after flows through create_batch to the provider.""" + captured = {} + + def capturing_create(**kwargs): + captured.update(kwargs) + mock_response = MagicMock() + mock_response.id = "batch_123" + return mock_response + + with patch("litellm.batches.main.openai_batches_instance") as mock_instance: + mock_instance.create_batch.side_effect = capturing_create + litellm.create_batch( + completion_window="24h", + endpoint="/v1/chat/completions", + input_file_id="file-abc123", + output_expires_after=CALLER_EXPIRY, + custom_llm_provider="openai", + ) + + assert captured["create_batch_data"]["output_expires_after"] == CALLER_EXPIRY + + +class TestBatchEndpointTeamOverride: + """Verify team-level enforced_batch_output_expires_after in the proxy endpoint.""" + + def _post_batch( + self, + monkeypatch, + llm_router: Router, + team_metadata: dict, + request_body: dict, + ) -> dict: + """POST /v1/batches with given team_metadata and body, return captured kwargs.""" + _setup_proxy(monkeypatch, llm_router) + + user_key = UserAPIKeyAuth( + api_key="test-key", + team_metadata=team_metadata, + ) + app.dependency_overrides[user_api_key_auth] = lambda: user_key + + captured_kwargs = {} + + async def mock_acreate_batch(**kwargs): + captured_kwargs.update(kwargs) + return _make_batch_response() + + monkeypatch.setattr(litellm, "acreate_batch", mock_acreate_batch) + + try: + response = client.post( + "/v1/batches", + json=request_body, + headers={"Authorization": "Bearer test-key"}, ) + assert response.status_code == 200 + finally: + app.dependency_overrides.clear() - create_batch_data = captured["create_batch_data"] - assert create_batch_data["output_expires_after"] == { - "anchor": "created_at", - "seconds": 86400, - } + return captured_kwargs - def test_output_expires_after_absent_when_not_provided(self): - """Backward compat: output_expires_after not in request when omitted.""" - captured = {} + def test_team_override_overrides_caller(self, monkeypatch, llm_router): + """Team enforcement wins over caller-provided value.""" + kwargs = self._post_batch( + monkeypatch, + llm_router, + team_metadata={ + "enforced_batch_output_expires_after": TEAM_EXPIRY, + }, + request_body={ + "input_file_id": "file-abc123", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "output_expires_after": CALLER_EXPIRY, + }, + ) + assert kwargs["output_expires_after"] == TEAM_EXPIRY - def capturing_create(**kwargs): - captured.update(kwargs) - mock_response = MagicMock() - mock_response.id = "batch_123" - return mock_response - - with patch( - "litellm.batches.main.openai_batches_instance" - ) as mock_instance: - mock_instance.create_batch.side_effect = capturing_create - litellm.create_batch( - completion_window="24h", - endpoint="/v1/chat/completions", - input_file_id="file-abc123", - custom_llm_provider="openai", - ) - - create_batch_data = captured["create_batch_data"] - assert "output_expires_after" not in create_batch_data + def test_no_team_setting_preserves_caller(self, monkeypatch, llm_router): + """No team setting = caller value passes through.""" + kwargs = self._post_batch( + monkeypatch, + llm_router, + team_metadata={}, + request_body={ + "input_file_id": "file-abc123", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "output_expires_after": CALLER_EXPIRY, + }, + ) + assert kwargs["output_expires_after"] == CALLER_EXPIRY From 08613b24cbbf0315249a305b743e43cc5e73411f Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 11:03:14 -0800 Subject: [PATCH 066/171] feat(proxy): add team-level file expiry enforcement --- .../openai_files_endpoints/files_endpoints.py | 11 +- .../test_files_endpoint.py | 135 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index ec6e9733344..a641fb1c69f 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -454,8 +454,17 @@ async def create_file( # noqa: PLR0915 model=router_model, llm_router=llm_router ) + # Apply team-level file expiry enforcement + team_metadata = user_api_key_dict.team_metadata or {} + enforced_file_expiry = team_metadata.get("enforced_file_expires_after") + if enforced_file_expiry is not None: + expires_after = FileExpiresAfter( + anchor=enforced_file_expiry["anchor"], + seconds=enforced_file_expiry["seconds"], + ) + _create_file_request = CreateFileRequest( - file=file_data, + file=file_data, purpose=cast(CREATE_FILE_REQUESTS_PURPOSE, purpose), expires_after=expires_after, **data diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py index fb063ef8ee7..0239c39e67f 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py @@ -1168,3 +1168,138 @@ def test_create_file_with_deep_nested_litellm_metadata( assert captured_litellm_metadata["config"]["database"]["port"] == "5432" assert "cache" in captured_litellm_metadata["config"] assert captured_litellm_metadata["config"]["cache"]["enabled"] == "true" + + +# --------------------------------------------------------------------------- +# Team-level enforced_file_expires_after tests +# --------------------------------------------------------------------------- + + +def _make_capturing_managed_files(): + """Create a DummyManagedFiles that captures the expires_after from the request.""" + from litellm.llms.base_llm.files.transformation import BaseFileEndpoints + + captured = {} + + class CapturingManagedFiles(BaseFileEndpoints): + async def acreate_file( + self, + llm_router, + create_file_request, + target_model_names_list, + litellm_parent_otel_span, + user_api_key_dict, + ): + if isinstance(create_file_request, dict): + captured["expires_after"] = create_file_request.get("expires_after") + else: + captured["expires_after"] = getattr( + create_file_request, "expires_after", None + ) + return OpenAIFileObject( + id="file-abc123", + object="file", + bytes=100, + created_at=1234567890, + filename="mydata.jsonl", + purpose="batch", + status="uploaded", + ) + + async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router): + raise NotImplementedError + + async def afile_list(self, purpose, litellm_parent_otel_span): + raise NotImplementedError + + async def afile_delete( + self, file_id, litellm_parent_otel_span, llm_router, **data + ): + raise NotImplementedError + + async def afile_content( + self, file_id, litellm_parent_otel_span, llm_router, **data + ): + raise NotImplementedError + + return CapturingManagedFiles(), captured + + +def _post_file_with_team_metadata( + monkeypatch, + llm_router: Router, + team_metadata: dict, + form_data: dict, +): + """POST /v1/files with given team_metadata, return captured expires_after.""" + from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + + proxy_logging_obj = ProxyLogging( + user_api_key_cache=DualCache(default_in_memory_ttl=1) + ) + dummy, captured = _make_capturing_managed_files() + proxy_logging_obj.proxy_hook_mapping["managed_files"] = dummy + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router) + monkeypatch.setattr( + "litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj + ) + + user_key = UserAPIKeyAuth(api_key="test-key", team_metadata=team_metadata) + app.dependency_overrides[user_api_key_auth] = lambda: user_key + + test_file = ("mydata.jsonl", b'{"prompt": "Hello"}', "application/json") + try: + response = client.post( + "/v1/files", + files={"file": test_file}, + data=form_data, + headers={"Authorization": "Bearer test-key"}, + ) + assert response.status_code == 200 + finally: + app.dependency_overrides.clear() + + return captured["expires_after"] + + +def test_file_team_override_overrides_caller( + mocker: MockerFixture, monkeypatch, llm_router: Router +): + """Team enforced_file_expires_after wins over caller-provided value.""" + expires_after = _post_file_with_team_metadata( + monkeypatch, + llm_router, + team_metadata={ + "enforced_file_expires_after": { + "anchor": "created_at", + "seconds": 3600, + } + }, + form_data={ + "purpose": "batch", + "target_model_names": "gpt-3.5-turbo", + "expires_after[anchor]": "created_at", + "expires_after[seconds]": "86400", + }, + ) + assert expires_after["anchor"] == "created_at" + assert expires_after["seconds"] == 3600 + + +def test_file_no_team_setting_preserves_caller( + mocker: MockerFixture, monkeypatch, llm_router: Router +): + """No team setting = caller-provided expires_after passes through.""" + expires_after = _post_file_with_team_metadata( + monkeypatch, + llm_router, + team_metadata={}, + form_data={ + "purpose": "batch", + "target_model_names": "gpt-3.5-turbo", + "expires_after[anchor]": "created_at", + "expires_after[seconds]": "86400", + }, + ) + assert expires_after["anchor"] == "created_at" + assert expires_after["seconds"] == 86400 From 4edb1e00c60bf620ec6a179e29224298b000e081 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 12:36:26 -0800 Subject: [PATCH 067/171] [Test] UI - Add unit tests for project hooks Co-Authored-By: Claude Sonnet 4.6 --- .../hooks/projects/useCreateProject.test.ts | 111 ++++++++++++++ .../hooks/projects/useDeleteProject.test.ts | 88 +++++++++++ .../hooks/projects/useProjectDetails.test.ts | 144 ++++++++++++++++++ .../hooks/projects/useProjects.test.ts | 124 +++++++++++++++ .../hooks/projects/useUpdateProject.test.ts | 116 ++++++++++++++ 5 files changed, 583 insertions(+) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useCreateProject.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjectDetails.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjects.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useUpdateProject.test.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useCreateProject.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useCreateProject.test.ts new file mode 100644 index 00000000000..64d950d59ee --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useCreateProject.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useCreateProject, ProjectCreateParams } from "./useCreateProject"; +import { projectKeys, ProjectResponse } from "./useProjects"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => ""), + getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), + deriveErrorMessage: vi.fn((data: any) => data?.error || "Error"), + handleError: vi.fn(), +})); + +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +const mockProject: ProjectResponse = { + project_id: "proj-1", + project_alias: "Test Project", + description: "A test project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4"], + spend: 25.0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-02T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, +}; + +function makeWrapper(queryClient: QueryClient) { + return ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +} + +describe("useCreateProject", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + vi.clearAllMocks(); + global.fetch = vi.fn(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + }); + + it("should render", () => { + const { result } = renderHook(() => useCreateProject(), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.mutate).toBeDefined(); + }); + + it("should POST to /project/new and return the created project", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + const { result } = renderHook(() => useCreateProject(), { + wrapper: makeWrapper(queryClient), + }); + const params: ProjectCreateParams = { team_id: "team-1", project_alias: "New Project" }; + const data = await result.current.mutateAsync(params); + expect(data).toEqual(mockProject); + const [url, init] = (global.fetch as any).mock.calls[0]; + expect(url).toContain("/project/new"); + expect(init.method).toBe("POST"); + expect(JSON.parse(init.body)).toMatchObject(params); + }); + + it("should invalidate project queries on success", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + const { result } = renderHook(() => useCreateProject(), { + wrapper: makeWrapper(queryClient), + }); + await result.current.mutateAsync({ team_id: "team-1" }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: projectKeys.all }); + }); + + it("should set isError when the request fails", async () => { + (global.fetch as any).mockResolvedValue({ + ok: false, + json: async () => ({ error: "Server error" }), + }); + const { result } = renderHook(() => useCreateProject(), { + wrapper: makeWrapper(queryClient), + }); + result.current.mutateAsync({ team_id: "team-1" }).catch(() => {}); + await waitFor(() => expect(result.current.isError).toBe(true)); + }); + + it("should throw when accessToken is missing", async () => { + mockUseAuthorized.mockReturnValue({ accessToken: null, userRole: "Admin" }); + const { result } = renderHook(() => useCreateProject(), { + wrapper: makeWrapper(queryClient), + }); + await expect(result.current.mutateAsync({ team_id: "team-1" })).rejects.toThrow( + "Access token is required" + ); + expect(global.fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.test.ts new file mode 100644 index 00000000000..85a9f3e0b10 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useDeleteProject } from "./useDeleteProject"; +import { projectKeys } from "./useProjects"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => ""), + getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), + deriveErrorMessage: vi.fn((data: any) => data?.error || "Error"), + handleError: vi.fn(), +})); + +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +function makeWrapper(queryClient: QueryClient) { + return ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +} + +describe("useDeleteProject", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + vi.clearAllMocks(); + global.fetch = vi.fn(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + }); + + it("should render", () => { + const { result } = renderHook(() => useDeleteProject(), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.mutate).toBeDefined(); + }); + + it("should send DELETE to /project/delete with the given project IDs", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => ({}) }); + const { result } = renderHook(() => useDeleteProject(), { + wrapper: makeWrapper(queryClient), + }); + await result.current.mutateAsync(["proj-1", "proj-2"]); + const [url, init] = (global.fetch as any).mock.calls[0]; + expect(url).toContain("/project/delete"); + expect(init.method).toBe("DELETE"); + expect(JSON.parse(init.body)).toEqual({ project_ids: ["proj-1", "proj-2"] }); + }); + + it("should invalidate project queries on success", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => ({}) }); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + const { result } = renderHook(() => useDeleteProject(), { + wrapper: makeWrapper(queryClient), + }); + await result.current.mutateAsync(["proj-1"]); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: projectKeys.all }); + }); + + it("should set isError when the request fails", async () => { + (global.fetch as any).mockResolvedValue({ + ok: false, + json: async () => ({ error: "Not found" }), + }); + const { result } = renderHook(() => useDeleteProject(), { + wrapper: makeWrapper(queryClient), + }); + result.current.mutateAsync(["proj-1"]).catch(() => {}); + await waitFor(() => expect(result.current.isError).toBe(true)); + }); + + it("should throw when accessToken is missing", async () => { + mockUseAuthorized.mockReturnValue({ accessToken: null, userRole: "Admin" }); + const { result } = renderHook(() => useDeleteProject(), { + wrapper: makeWrapper(queryClient), + }); + await expect(result.current.mutateAsync(["proj-1"])).rejects.toThrow( + "Access token is required" + ); + expect(global.fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjectDetails.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjectDetails.test.ts new file mode 100644 index 00000000000..426abfe9bb6 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjectDetails.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useProjectDetails } from "./useProjectDetails"; +import { projectKeys, ProjectResponse } from "./useProjects"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => ""), + getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), + deriveErrorMessage: vi.fn((data: any) => data?.error || "Error"), + handleError: vi.fn(), +})); + +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +const mockProject: ProjectResponse = { + project_id: "proj-1", + project_alias: "Test Project", + description: "A test project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4"], + spend: 25.0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-02T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, +}; + +const mockProjects: ProjectResponse[] = [ + mockProject, + { ...mockProject, project_id: "proj-2", project_alias: "Test Project 2" }, +]; + +function makeWrapper(queryClient: QueryClient) { + return ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +} + +describe("useProjectDetails", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + vi.clearAllMocks(); + global.fetch = vi.fn(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + }); + + it("should render", () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + const { result } = renderHook(() => useProjectDetails("proj-1"), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current).toBeDefined(); + }); + + it("should return project details when the request succeeds", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + const { result } = renderHook(() => useProjectDetails("proj-1"), { + wrapper: makeWrapper(queryClient), + }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data).toEqual(mockProject); + }); + + it("should call /project/info with the projectId encoded as a query param", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + renderHook(() => useProjectDetails("proj-1"), { wrapper: makeWrapper(queryClient) }); + await waitFor(() => expect(global.fetch).toHaveBeenCalled()); + const [url] = (global.fetch as any).mock.calls[0]; + expect(url).toContain("/project/info"); + expect(url).toContain("project_id=proj-1"); + }); + + it("should not fetch when projectId is missing", () => { + const { result } = renderHook(() => useProjectDetails(undefined), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.isFetched).toBe(false); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("should not fetch when accessToken is missing", () => { + mockUseAuthorized.mockReturnValue({ accessToken: null, userRole: "Admin" }); + const { result } = renderHook(() => useProjectDetails("proj-1"), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.isFetched).toBe(false); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("should not fetch when userRole is not an admin role", () => { + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Internal User" }); + const { result } = renderHook(() => useProjectDetails("proj-1"), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.isFetched).toBe(false); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("should seed initialData from the projects list cache", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + queryClient.setQueryData(projectKeys.list({}), mockProjects); + const { result } = renderHook(() => useProjectDetails("proj-1"), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.data).toEqual(mockProject); + expect(result.current.isLoading).toBe(false); + await waitFor(() => expect(result.current.isFetching).toBe(false)); + }); + + it("should return undefined initialData when projectId is not in the cache", () => { + queryClient.setQueryData(projectKeys.list({}), mockProjects); + const { result } = renderHook(() => useProjectDetails("non-existent"), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.data).toBeUndefined(); + }); + + it("should set isError when the request fails", async () => { + (global.fetch as any).mockResolvedValue({ + ok: false, + json: async () => ({ error: "Not found" }), + }); + const { result } = renderHook(() => useProjectDetails("proj-1"), { + wrapper: makeWrapper(queryClient), + }); + await waitFor(() => expect(result.current.isError).toBe(true)); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjects.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjects.test.ts new file mode 100644 index 00000000000..13b9107bdc1 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useProjects.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useProjects, ProjectResponse } from "./useProjects"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => ""), + getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), + deriveErrorMessage: vi.fn((data: any) => data?.error || "Error"), + handleError: vi.fn(), +})); + +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +const mockProjects: ProjectResponse[] = [ + { + project_id: "proj-1", + project_alias: "Test Project", + description: "A test project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4"], + spend: 25.0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-02T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, + }, + { + project_id: "proj-2", + project_alias: "Test Project 2", + description: null, + team_id: "team-1", + budget_id: null, + metadata: null, + models: [], + spend: 0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-03T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-03T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, + }, +]; + +function makeWrapper(queryClient: QueryClient) { + return ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +} + +describe("useProjects", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + vi.clearAllMocks(); + global.fetch = vi.fn(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + }); + + it("should render", () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProjects }); + const { result } = renderHook(() => useProjects(), { wrapper: makeWrapper(queryClient) }); + expect(result.current).toBeDefined(); + }); + + it("should return projects when the request succeeds", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProjects }); + const { result } = renderHook(() => useProjects(), { wrapper: makeWrapper(queryClient) }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data).toEqual(mockProjects); + }); + + it("should call GET /project/list with the auth header", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProjects }); + renderHook(() => useProjects(), { wrapper: makeWrapper(queryClient) }); + await waitFor(() => expect(global.fetch).toHaveBeenCalled()); + const [url, init] = (global.fetch as any).mock.calls[0]; + expect(url).toContain("/project/list"); + expect(init.headers["Authorization"]).toBe("Bearer test-token"); + }); + + it("should set isError when the request fails", async () => { + (global.fetch as any).mockResolvedValue({ + ok: false, + json: async () => ({ error: "Not authorized" }), + }); + const { result } = renderHook(() => useProjects(), { wrapper: makeWrapper(queryClient) }); + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.data).toBeUndefined(); + }); + + it("should not fetch when accessToken is missing", () => { + mockUseAuthorized.mockReturnValue({ accessToken: null, userRole: "Admin" }); + const { result } = renderHook(() => useProjects(), { wrapper: makeWrapper(queryClient) }); + expect(result.current.isFetched).toBe(false); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("should not fetch when userRole is not an admin role", () => { + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Internal User" }); + const { result } = renderHook(() => useProjects(), { wrapper: makeWrapper(queryClient) }); + expect(result.current.isFetched).toBe(false); + expect(global.fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useUpdateProject.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useUpdateProject.test.ts new file mode 100644 index 00000000000..31d1a5fb352 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useUpdateProject.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useUpdateProject } from "./useUpdateProject"; +import { projectKeys, ProjectResponse } from "./useProjects"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => ""), + getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), + deriveErrorMessage: vi.fn((data: any) => data?.error || "Error"), + handleError: vi.fn(), +})); + +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +const mockProject: ProjectResponse = { + project_id: "proj-1", + project_alias: "Test Project", + description: "A test project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4"], + spend: 25.0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-02T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, +}; + +function makeWrapper(queryClient: QueryClient) { + return ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +} + +describe("useUpdateProject", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + vi.clearAllMocks(); + global.fetch = vi.fn(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + }); + + it("should render", () => { + const { result } = renderHook(() => useUpdateProject(), { + wrapper: makeWrapper(queryClient), + }); + expect(result.current.mutate).toBeDefined(); + }); + + it("should POST to /project/update and return the updated project", async () => { + const updated = { ...mockProject, project_alias: "Updated Name" }; + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => updated }); + const { result } = renderHook(() => useUpdateProject(), { + wrapper: makeWrapper(queryClient), + }); + const data = await result.current.mutateAsync({ + projectId: "proj-1", + params: { project_alias: "Updated Name" }, + }); + expect(data).toEqual(updated); + const [url, init] = (global.fetch as any).mock.calls[0]; + expect(url).toContain("/project/update"); + expect(JSON.parse(init.body)).toMatchObject({ + project_id: "proj-1", + project_alias: "Updated Name", + }); + }); + + it("should invalidate project queries on success", async () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => mockProject }); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + const { result } = renderHook(() => useUpdateProject(), { + wrapper: makeWrapper(queryClient), + }); + await result.current.mutateAsync({ projectId: "proj-1", params: {} }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: projectKeys.all }); + }); + + it("should set isError when the request fails", async () => { + (global.fetch as any).mockResolvedValue({ + ok: false, + json: async () => ({ error: "Server error" }), + }); + const { result } = renderHook(() => useUpdateProject(), { + wrapper: makeWrapper(queryClient), + }); + result.current.mutateAsync({ projectId: "proj-1", params: {} }).catch(() => {}); + await waitFor(() => expect(result.current.isError).toBe(true)); + }); + + it("should throw when accessToken is missing", async () => { + mockUseAuthorized.mockReturnValue({ accessToken: null, userRole: "Admin" }); + const { result } = renderHook(() => useUpdateProject(), { + wrapper: makeWrapper(queryClient), + }); + await expect( + result.current.mutateAsync({ projectId: "proj-1", params: {} }) + ).rejects.toThrow("Access token is required"); + expect(global.fetch).not.toHaveBeenCalled(); + }); +}); From 6b4bc99202c1fad64920c260f437ca52116966a5 Mon Sep 17 00:00:00 2001 From: Julio Quinteros Pro Date: Tue, 3 Mar 2026 18:12:49 -0300 Subject: [PATCH 068/171] Fix Anthropic streaming sync __next__ and Azure GPT-5.1 logprobs Two independent fixes for pre-existing test failures on main: 1. Anthropic streaming: The sync __next__ method used a simple holding_chunk pattern that lost chunks when multiple events needed to be returned. Refactored to use the same chunk_queue approach as the async __anext__ method. Also fixed tests that used ModelResponse (which defaults finish_reason to 'stop') instead of ModelResponseStream. 2. Azure GPT-5.1 logprobs: The base OpenAI class includes logprobs for gpt-5.1+ models, but Azure hasn't verified support for gpt-5.1. Added explicit removal of logprobs/top_logprobs for gpt-5.1 (non-5.2) models in the Azure config. Co-Authored-By: Claude Opus 4.6 --- .../adapters/streaming_iterator.py | 135 ++++++++++-------- .../llms/azure/chat/gpt_5_transformation.py | 8 +- .../test_content_after_stop_reason.py | 14 +- .../messages/test_parallel_tool_calls.py | 26 ++-- .../messages/test_sse_wrapper.py | 17 +-- 5 files changed, 105 insertions(+), 95 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index de634ff9ecf..cdf8ac5ca82 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -80,38 +80,40 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): from .transformation import LiteLLMAnthropicMessagesAdapter try: + # Always return queued chunks first + if self.chunk_queue: + return self.chunk_queue.popleft() + + # Queue initial chunks if not sent yet if self.sent_first_chunk is False: self.sent_first_chunk = True - return { - "type": "message_start", - "message": { - "id": "msg_{}".format(uuid.uuid4()), - "type": "message", - "role": "assistant", - "content": [], - "model": self.model, - "stop_reason": None, - "stop_sequence": None, - "usage": self._create_initial_usage_delta(), - }, - } + self.chunk_queue.append( + { + "type": "message_start", + "message": { + "id": "msg_{}".format(uuid.uuid4()), + "type": "message", + "role": "assistant", + "content": [], + "model": self.model, + "stop_reason": None, + "stop_sequence": None, + "usage": self._create_initial_usage_delta(), + }, + } + ) + return self.chunk_queue.popleft() + if self.sent_content_block_start is False: self.sent_content_block_start = True - return { - "type": "content_block_start", - "index": self.current_content_block_index, - "content_block": {"type": "text", "text": ""}, - } - - # Handle pending new content block start - if self.pending_new_content_block: - self.pending_new_content_block = False - self.sent_content_block_finish = False # Reset for new block - return { - "type": "content_block_start", - "index": self.current_content_block_index, - "content_block": self.current_content_block_start, - } + self.chunk_queue.append( + { + "type": "content_block_start", + "index": self.current_content_block_index, + "content_block": {"type": "text", "text": ""}, + } + ) + return self.chunk_queue.popleft() for chunk in self.completion_stream: if chunk == "None" or chunk is None: @@ -126,45 +128,65 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): current_content_block_index=self.current_content_block_index, ) - # Check if we need to start a new content block - # This is where you'd add your logic to detect when a new content block should start - # For example, if the chunk indicates a tool call or different content type - if should_start_new_block and not self.sent_content_block_finish: - # End current content block and prepare for new one - self.holding_chunk = processed_chunk - self.sent_content_block_finish = True - self.pending_new_content_block = True - return { - "type": "content_block_stop", - "index": max(self.current_content_block_index - 1, 0), - } + # Queue the sequence: content_block_stop -> content_block_start + # The trigger chunk itself is not emitted as a delta since the + # content_block_start already carries the relevant information. + self.chunk_queue.append( + { + "type": "content_block_stop", + "index": max(self.current_content_block_index - 1, 0), + } + ) + self.chunk_queue.append( + { + "type": "content_block_start", + "index": self.current_content_block_index, + "content_block": self.current_content_block_start, + } + ) + self.sent_content_block_finish = False + return self.chunk_queue.popleft() if ( processed_chunk["type"] == "message_delta" and self.sent_content_block_finish is False ): - self.holding_chunk = processed_chunk + # Queue both the content_block_stop and the message_delta + self.chunk_queue.append( + { + "type": "content_block_stop", + "index": self.current_content_block_index, + } + ) self.sent_content_block_finish = True - return { - "type": "content_block_stop", - "index": self.current_content_block_index, - } + self.chunk_queue.append(processed_chunk) + return self.chunk_queue.popleft() elif self.holding_chunk is not None: - return_chunk = self.holding_chunk - self.holding_chunk = processed_chunk - return return_chunk + self.chunk_queue.append(self.holding_chunk) + self.chunk_queue.append(processed_chunk) + self.holding_chunk = None + return self.chunk_queue.popleft() else: - return processed_chunk + self.chunk_queue.append(processed_chunk) + return self.chunk_queue.popleft() + + # Handle any remaining held chunks after stream ends if self.holding_chunk is not None: - return_chunk = self.holding_chunk + self.chunk_queue.append(self.holding_chunk) self.holding_chunk = None - return return_chunk - if self.sent_last_message is False: + + if not self.sent_last_message: self.sent_last_message = True - return {"type": "message_stop"} + self.chunk_queue.append({"type": "message_stop"}) + + if self.chunk_queue: + return self.chunk_queue.popleft() + raise StopIteration except StopIteration: + if self.chunk_queue: + return self.chunk_queue.popleft() if self.sent_last_message is False: self.sent_last_message = True return {"type": "message_stop"} @@ -265,7 +287,9 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): if not self.queued_usage_chunk: if should_start_new_block and not self.sent_content_block_finish: - # Queue the sequence: content_block_stop -> content_block_start -> current_chunk + # Queue the sequence: content_block_stop -> content_block_start + # The trigger chunk itself is not emitted as a delta since the + # content_block_start already carries the relevant information. # 1. Stop current content block self.chunk_queue.append( @@ -284,9 +308,6 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): } ) - # 3. Queue the current chunk (don't lose it!) - self.chunk_queue.append(processed_chunk) - # Reset state for new block self.sent_content_block_finish = False diff --git a/litellm/llms/azure/chat/gpt_5_transformation.py b/litellm/llms/azure/chat/gpt_5_transformation.py index eeb55911ecf..2a2955fca37 100644 --- a/litellm/llms/azure/chat/gpt_5_transformation.py +++ b/litellm/llms/azure/chat/gpt_5_transformation.py @@ -43,8 +43,12 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): if "tool_choice" not in params: params.append("tool_choice") - # Only gpt-5.2 has been verified to support logprobs on Azure - if self.is_model_gpt_5_2_model(model): + # Only gpt-5.2 has been verified to support logprobs on Azure. + # The base OpenAI class includes logprobs for gpt-5.1+, but Azure + # hasn't verified support for gpt-5.1, so remove them unless gpt-5.2. + if self.is_model_gpt_5_1_model(model) and not self.is_model_gpt_5_2_model(model): + params = [p for p in params if p not in ["logprobs", "top_logprobs"]] + elif self.is_model_gpt_5_2_model(model): azure_supported_params = ["logprobs", "top_logprobs"] params.extend(azure_supported_params) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py index 4a170d666f5..eadc0da2f1f 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py @@ -23,7 +23,7 @@ sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterator import ( AnthropicStreamWrapper, ) -from litellm.types.utils import Delta, ModelResponse, StreamingChoices, Usage +from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices, Usage class MockCompletionStreamWithContentAfterStopReason: @@ -32,16 +32,14 @@ class MockCompletionStreamWithContentAfterStopReason: def __init__(self): self.responses = [ # Initial text content - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content="Hello"), index=0, finish_reason=None ) ], ), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=" world"), index=0, finish_reason=None @@ -49,8 +47,7 @@ class MockCompletionStreamWithContentAfterStopReason: ], ), # Message delta with stop_reason AND usage (this is how it actually comes from the API) - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=""), index=0, finish_reason="stop" @@ -60,8 +57,7 @@ class MockCompletionStreamWithContentAfterStopReason: ), # Additional content after the stop_reason - this simulates the scenario # where there might be additional content blocks after the main response - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=" Additional content"), diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py index 9d4e58f3c88..1d25d719384 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py @@ -10,7 +10,7 @@ from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterato ) from litellm.types.utils import ( Delta, - ModelResponse, + ModelResponseStream, StreamingChoices, Usage, ChatCompletionDeltaToolCall, @@ -19,7 +19,7 @@ from litellm.types.utils import ( class MockCompletionStream: - def __init__(self, responses: List[ModelResponse]): + def __init__(self, responses: List[ModelResponseStream]): self.responses = responses self.index = 0 @@ -44,9 +44,8 @@ class MockCompletionStream: return response -def construct_text_chunk(text: str) -> ModelResponse: - return ModelResponse( - stream=True, +def construct_text_chunk(text: str) -> ModelResponseStream: + return ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=text), @@ -59,11 +58,10 @@ def construct_text_chunk(text: str) -> ModelResponse: def construct_split_tool_call( id: str, function_name: str, function_arg_parts: List[str] -) -> List[ModelResponse]: +) -> List[ModelResponseStream]: return [ # https://platform.openai.com/docs/guides/function-calling#streaming - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta( @@ -82,8 +80,7 @@ def construct_split_tool_call( ], ), *[ - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta( @@ -109,8 +106,7 @@ def construct_split_tool_call( def test_anthropic_stream_wrapper_single_tool_call(): responses = [ *construct_split_tool_call("tooluse_foo", "get_weather", ['{"city":', '"NY"}']), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content="", stop_reason="tool_calls"), @@ -172,8 +168,7 @@ def test_anthropic_stream_wrapper_back_to_back_tool_calls(): responses = [ *construct_split_tool_call("tooluse_foo", "get_weather", ['{"city":', '"NY"}']), *construct_split_tool_call("tooluse_bar", "get_weather", ['{"city":', '"SF"}']), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content="", stop_reason="tool_calls"), @@ -244,8 +239,7 @@ def test_anthropic_stream_wrapper_interleaved_tool_calls_and_text(): "tooluse_bar", "get_weather", ['{"city":', '"CHI"}'] ), construct_text_chunk("The weather is not so nice today."), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content="", stop_reason="tool_calls"), diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py index dfcb9b3eb74..63fed907c3c 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py @@ -9,31 +9,28 @@ sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterator import ( AnthropicStreamWrapper, ) -from litellm.types.utils import Delta, ModelResponse, StreamingChoices +from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices # Create a simple test class MockCompletionStream: def __init__(self): self.responses = [ - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content="Hello"), index=0, finish_reason=None ) ], ), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=" World"), index=0, finish_reason=None ) ], ), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=""), index=0, finish_reason="stop" @@ -109,16 +106,14 @@ async def test_async_anthropic_sse_wrapper(): class AsyncMockCompletionStream: def __init__(self): self.responses = [ - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content="Hello"), index=0, finish_reason=None ) ], ), - ModelResponse( - stream=True, + ModelResponseStream( choices=[ StreamingChoices( delta=Delta(content=" World"), index=0, finish_reason=None From ab718444c57db2bc88f078d82a8821ca7f9ba7d7 Mon Sep 17 00:00:00 2001 From: Julio Quinteros Pro Date: Tue, 3 Mar 2026 18:28:34 -0300 Subject: [PATCH 069/171] Remove dead pending_new_content_block attribute Cleanup per review: this class attribute is no longer used after the __next__ refactor to queue-based approach. Co-Authored-By: Claude Opus 4.6 --- .../experimental_pass_through/adapters/streaming_iterator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index cdf8ac5ca82..7f17526e75c 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -41,7 +41,6 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): type="text", text="", ) - pending_new_content_block: bool = False chunk_queue: deque = deque() # Queue for buffering multiple chunks def __init__( From d6ad312a4c2a2d14625210a9bd6daceb14940f97 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 13:37:11 -0800 Subject: [PATCH 070/171] fix(proxy): validate enforced_file_expires_after keys before access Add key validation for enforced_file_expires_after to return a clear 400 error instead of an unhandled KeyError 500. --- .../proxy/openai_files_endpoints/files_endpoints.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index a641fb1c69f..386ab2bf044 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -458,11 +458,22 @@ async def create_file( # noqa: PLR0915 team_metadata = user_api_key_dict.team_metadata or {} enforced_file_expiry = team_metadata.get("enforced_file_expires_after") if enforced_file_expiry is not None: + if "anchor" not in enforced_file_expiry or "seconds" not in enforced_file_expiry: + raise HTTPException( + status_code=400, + detail={ + "error": "enforced_file_expires_after must contain 'anchor' and 'seconds' keys", + }, + ) expires_after = FileExpiresAfter( anchor=enforced_file_expiry["anchor"], seconds=enforced_file_expiry["seconds"], ) + verbose_proxy_logger.info( + "create_file expires_after: %s", expires_after + ) + _create_file_request = CreateFileRequest( file=file_data, purpose=cast(CREATE_FILE_REQUESTS_PURPOSE, purpose), From 903ade4a1b3d10c83c2b7b91ffdc22322739e55e Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 13:51:41 -0800 Subject: [PATCH 071/171] fix(proxy): add anchor validation for file expiry, key validation for batch expiry Validate anchor is "created_at" in enforced_file_expires_after (matching user-provided path). Add key existence validation to batch endpoint for enforced_batch_output_expires_after. --- litellm/proxy/batches_endpoints/endpoints.py | 7 +++++++ litellm/proxy/openai_files_endpoints/files_endpoints.py | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 60905243369..850134b649c 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -125,6 +125,13 @@ async def create_batch( # noqa: PLR0915 "enforced_batch_output_expires_after" ) if enforced_batch_expiry is not None: + if "anchor" not in enforced_batch_expiry or "seconds" not in enforced_batch_expiry: + raise HTTPException( + status_code=400, + detail={ + "error": "enforced_batch_output_expires_after must contain 'anchor' and 'seconds' keys", + }, + ) _create_batch_data["output_expires_after"] = enforced_batch_expiry input_file_id = _create_batch_data.get("input_file_id", None) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 386ab2bf044..82cae8c64ea 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -465,8 +465,15 @@ async def create_file( # noqa: PLR0915 "error": "enforced_file_expires_after must contain 'anchor' and 'seconds' keys", }, ) + if enforced_file_expiry["anchor"] != "created_at": + raise HTTPException( + status_code=400, + detail={ + "error": f"enforced_file_expires_after anchor must be 'created_at', got '{enforced_file_expiry['anchor']}'", + }, + ) expires_after = FileExpiresAfter( - anchor=enforced_file_expiry["anchor"], + anchor="created_at", seconds=enforced_file_expiry["seconds"], ) From 657a60ea5b55fa3497e4222b12ec782fc54f2df8 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 14:24:55 -0800 Subject: [PATCH 072/171] fix(audit): AND semantics for combined JSON filters; remove unused allTeams prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix object_team_id + object_key_hash combining incorrectly as OR — each filter now adds an AND clause wrapping an internal OR over before_value and updated_values, so both conditions must be satisfied simultaneously - Rename helper to _build_json_field_or_condition to reflect its purpose - Remove allTeams from AuditLogsProps and its call site in index.tsx Co-Authored-By: Claude Sonnet 4.6 --- .../proxy/audit_logging_endpoints.py | 42 ++++++++++--------- .../src/components/view_logs/audit_logs.tsx | 2 - .../src/components/view_logs/index.tsx | 1 - 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py index 5ab3669b50c..18ac29b9781 100644 --- a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py @@ -22,19 +22,25 @@ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth router = APIRouter() -def _build_json_field_conditions( - field: str, json_key: str, value: str -) -> List[Dict[str, Any]]: +def _build_json_field_or_condition(json_key: str, value: str) -> Dict[str, Any]: """ - Build OR conditions to match a value inside a JSON column at the given key. + Build an OR condition that matches a value inside a JSON column at the + given key, checking both before_value and updated_values. - Uses Prisma's JSON path filtering (PostgreSQL only). Returns a list of - two conditions — one for `before_value` and one for `updated_values` — to - be merged into the caller's top-level OR list. + Uses Prisma's JSON path filtering (PostgreSQL only). + + Example result (team_id="t1"): + {"OR": [ + {"before_value": {"path": ["team_id"], "string_contains": "t1"}}, + {"updated_values": {"path": ["team_id"], "string_contains": "t1"}}, + ]} """ - return [ - {field: {"path": [json_key], "string_contains": value}}, - ] + return { + "OR": [ + {"before_value": {"path": [json_key], "string_contains": value}}, + {"updated_values": {"path": [json_key], "string_contains": value}}, + ] + } @router.get( @@ -115,19 +121,15 @@ async def get_audit_logs( date_filter["lte"] = end_date where_conditions["updated_at"] = date_filter - # JSON field filters (PostgreSQL only) — search inside before_value and - # updated_values for a matching key/value pair. + # JSON field filters (PostgreSQL only) — each filter is AND'd with the + # others, but checks both before_value and updated_values internally (OR). if object_team_id: - where_conditions["OR"] = [ - *_build_json_field_conditions("before_value", "team_id", object_team_id), - *_build_json_field_conditions("updated_values", "team_id", object_team_id), + where_conditions["AND"] = where_conditions.get("AND", []) + [ + _build_json_field_or_condition("team_id", object_team_id) ] if object_key_hash: - existing_or: List[Dict[str, Any]] = where_conditions.get("OR", []) - where_conditions["OR"] = [ - *existing_or, - *_build_json_field_conditions("before_value", "token", object_key_hash), - *_build_json_field_conditions("updated_values", "token", object_key_hash), + where_conditions["AND"] = where_conditions.get("AND", []) + [ + _build_json_field_or_condition("token", object_key_hash) ] # Build sort conditions diff --git a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx index d3372eca6ff..693318acca8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx @@ -7,7 +7,6 @@ import moment from "moment"; import { uiAuditLogsCall } from "../networking"; import { AuditLogEntry } from "./columns"; import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer"; -import { Team } from "../key_team_helpers/key_list"; const { Search } = Input; @@ -18,7 +17,6 @@ interface AuditLogsProps { userID: string | null; isActive: boolean; premiumUser: boolean; - allTeams: Team[]; } const asset_logos_folder = "../ui/assets/"; diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 22cc06a73e9..64dc53c0e77 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -714,7 +714,6 @@ export default function SpendLogsTable({ accessToken={accessToken} isActive={activeTab === "audit logs"} premiumUser={premiumUser} - allTeams={allTeams} /> From 35d2bc382f0185850fbe6c9e505a69d940fb1619 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 14:28:25 -0800 Subject: [PATCH 073/171] fix(batches): suppress PLR0915 lint for create_batch dispatch function --- litellm/batches/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/batches/main.py b/litellm/batches/main.py index e73f73d2f33..e69c5a5c377 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -154,7 +154,7 @@ async def acreate_batch( @client -def create_batch( +def create_batch( # noqa: PLR0915 completion_window: Literal["24h"], endpoint: Literal["/v1/chat/completions", "/v1/embeddings", "/v1/completions"], input_file_id: str, From c8e6428eb77196030d65f68de9e8a09aab6ce72c Mon Sep 17 00:00:00 2001 From: ryan-crabbe <128659760+ryan-crabbe@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:35:58 -0800 Subject: [PATCH 074/171] Update litellm/proxy/openai_files_endpoints/files_endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/proxy/openai_files_endpoints/files_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 82cae8c64ea..44bd9b09d8e 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -477,7 +477,7 @@ async def create_file( # noqa: PLR0915 seconds=enforced_file_expiry["seconds"], ) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "create_file expires_after: %s", expires_after ) From 5da7fa9ac18a0b82a1a294389d6cf146fa443dd9 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 14:39:39 -0800 Subject: [PATCH 075/171] [Feature] UI - Virtual Keys: Add manual spend reset to unblock keys Adds a "Reset Spend" button to the key detail view so proxy admins and team admins can immediately reset a key's spend to $0, unblocking keys that have hit their budget limit without waiting for the next scheduled budget reset. Co-Authored-By: Claude Sonnet 4.6 --- .../hooks/keys/useResetKeySpend.ts | 66 ++++++++ .../components/templates/KeyInfoHeader.tsx | 8 + .../templates/key_info_view.test.tsx | 141 ++++++++++++++++++ .../components/templates/key_info_view.tsx | 50 ++++++- 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts new file mode 100644 index 00000000000..a845fc5881a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts @@ -0,0 +1,66 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + getProxyBaseUrl, + getGlobalLitellmHeaderName, + deriveErrorMessage, + handleError, +} from "@/components/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { keyKeys } from "./useKeys"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +export interface ResetKeySpendResponse { + key_hash: string; + spend: number; + previous_spend: number; + max_budget: number | null; + budget_reset_at: string | null; +} + +// ── Fetch function ──────────────────────────────────────────────────────────── + +export const resetKeySpend = async ( + accessToken: string, + keyToken: string, +): Promise => { + const baseUrl = getProxyBaseUrl(); + const url = `${baseUrl ? `${baseUrl}/key/${keyToken}/reset_spend` : `/key/${keyToken}/reset_spend`}`; + + const response = await fetch(url, { + method: "POST", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ reset_to: 0 }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return response.json(); +}; + +// ── Hook ────────────────────────────────────────────────────────────────────── + +export const useResetKeySpend = () => { + const { accessToken } = useAuthorized(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (keyToken) => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return resetKeySpend(accessToken, keyToken); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: keyKeys.all }); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx index 1befd657843..df3678afab6 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx @@ -11,6 +11,7 @@ import { ClockCircleOutlined, ThunderboltOutlined, SafetyCertificateOutlined, + DollarOutlined, } from "@ant-design/icons"; import LabeledField from "../common_components/LabeledField"; @@ -33,6 +34,7 @@ interface KeyInfoHeaderProps { onCreateNew?: () => void; onRegenerate?: () => void; onDelete?: () => void; + onResetSpend?: () => void; canModifyKey?: boolean; backButtonText?: string; regenerateDisabled?: boolean; @@ -45,6 +47,7 @@ export function KeyInfoHeader({ onCreateNew, onRegenerate, onDelete, + onResetSpend, canModifyKey = true, backButtonText = "Back to Keys", regenerateDisabled = false, @@ -77,6 +80,11 @@ export function KeyInfoHeader({ {canModifyKey && ( + {onResetSpend && ( + + )} )} diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 35a75b2f2fe..b77b13b1591 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -450,8 +450,8 @@ export default function KeyInfoView({ $0?

- Current spend: ${formatNumberWithCommas(currentKeyData.spend, 4)}. The key will be - immediately unblocked and able to make requests again. + Current spend: ${formatNumberWithCommas(currentKeyData.spend, 4)}. Spend history is + preserved in logs. This resets the current period spend counter, the same as an automatic budget reset.

From 5b2110ddb56492c6b8876c1e5cf3608d754d3820 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 15:16:47 -0800 Subject: [PATCH 083/171] Polish Reset Spend button and modal - Move Reset Spend button after Regenerate Key in header - Make modal OK button danger style with text "Reset" Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/templates/KeyInfoHeader.tsx | 10 +++++----- .../src/components/templates/key_info_view.test.tsx | 4 ++-- .../src/components/templates/key_info_view.tsx | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx index c750b1b4ac7..93ebae9c4be 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx @@ -80,11 +80,6 @@ export function KeyInfoHeader({ {canModifyKey && ( - {onResetSpend && ( - - )} + {onResetSpend && ( + + )} diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index c19866e5d84..f269ad96a27 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -642,7 +642,7 @@ describe("KeyInfoView", () => { await waitFor(() => { expect(screen.getByText("Reset Key Spend")).toBeInTheDocument(); - expect(screen.getByText(/reset to \$0/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /^reset$/i })).toBeInTheDocument(); }); }); @@ -670,7 +670,7 @@ describe("KeyInfoView", () => { }); // Click the confirm button in the modal - await userEvent.click(screen.getByRole("button", { name: /reset to \$0/i })); + await userEvent.click(screen.getByRole("button", { name: /^reset$/i })); await waitFor(() => { expect(mockResetKeySpendMutate).toHaveBeenCalledWith( diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index b77b13b1591..6733cd6a595 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -442,7 +442,8 @@ export default function KeyInfoView({ open={isResetSpendModalOpen} onOk={handleResetSpend} onCancel={() => setIsResetSpendModalOpen(false)} - okText="Reset to $0" + okText="Reset" + okButtonProps={{ danger: true }} confirmLoading={resetSpendLoading} >

From 1a23ba41fd29823f8f845ddd6de83bf68700f498 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Tue, 3 Mar 2026 15:18:52 -0800 Subject: [PATCH 084/171] Fixed: undefined is not a valid Select option value --- ui/litellm-dashboard/src/components/model_info_view.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 0ba86e9f243..b8a5cb10fa1 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -627,7 +627,7 @@ export default function ModelInfoView({ : [], tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [], health_check_model: isWildcardModel ? localModelData.model_info?.health_check_model : null, - litellm_credential_name: localModelData.litellm_params?.litellm_credential_name || undefined, + litellm_credential_name: localModelData.litellm_params?.litellm_credential_name || "", litellm_extra_params: JSON.stringify( Object.fromEntries( Object.entries(localModelData.litellm_params || {}).filter( @@ -970,7 +970,7 @@ export default function ModelInfoView({ (option?.label ?? "").toLowerCase().includes(input.toLowerCase()) } options={[ - { value: undefined, label: "None" }, + { value: "", label: "None" }, ...credentialsList.map((credential) => ({ value: credential.credential_name, label: credential.credential_name, From 98b9bc8b722b3f8d590d4cbe678280f8fa95cca7 Mon Sep 17 00:00:00 2001 From: milan-berri Date: Wed, 4 Mar 2026 01:43:02 +0200 Subject: [PATCH 085/171] fix: resolve base_model in /cost/estimate for Azure custom deployments (#22724) The _resolve_model_for_cost_lookup function was only checking litellm_params.model when resolving model names from the router. For Azure custom deployment names (e.g. azure/openai/gpt-5.3-codex), this deployment name doesn't exist in the model cost map, so cost returned /bin/zsh. Now checks model_info.base_model and litellm_params.base_model first, falling back to litellm_params.model only if no base_model is set. This matches how the router resolves base_model everywhere else. --- .../cost_tracking_settings.py | 15 ++- .../test_cost_tracking_settings.py | 120 ++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/management_endpoints/cost_tracking_settings.py b/litellm/proxy/management_endpoints/cost_tracking_settings.py index 6cdadfe216a..38dd4578c05 100644 --- a/litellm/proxy/management_endpoints/cost_tracking_settings.py +++ b/litellm/proxy/management_endpoints/cost_tracking_settings.py @@ -54,16 +54,27 @@ def _resolve_model_for_cost_lookup(model: str) -> Tuple[str, Optional[str]]: deployments = llm_router.get_model_list(model_name=model) if deployments and len(deployments) > 0: - # Get the first deployment's litellm model first_deployment = deployments[0] litellm_params = first_deployment.get("litellm_params", {}) + model_info = first_deployment.get("model_info", {}) + + # Check base_model first (needed for Azure custom deployment names) + base_model = model_info.get("base_model") or litellm_params.get( + "base_model" + ) + if base_model: + verbose_proxy_logger.debug( + f"Resolved model '{model}' to base_model '{base_model}' from router" + ) + custom_llm_provider = litellm_params.get("custom_llm_provider") + return base_model, custom_llm_provider + resolved_model = litellm_params.get("model") if resolved_model: verbose_proxy_logger.debug( f"Resolved model '{model}' to '{resolved_model}' from router" ) - # Extract custom_llm_provider if present custom_llm_provider = litellm_params.get("custom_llm_provider") return resolved_model, custom_llm_provider except Exception as e: diff --git a/tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py b/tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py index 275240dcc9e..1284cceba26 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py +++ b/tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py @@ -270,3 +270,123 @@ class TestCostTrackingSettings: assert "error" in response_data["detail"] assert "STORE_MODEL_IN_DB" in response_data["detail"]["error"] + + +class TestResolveModelForCostLookup: + """Tests for _resolve_model_for_cost_lookup base_model resolution.""" + + def test_resolves_base_model_for_azure_deployment(self): + """ + When a model group has base_model set in model_info, + _resolve_model_for_cost_lookup should return the base_model + instead of the raw litellm_params.model (Azure deployment name). + """ + from litellm.proxy.management_endpoints.cost_tracking_settings import ( + _resolve_model_for_cost_lookup, + ) + + mock_router = MagicMock() + mock_router.get_model_list.return_value = [ + { + "model_name": "gpt-5.3-codex", + "litellm_params": { + "model": "azure/openai/gpt-5.3-codex", + "api_base": "https://fake.openai.azure.com/", + "api_key": "fake-key", + }, + "model_info": { + "id": "test-id", + "base_model": "azure/gpt-4o", + }, + } + ] + + with patch( + "litellm.proxy.proxy_server.llm_router", + mock_router, + ): + resolved_model, provider = _resolve_model_for_cost_lookup("gpt-5.3-codex") + + assert resolved_model == "azure/gpt-4o" + mock_router.get_model_list.assert_called_once_with(model_name="gpt-5.3-codex") + + def test_falls_back_to_litellm_params_model_when_no_base_model(self): + """ + When no base_model is set, should fall back to litellm_params.model. + """ + from litellm.proxy.management_endpoints.cost_tracking_settings import ( + _resolve_model_for_cost_lookup, + ) + + mock_router = MagicMock() + mock_router.get_model_list.return_value = [ + { + "model_name": "gpt-4", + "litellm_params": { + "model": "openai/gpt-4", + }, + "model_info": { + "id": "test-id", + }, + } + ] + + with patch( + "litellm.proxy.proxy_server.llm_router", + mock_router, + ): + resolved_model, provider = _resolve_model_for_cost_lookup("gpt-4") + + assert resolved_model == "openai/gpt-4" + + def test_resolves_base_model_from_litellm_params(self): + """ + When base_model is in litellm_params (not model_info), + it should still be resolved. + """ + from litellm.proxy.management_endpoints.cost_tracking_settings import ( + _resolve_model_for_cost_lookup, + ) + + mock_router = MagicMock() + mock_router.get_model_list.return_value = [ + { + "model_name": "my-azure-model", + "litellm_params": { + "model": "azure/my-custom-deployment", + "base_model": "azure/gpt-4o-mini", + }, + "model_info": { + "id": "test-id", + }, + } + ] + + with patch( + "litellm.proxy.proxy_server.llm_router", + mock_router, + ): + resolved_model, provider = _resolve_model_for_cost_lookup( + "my-azure-model" + ) + + assert resolved_model == "azure/gpt-4o-mini" + + def test_returns_original_model_when_no_router(self): + """ + When no router is available, should return the original model name. + """ + from litellm.proxy.management_endpoints.cost_tracking_settings import ( + _resolve_model_for_cost_lookup, + ) + + with patch( + "litellm.proxy.proxy_server.llm_router", + None, + ): + resolved_model, provider = _resolve_model_for_cost_lookup( + "azure/openai/gpt-5.3-codex" + ) + + assert resolved_model == "azure/openai/gpt-5.3-codex" + assert provider is None From 0a1b2635d7f8c2e17f9c3c5732bb9b8f3dc23c08 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 15:54:34 -0800 Subject: [PATCH 086/171] fix: allow team admins to access /key/{key}/reset_spend route The route-level auth check was blocking internal_user role (team admins) from reaching /key/{key}/reset_spend because KEY_RESET_SPEND was missing from key_management_routes. Added it so team admins pass the route check and the endpoint's existing _check_proxy_or_team_admin_for_key enforces actual authorization. Co-Authored-By: Claude Sonnet 4.6 --- litellm/proxy/_types.py | 1 + .../proxy/auth/test_route_checks.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 5122c64ea64..8d49020461d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -512,6 +512,7 @@ class LiteLLMRoutes(enum.Enum): KeyManagementRoutes.KEY_UNBLOCK.value, KeyManagementRoutes.KEY_BULK_UPDATE.value, KeyManagementRoutes.TEAM_DAILY_ACTIVITY.value, + KeyManagementRoutes.KEY_RESET_SPEND.value, ] management_routes = [ diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index ec1a13b8abc..f1e96f3e660 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -1116,3 +1116,77 @@ def test_route_in_additional_public_routes_exact_match(): assert route_in_additonal_public_routes("/status") is True # Non-matching routes should fail assert route_in_additonal_public_routes("/other") is False + + +def test_internal_user_can_access_key_reset_spend_route(): + """ + Regression test: team admins (role=internal_user) should pass the route-level + check for /key/{hash}/reset_spend. The endpoint itself enforces team admin status. + """ + user_obj = LiteLLM_UserTable( + user_id="team-admin-user", + user_email="teamadmin@example.com", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + valid_token = UserAPIKeyAuth( + user_id="team-admin-user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + request = MagicMock(spec=Request) + request.query_params = {} + + key_hash = "baec26d2901589fe9fec76610e6e2be4895cdd8e19b3ada9a4fa2eb85e1901ae" + route = f"/key/{key_hash}/reset_spend" + + # Should not raise — the route-level check must pass for team admins + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + + +def test_non_admin_non_team_admin_cannot_access_config_update_but_can_attempt_reset_spend(): + """ + An internal_user passes the route check for /key/{hash}/reset_spend + (authorization is deferred to the endpoint), but is still blocked from + admin-only routes like /config/update. + """ + user_obj = LiteLLM_UserTable( + user_id="regular-user", + user_email="user@example.com", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + valid_token = UserAPIKeyAuth( + user_id="regular-user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + request = MagicMock(spec=Request) + request.query_params = {} + + key_hash = "baec26d2901589fe9fec76610e6e2be4895cdd8e19b3ada9a4fa2eb85e1901ae" + + # /key/{hash}/reset_spend passes the route check for internal_user + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route=f"/key/{key_hash}/reset_spend", + request=request, + valid_token=valid_token, + request_data={}, + ) + + # /config/update is still blocked + with pytest.raises(Exception) as exc_info: + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route="/config/update", + request=request, + valid_token=valid_token, + request_data={}, + ) + assert "Only proxy admin can be used to generate" in str(exc_info.value) From 326ff422f1faad0a7654289d97824270be6c5686 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 15:54:46 -0800 Subject: [PATCH 087/171] fix(ui): Audit logs table and drawer polish Table: - Use DefaultProxyAdminTag for changed_by column - Remove tooltips on Object ID and API Key columns - Rename API Key column to API Key (Hash) - Move pagination controls to upper-right of filter bar; add icon-only refresh button Drawer: - Object ID is now copyable - API Key (Hash) is copyable and no longer truncated - Changed By uses DefaultProxyAdminTag - Expand JSON view boxes from max-h-72 to max-h-96 - Remove unnecessary vertical scrollbar (drop overflow-auto h-full from body div; use flex column layout so header and content flow naturally) Co-Authored-By: Claude Sonnet 4.6 --- .../AuditLogDrawer/AuditLogDrawer.tsx | 40 +++--- .../src/components/view_logs/audit_logs.tsx | 125 ++++++------------ 2 files changed, 62 insertions(+), 103 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx index 1e8dca34f22..de76709f5b2 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx @@ -1,7 +1,10 @@ -import { Drawer, Tag, Tooltip } from "antd"; +import { Drawer, Tag, Typography } from "antd"; import { CloseOutlined } from "@ant-design/icons"; import moment from "moment"; import { AuditLogEntry } from "../columns"; +import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag"; + +const { Text } = Typography; interface AuditLogDrawerProps { open: boolean; @@ -26,7 +29,7 @@ const ACTION_COLOR: Record = { function JsonBlock({ value }: { value: Record }) { return ( -

+    
       {JSON.stringify(value, null, 2)}
     
); @@ -92,7 +95,7 @@ function DiffSection({ log }: { log: AuditLogEntry }) { : { note: "No differing fields detected" }; } - const renderValue = (value: Record | null | undefined, label: string) => { + const renderValue = (value: Record | null | undefined) => { if (!value || Object.keys(value).length === 0) { return

N/A

; } @@ -125,11 +128,11 @@ function DiffSection({ log }: { log: AuditLogEntry }) {

Before

- {renderValue(displayBefore, "before")} + {renderValue(displayBefore)}

After

- {renderValue(displayAfter, "after")} + {renderValue(displayAfter)}
); @@ -150,10 +153,10 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) { closable={false} mask={true} maskClosable={true} - styles={{ body: { padding: 0 }, header: { display: "none" } }} + styles={{ body: { padding: 0, display: "flex", flexDirection: "column" }, header: { display: "none" } }} > {/* Header */} -
+
{log.action} @@ -172,7 +175,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
{/* Body */} -
+
{/* Metadata */}

@@ -182,21 +185,22 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) { - {log.object_id} - + + {log.object_id} + } /> - } + /> + - - {log.changed_by_api_key.slice(0, 12)}… - - + + {log.changed_by_api_key} + ) : ( "—" ) diff --git a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx index 693318acca8..71300fe0427 100644 --- a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx @@ -1,12 +1,13 @@ import { useState } from "react"; import { useQuery, keepPreviousData } from "@tanstack/react-query"; -import { Table, Tag, Input, Select, Button, Tooltip } from "antd"; +import { Table, Tag, Input, Select, Button, Pagination } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; -import type { ColumnsType, TablePaginationConfig } from "antd/es/table"; +import type { ColumnsType } from "antd/es/table"; import moment from "moment"; import { uiAuditLogsCall } from "../networking"; import { AuditLogEntry } from "./columns"; import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer"; +import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; const { Search } = Input; @@ -97,14 +98,7 @@ export default function AuditLogs({ placeholderData: keepPreviousData, }); - const handleFilterChange = () => { - // Reset to page 1 whenever a filter changes - setPage(1); - }; - - const handleTableChange = (pagination: TablePaginationConfig) => { - setPage(pagination.current ?? 1); - }; + const resetPage = () => setPage(1); const handleRowClick = (log: AuditLogEntry) => { setSelectedLog(log); @@ -146,9 +140,7 @@ export default function AuditLogs({ dataIndex: "object_id", key: "object_id", render: (val: string) => ( - - {val} - + {val} ), }, { @@ -156,18 +148,16 @@ export default function AuditLogs({ dataIndex: "changed_by", key: "changed_by", width: 200, - render: (val: string) => val || "—", + render: (val: string) => , }, { - title: "API Key", + title: "API Key (Hash)", dataIndex: "changed_by_api_key", key: "changed_by_api_key", width: 140, render: (val: string) => val ? ( - - {val.slice(0, 12)}… - + {val.slice(0, 12)}… ) : ( "—" ), @@ -212,76 +202,37 @@ export default function AuditLogs({

Audit Logs

-
- {/* Filters */} -
+ {/* Filters + pagination on same row */} +
{ - setObjectId(val); - handleFilterChange(); - }} - onChange={(e) => { - if (!e.target.value) { - setObjectId(""); - handleFilterChange(); - } - }} + onSearch={(val) => { setObjectId(val); resetPage(); }} + onChange={(e) => { if (!e.target.value) { setObjectId(""); resetPage(); } }} /> { - setChangedBy(val); - handleFilterChange(); - }} - onChange={(e) => { - if (!e.target.value) { - setChangedBy(""); - handleFilterChange(); - } - }} + onSearch={(val) => { setChangedBy(val); resetPage(); }} + onChange={(e) => { if (!e.target.value) { setChangedBy(""); resetPage(); } }} /> { - setTeamId(val); - handleFilterChange(); - }} - onChange={(e) => { - if (!e.target.value) { - setTeamId(""); - handleFilterChange(); - } - }} + onSearch={(val) => { setTeamId(val); resetPage(); }} + onChange={(e) => { if (!e.target.value) { setTeamId(""); resetPage(); } }} /> { - setKeyHash(val); - handleFilterChange(); - }} - onChange={(e) => { - if (!e.target.value) { - setKeyHash(""); - handleFilterChange(); - } - }} + onSearch={(val) => { setKeyHash(val); resetPage(); }} + onChange={(e) => { if (!e.target.value) { setKeyHash(""); resetPage(); } }} /> { - setTableName(val); - handleFilterChange(); - }} + onChange={(val) => { setTableName(val); resetPage(); }} /> + + {/* Pagination + refresh pushed to the right */} +
+
- {/* Table */} + {/* Table — pagination handled in header */} columns={columns} dataSource={auditLogs} rowKey="id" loading={query.isLoading} size="small" + pagination={false} onRow={(record) => ({ onClick: () => handleRowClick(record), style: { cursor: "pointer" }, })} - pagination={{ - current: page, - pageSize: PAGE_SIZE, - total, - showTotal: (t) => `${t} total`, - showSizeChanger: false, - onChange: (p) => setPage(p), - }} - onChange={handleTableChange} />
From 51f18b05a97f9352ef6fa20a064fb253c03565f1 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia <67175024+atapia27@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:00:16 -0800 Subject: [PATCH 088/171] Update ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../src/components/team/TeamVirtualKeysTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index de3061a003d..c3cdd7fba5b 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -569,7 +569,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi onSortingChange: handleSortingChange, onPaginationChange: setTablePagination, getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), + // getSortedRowModel not needed — manualSorting: true delegates sorting to the server enableSorting: true, manualSorting: true, // Server sorts via useKeys. Avoid redundant client-side sort manualPagination: true, From 5fa0c6e994de825502c3d5b1f8278a5ef03a4850 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 16:00:46 -0800 Subject: [PATCH 089/171] fix(ui): Copyable JSON blocks in audit drawer; custom table spinner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace plain JsonBlock with CopyableJsonBlock: header row with label and copy icon (CheckOutlined on success), pre block below — matches the spend logs RequestResponsePanel pattern - Key-table rows use the same card chrome for visual consistency - Remove separate "Changes" section label (now redundant with block headers) - Table loading spinner replaced with custom Spin + LoadingOutlined Co-Authored-By: Claude Sonnet 4.6 --- .../AuditLogDrawer/AuditLogDrawer.tsx | 105 ++++++++++++------ .../src/components/view_logs/audit_logs.tsx | 9 +- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx index de76709f5b2..19989ef4882 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx @@ -1,5 +1,6 @@ import { Drawer, Tag, Typography } from "antd"; -import { CloseOutlined } from "@ant-design/icons"; +import { CloseOutlined, CopyOutlined, CheckOutlined } from "@ant-design/icons"; +import { useState, useCallback } from "react"; import moment from "moment"; import { AuditLogEntry } from "../columns"; import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag"; @@ -27,11 +28,48 @@ const ACTION_COLOR: Record = { rotated: "orange", }; -function JsonBlock({ value }: { value: Record }) { +function CopyableJsonBlock({ label, value }: { label: string; value: Record }) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + try { + const text = JSON.stringify(value, null, 2); + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + const el = document.createElement("textarea"); + el.value = text; + el.style.position = "fixed"; + el.style.opacity = "0"; + document.body.appendChild(el); + el.focus(); + el.select(); + document.execCommand("copy"); + document.body.removeChild(el); + } + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (e) { + console.error("Copy failed:", e); + } + }, [value]); + return ( -
-      {JSON.stringify(value, null, 2)}
-    
+
+
+ {label} + +
+
+        {JSON.stringify(value, null, 2)}
+      
+
); } @@ -95,45 +133,51 @@ function DiffSection({ log }: { log: AuditLogEntry }) { : { note: "No differing fields detected" }; } - const renderValue = (value: Record | null | undefined) => { + const renderValue = (label: string, value: Record | null | undefined) => { if (!value || Object.keys(value).length === 0) { - return

N/A

; + return ( +
+
+ {label} +
+

N/A

+
+ ); } - // For key table updates, filter to only show meaningful fields + // For key table updates, show only meaningful fields as plain text if (isKeyTable && isUpdateAction) { const knownKeyFields = ["token", "spend", "max_budget"]; const hasOnlyKnown = Object.keys(value).every((k) => knownKeyFields.includes(k)); if (hasOnlyKnown && !("note" in value)) { return ( -
- {value.token !== undefined && ( -

Token: {value.token ?? "N/A"}

- )} - {value.spend !== undefined && ( -

Spend: ${Number(value.spend).toFixed(6)}

- )} - {value.max_budget !== undefined && ( -

Max Budget: ${Number(value.max_budget).toFixed(6)}

- )} +
+
+ {label} +
+
+ {value.token !== undefined && ( +

Token: {value.token ?? "N/A"}

+ )} + {value.spend !== undefined && ( +

Spend: ${Number(value.spend).toFixed(6)}

+ )} + {value.max_budget !== undefined && ( +

Max Budget: ${Number(value.max_budget).toFixed(6)}

+ )} +
); } } - return ; + return ; }; return (
-
-

Before

- {renderValue(displayBefore)} -
-
-

After

- {renderValue(displayAfter)} -
+ {renderValue("Before", displayBefore)} + {renderValue("After", displayAfter)}
); } @@ -209,12 +253,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
{/* Diff */} -
-

- Changes -

- -
+
); diff --git a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx index 71300fe0427..b16ba30049d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQuery, keepPreviousData } from "@tanstack/react-query"; -import { Table, Tag, Input, Select, Button, Pagination } from "antd"; -import { ReloadOutlined } from "@ant-design/icons"; +import { Table, Tag, Input, Select, Button, Pagination, Spin } from "antd"; +import { ReloadOutlined, LoadingOutlined } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import moment from "moment"; import { uiAuditLogsCall } from "../networking"; @@ -285,7 +285,10 @@ export default function AuditLogs({ columns={columns} dataSource={auditLogs} rowKey="id" - loading={query.isLoading} + loading={{ + spinning: query.isLoading, + indicator: } size="small" />, + }} size="small" pagination={false} onRow={(record) => ({ From 383f79f36f4a9a6cfe38bfe786a5a940f528325d Mon Sep 17 00:00:00 2001 From: Alejandro Tapia <67175024+atapia27@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:00:57 -0800 Subject: [PATCH 090/171] Update ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../src/components/key_team_helpers/filter_helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index 97166f92735..a1602fc3aea 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -1,4 +1,4 @@ -import { teamListCall, organizationListCall, keyAliasesCall, keyListCall } from "../networking"; +import { teamListCall, organizationListCall, keyListCall } from "../networking"; import { Team } from "./key_list"; import { Organization } from "../networking"; From 47ff0d1d863426a3390dfb30bd871b98bf28b9a1 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Tue, 3 Mar 2026 16:03:25 -0800 Subject: [PATCH 091/171] Fixed: 50 sequential API calls on mount is excessive --- .../key_team_helpers/filter_helpers.ts | 109 +++++++++++------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index a1602fc3aea..539ac172569 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -9,12 +9,35 @@ export interface TeamFilterOptions { } const FILTER_OPTIONS_PAGE_SIZE = 100; // API max per page -const MAX_PAGES = 50; // Cap at 5000 keys to avoid unbounded fetches +const MAX_PAGES = 10; // Cap at 1000 keys; filter completeness beyond ~500 has diminishing returns + +const processKeysIntoOptions = ( + keys: Array>, + keyAliases: Set, + organizationIds: Set, + userMap: Map, +) => { + for (const key of keys) { + const alias = key?.key_alias; + if (alias && typeof alias === "string") { + keyAliases.add(alias.trim()); + } + const orgId = key?.organization_id; + if (orgId && typeof orgId === "string") { + organizationIds.add(orgId.trim()); + } + const userId = key?.user_id; + if (userId && typeof userId === "string") { + const email = (key?.user as { user_email?: string })?.user_email || userId; + userMap.set(userId, email); + } + } +}; /** - * Fetches filter options (key aliases, org IDs, user IDs) from all team keys. - * Paginates through pages to build complete dropdowns. Capped at 50 pages - * (5000 keys) to limit load for very large teams. + * Fetches filter options (key aliases, org IDs, user IDs) from team keys. + * Fetches page 1 first to get totalPages, then batches remaining pages with + * Promise.all. Capped at 10 pages (1000 keys) */ export const fetchTeamFilterOptions = async ( accessToken: string | null, @@ -29,46 +52,50 @@ export const fetchTeamFilterOptions = async ( const organizationIds = new Set(); const userMap = new Map(); - let page = 1; - let totalPages = 1; + // First request: get page 1 and totalPages + const firstResponse = await keyListCall( + accessToken, + null, + teamId, + null, + null, + null, + 1, + FILTER_OPTIONS_PAGE_SIZE, + null, + null, + "user", + null, + ); - do { - const response = await keyListCall( - accessToken, - null, - teamId, - null, - null, - null, - page, - FILTER_OPTIONS_PAGE_SIZE, - null, - null, - "user", - null, + const firstKeys = firstResponse?.keys || []; + const totalPages = firstResponse?.total_pages ?? 1; + processKeysIntoOptions(firstKeys, keyAliases, organizationIds, userMap); + + // Batch fetch remaining pages (2 through min(totalPages, MAX_PAGES)) in parallel + const pagesToFetch = Math.min(totalPages, MAX_PAGES) - 1; + if (pagesToFetch > 0) { + const pagePromises = Array.from({ length: pagesToFetch }, (_, i) => + keyListCall( + accessToken, + null, + teamId, + null, + null, + null, + i + 2, + FILTER_OPTIONS_PAGE_SIZE, + null, + null, + "user", + null, + ), ); - - const keys = response?.keys || []; - totalPages = response?.total_pages ?? 1; - - for (const key of keys) { - const alias = key?.key_alias; - if (alias && typeof alias === "string") { - keyAliases.add(alias.trim()); - } - const orgId = key?.organization_id; - if (orgId && typeof orgId === "string") { - organizationIds.add(orgId.trim()); - } - const userId = key?.user_id; - if (userId && typeof userId === "string") { - const email = key?.user?.user_email || userId; - userMap.set(userId, email); - } + const responses = await Promise.all(pagePromises); + for (const response of responses) { + processKeysIntoOptions(response?.keys || [], keyAliases, organizationIds, userMap); } - - page++; - } while (page <= totalPages && page <= MAX_PAGES); + } return { keyAliases: Array.from(keyAliases).sort(), From 34acbb39da74f890f22f014d3dcac431705efa45 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia <67175024+atapia27@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:19:32 -0800 Subject: [PATCH 092/171] Update ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../src/components/team/TeamVirtualKeysTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index c3cdd7fba5b..bb4ab2b974d 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -8,7 +8,7 @@ import { ColumnDef, flexRender, getCoreRowModel, - getSortedRowModel, + getCoreRowModel, PaginationState, SortingState, useReactTable, From 18a2213cdb375d31f4cb255533c156d2452b7900 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Tue, 3 Mar 2026 16:21:02 -0800 Subject: [PATCH 093/171] Fix: Promise.all loses all results on single page failure --- .../src/components/key_team_helpers/filter_helpers.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index 539ac172569..b587e090d33 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -37,7 +37,7 @@ const processKeysIntoOptions = ( /** * Fetches filter options (key aliases, org IDs, user IDs) from team keys. * Fetches page 1 first to get totalPages, then batches remaining pages with - * Promise.all. Capped at 10 pages (1000 keys) + * Promise.allSettled (preserves successful pages if some fail). Capped at 10 pages (1000 keys) */ export const fetchTeamFilterOptions = async ( accessToken: string | null, @@ -91,9 +91,11 @@ export const fetchTeamFilterOptions = async ( null, ), ); - const responses = await Promise.all(pagePromises); - for (const response of responses) { - processKeysIntoOptions(response?.keys || [], keyAliases, organizationIds, userMap); + const results = await Promise.allSettled(pagePromises); + for (const result of results) { + if (result.status === "fulfilled") { + processKeysIntoOptions(result.value?.keys || [], keyAliases, organizationIds, userMap); + } } } From 5f38c3bfecd403bd126e6999d1729471a2f72701 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia <67175024+atapia27@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:30:10 -0800 Subject: [PATCH 094/171] Update ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../src/components/team/TeamVirtualKeysTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index bb4ab2b974d..5d76b99ef91 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -8,7 +8,6 @@ import { ColumnDef, flexRender, getCoreRowModel, - getCoreRowModel, PaginationState, SortingState, useReactTable, From 52ec73c07d52fbf6c2de64200f5d46810377a8f5 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 3 Mar 2026 17:29:39 -0800 Subject: [PATCH 095/171] fix(proxy): improve team expiry enforcement validation - Change status codes from 400 to 500 for team metadata misconfig errors (callers can't fix admin-set config, 400 is misleading) - Add anchor value validation to batch endpoint (matching files endpoint) - Coerce seconds to int to handle string values from metadata - Add error-path tests: missing keys, invalid anchor, status code assertions - Add happy-path test: team injects expiry when caller sends nothing --- litellm/proxy/batches_endpoints/endpoints.py | 16 ++- .../openai_files_endpoints/files_endpoints.py | 10 +- .../test_files_endpoint.py | 118 ++++++++++++++++++ tests/test_litellm/proxy/test_batch_expiry.py | 100 +++++++++++++++ 4 files changed, 236 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 850134b649c..17855cf9bd7 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -127,12 +127,22 @@ async def create_batch( # noqa: PLR0915 if enforced_batch_expiry is not None: if "anchor" not in enforced_batch_expiry or "seconds" not in enforced_batch_expiry: raise HTTPException( - status_code=400, + status_code=500, detail={ - "error": "enforced_batch_output_expires_after must contain 'anchor' and 'seconds' keys", + "error": "Server configuration error: team metadata field 'enforced_batch_output_expires_after' is malformed - must contain 'anchor' and 'seconds' keys. Contact your team or proxy admin to fix this setting.", }, ) - _create_batch_data["output_expires_after"] = enforced_batch_expiry + if enforced_batch_expiry["anchor"] != "created_at": + raise HTTPException( + status_code=500, + detail={ + "error": f"Server configuration error: team metadata field 'enforced_batch_output_expires_after' has invalid anchor '{enforced_batch_expiry['anchor']}' - must be 'created_at'. Contact your team or proxy admin to fix this setting.", + }, + ) + _create_batch_data["output_expires_after"] = { + "anchor": "created_at", + "seconds": int(enforced_batch_expiry["seconds"]), + } input_file_id = _create_batch_data.get("input_file_id", None) unified_file_id: Union[str, Literal[False]] = False diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 44bd9b09d8e..8a02f96926e 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -460,21 +460,21 @@ async def create_file( # noqa: PLR0915 if enforced_file_expiry is not None: if "anchor" not in enforced_file_expiry or "seconds" not in enforced_file_expiry: raise HTTPException( - status_code=400, + status_code=500, detail={ - "error": "enforced_file_expires_after must contain 'anchor' and 'seconds' keys", + "error": "Server configuration error: team metadata field 'enforced_file_expires_after' is malformed - must contain 'anchor' and 'seconds' keys. Contact your team or proxy admin to fix this setting.", }, ) if enforced_file_expiry["anchor"] != "created_at": raise HTTPException( - status_code=400, + status_code=500, detail={ - "error": f"enforced_file_expires_after anchor must be 'created_at', got '{enforced_file_expiry['anchor']}'", + "error": f"Server configuration error: team metadata field 'enforced_file_expires_after' has invalid anchor '{enforced_file_expiry['anchor']}' - must be 'created_at'. Contact your team or proxy admin to fix this setting.", }, ) expires_after = FileExpiresAfter( anchor="created_at", - seconds=enforced_file_expiry["seconds"], + seconds=int(enforced_file_expiry["seconds"]), ) verbose_proxy_logger.debug( diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py index 0239c39e67f..83f7bb520af 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py @@ -1303,3 +1303,121 @@ def test_file_no_team_setting_preserves_caller( ) assert expires_after["anchor"] == "created_at" assert expires_after["seconds"] == 86400 + + +def test_file_team_injects_when_caller_sends_nothing( + mocker: MockerFixture, monkeypatch, llm_router: Router +): + """Team enforcement applies even when caller sends no expiry.""" + expires_after = _post_file_with_team_metadata( + monkeypatch, + llm_router, + team_metadata={ + "enforced_file_expires_after": { + "anchor": "created_at", + "seconds": 3600, + } + }, + form_data={ + "purpose": "batch", + "target_model_names": "gpt-3.5-turbo", + }, + ) + assert expires_after["anchor"] == "created_at" + assert expires_after["seconds"] == 3600 + + +# --------------------------------------------------------------------------- +# Team-level enforced_file_expires_after validation error tests +# --------------------------------------------------------------------------- + + +def _post_file_raw(monkeypatch, llm_router: Router, team_metadata: dict, form_data: dict): + """POST /v1/files and return the raw response (no status assertion).""" + from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + + proxy_logging_obj = ProxyLogging( + user_api_key_cache=DualCache(default_in_memory_ttl=1) + ) + dummy, _ = _make_capturing_managed_files() + proxy_logging_obj.proxy_hook_mapping["managed_files"] = dummy + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router) + monkeypatch.setattr( + "litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj + ) + + user_key = UserAPIKeyAuth(api_key="test-key", team_metadata=team_metadata) + app.dependency_overrides[user_api_key_auth] = lambda: user_key + + test_file = ("mydata.jsonl", b'{"prompt": "Hello"}', "application/json") + try: + response = client.post( + "/v1/files", + files={"file": test_file}, + data=form_data, + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.clear() + + return response + + +def test_file_missing_anchor_key_returns_500( + mocker: MockerFixture, monkeypatch, llm_router: Router +): + """Missing 'anchor' key in team metadata returns 500.""" + response = _post_file_raw( + monkeypatch, + llm_router, + team_metadata={ + "enforced_file_expires_after": {"seconds": 3600}, + }, + form_data={ + "purpose": "batch", + "target_model_names": "gpt-3.5-turbo", + }, + ) + assert response.status_code == 500 + assert "malformed" in response.json()["error"]["message"] + + +def test_file_missing_seconds_key_returns_500( + mocker: MockerFixture, monkeypatch, llm_router: Router +): + """Missing 'seconds' key in team metadata returns 500.""" + response = _post_file_raw( + monkeypatch, + llm_router, + team_metadata={ + "enforced_file_expires_after": {"anchor": "created_at"}, + }, + form_data={ + "purpose": "batch", + "target_model_names": "gpt-3.5-turbo", + }, + ) + assert response.status_code == 500 + assert "malformed" in response.json()["error"]["message"] + + +def test_file_invalid_anchor_returns_500( + mocker: MockerFixture, monkeypatch, llm_router: Router +): + """Invalid anchor value in team metadata returns 500.""" + response = _post_file_raw( + monkeypatch, + llm_router, + team_metadata={ + "enforced_file_expires_after": { + "anchor": "updated_at", + "seconds": 3600, + }, + }, + form_data={ + "purpose": "batch", + "target_model_names": "gpt-3.5-turbo", + }, + ) + assert response.status_code == 500 + assert "created_at" in response.json()["error"]["message"] diff --git a/tests/test_litellm/proxy/test_batch_expiry.py b/tests/test_litellm/proxy/test_batch_expiry.py index 1f54f190c63..d63f278e715 100644 --- a/tests/test_litellm/proxy/test_batch_expiry.py +++ b/tests/test_litellm/proxy/test_batch_expiry.py @@ -160,3 +160,103 @@ class TestBatchEndpointTeamOverride: }, ) assert kwargs["output_expires_after"] == CALLER_EXPIRY + + def test_team_injects_when_caller_sends_nothing(self, monkeypatch, llm_router): + """Team enforcement applies even when caller sends no expiry.""" + kwargs = self._post_batch( + monkeypatch, + llm_router, + team_metadata={ + "enforced_batch_output_expires_after": TEAM_EXPIRY, + }, + request_body={ + "input_file_id": "file-abc123", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + }, + ) + assert kwargs["output_expires_after"] == TEAM_EXPIRY + + +class TestBatchEndpointTeamValidation: + """Verify validation errors for malformed team metadata on batch endpoint.""" + + def _post_batch_raw( + self, + monkeypatch, + llm_router: Router, + team_metadata: dict, + request_body: dict, + ): + """POST /v1/batches and return the raw response (no status assertion).""" + _setup_proxy(monkeypatch, llm_router) + + user_key = UserAPIKeyAuth( + api_key="test-key", + team_metadata=team_metadata, + ) + app.dependency_overrides[user_api_key_auth] = lambda: user_key + + async def mock_acreate_batch(**kwargs): + return _make_batch_response() + + monkeypatch.setattr(litellm, "acreate_batch", mock_acreate_batch) + + try: + response = client.post( + "/v1/batches", + json=request_body, + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.clear() + + return response + + _BATCH_BODY = { + "input_file_id": "file-abc123", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + } + + def test_missing_anchor_key_returns_500(self, monkeypatch, llm_router): + """Missing 'anchor' key in team metadata returns 500.""" + response = self._post_batch_raw( + monkeypatch, + llm_router, + team_metadata={ + "enforced_batch_output_expires_after": {"seconds": 3600}, + }, + request_body=self._BATCH_BODY, + ) + assert response.status_code == 500 + assert "malformed" in response.json()["error"]["message"] + + def test_missing_seconds_key_returns_500(self, monkeypatch, llm_router): + """Missing 'seconds' key in team metadata returns 500.""" + response = self._post_batch_raw( + monkeypatch, + llm_router, + team_metadata={ + "enforced_batch_output_expires_after": {"anchor": "created_at"}, + }, + request_body=self._BATCH_BODY, + ) + assert response.status_code == 500 + assert "malformed" in response.json()["error"]["message"] + + def test_invalid_anchor_returns_500(self, monkeypatch, llm_router): + """Invalid anchor value in team metadata returns 500.""" + response = self._post_batch_raw( + monkeypatch, + llm_router, + team_metadata={ + "enforced_batch_output_expires_after": { + "anchor": "last_active_at", + "seconds": 3600, + }, + }, + request_body=self._BATCH_BODY, + ) + assert response.status_code == 500 + assert "created_at" in response.json()["error"]["message"] From 90eb6729d57c269b717f886479e4e53d4a8979dd Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Tue, 3 Mar 2026 18:19:12 -0800 Subject: [PATCH 096/171] Agent Tracing - support context_id based trace id propogation + nested llm calls (#22626) * style(ui/): distinguish agent calls from llm calls on ui * feat: initial grouping working * feat: set stable contextid for a2a calls - allows for easily passing to downstream llm/mcp calls * feat(a2a_endpoints.py): fix tracing to avoid recreating logging objects for the same call allows stable trace id usage * fix(guardrail_endpoints): handle string ui_type values in _build_field_dict _build_field_dict unconditionally called .value on ui_type, which crashes for guardrail configs that use plain strings (e.g. BlockCodeExecutionGuardrailConfigModel uses "multiselect" and "percentage"). Now checks with hasattr before calling .value. Co-Authored-By: Claude Opus 4.6 * fix: propagate trace/session id from headers in MCP server calls Cherry-picked mcp_server/server.py fixes from 6feb9bab: adds get_chain_id_from_headers to extract x-litellm-trace-id / x-litellm-session-id from raw headers, and uses it in call_tool and list_tools to keep spend logs and tracing consistent with A2A. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- docs/my-website/docs/response_api.md | 2 +- litellm/a2a_protocol/main.py | 91 ++++-- .../litellm_core_utils/get_litellm_params.py | 8 +- litellm/litellm_core_utils/litellm_logging.py | 306 +++++++++--------- .../proxy/_experimental/mcp_server/server.py | 17 +- .../out/{404.html => 404/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{budgets.html => budgets/index.html} | 0 .../{caching.html => caching/index.html} | 0 .../index.html} | 0 .../{old-usage.html => old-usage/index.html} | 0 .../{prompts.html => prompts/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../out/{login.html => login/index.html} | 0 .../out/{logs.html => logs/index.html} | 0 .../{callback.html => callback/index.html} | 0 .../{model-hub.html => model-hub/index.html} | 0 .../{model_hub.html => model_hub/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{policies.html => policies/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{ui-theme.html => ui-theme/index.html} | 0 .../out/{teams.html => teams/index.html} | 0 .../{test-key.html => test-key/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../out/{usage.html => usage/index.html} | 0 .../out/{users.html => users/index.html} | 0 .../index.html} | 0 .../proxy/agent_endpoints/a2a_endpoints.py | 49 ++- .../proxy/guardrails/guardrail_endpoints.py | 8 +- litellm/proxy/litellm_pre_call_utils.py | 79 +++-- .../spend_tracking/spend_tracking_utils.py | 83 +++-- litellm/utils.py | 52 +-- .../guardrails/test_guardrail_endpoints.py | 43 ++- .../proxy/test_litellm_pre_call_utils.py | 100 ++++-- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 39 ++- .../src/components/view_logs/TypeBadges.tsx | 21 +- .../src/components/view_logs/columns.tsx | 25 +- .../src/components/view_logs/constants.ts | 3 + .../src/components/view_logs/index.tsx | 9 +- ui/litellm-dashboard/tsconfig.json | 2 +- 51 files changed, 579 insertions(+), 358 deletions(-) rename litellm/proxy/_experimental/out/{404.html => 404/index.html} (100%) rename litellm/proxy/_experimental/out/{_not-found.html => _not-found/index.html} (100%) rename litellm/proxy/_experimental/out/{api-reference.html => api-reference/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{api-playground.html => api-playground/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{budgets.html => budgets/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{caching.html => caching/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{claude-code-plugins.html => claude-code-plugins/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{old-usage.html => old-usage/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{prompts.html => prompts/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{tag-management.html => tag-management/index.html} (100%) rename litellm/proxy/_experimental/out/{guardrails.html => guardrails/index.html} (100%) rename litellm/proxy/_experimental/out/{login.html => login/index.html} (100%) rename litellm/proxy/_experimental/out/{logs.html => logs/index.html} (100%) rename litellm/proxy/_experimental/out/mcp/oauth/{callback.html => callback/index.html} (100%) rename litellm/proxy/_experimental/out/{model-hub.html => model-hub/index.html} (100%) rename litellm/proxy/_experimental/out/{model_hub.html => model_hub/index.html} (100%) rename litellm/proxy/_experimental/out/{model_hub_table.html => model_hub_table/index.html} (100%) rename litellm/proxy/_experimental/out/{models-and-endpoints.html => models-and-endpoints/index.html} (100%) rename litellm/proxy/_experimental/out/{onboarding.html => onboarding/index.html} (100%) rename litellm/proxy/_experimental/out/{organizations.html => organizations/index.html} (100%) rename litellm/proxy/_experimental/out/{playground.html => playground/index.html} (100%) rename litellm/proxy/_experimental/out/{policies.html => policies/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{admin-settings.html => admin-settings/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{logging-and-alerts.html => logging-and-alerts/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{router-settings.html => router-settings/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{ui-theme.html => ui-theme/index.html} (100%) rename litellm/proxy/_experimental/out/{teams.html => teams/index.html} (100%) rename litellm/proxy/_experimental/out/{test-key.html => test-key/index.html} (100%) rename litellm/proxy/_experimental/out/tools/{mcp-servers.html => mcp-servers/index.html} (100%) rename litellm/proxy/_experimental/out/tools/{vector-stores.html => vector-stores/index.html} (100%) rename litellm/proxy/_experimental/out/{usage.html => usage/index.html} (100%) rename litellm/proxy/_experimental/out/{users.html => users/index.html} (100%) rename litellm/proxy/_experimental/out/{virtual-keys.html => virtual-keys/index.html} (100%) diff --git a/docs/my-website/docs/response_api.md b/docs/my-website/docs/response_api.md index a7cf61ef16a..76899a17ccb 100644 --- a/docs/my-website/docs/response_api.md +++ b/docs/my-website/docs/response_api.md @@ -930,7 +930,7 @@ For Responses API with load balancing across deployments with **different API ke Notes: - User-key affinity is keyed on `metadata.user_api_key_hash` (the API key hash). The OpenAI `user` request parameter is an end-user identifier and is intentionally not used for deployment affinity. -- Session-ID affinity is keyed on `metadata.session_id`. For proxy requests, this can be passed via the `x-litellm-session-id` HTTP header. For Python SDK requests, you can pass it via `litellm_metadata={"session_id": "value"}` in request args. +- Session-ID affinity is keyed on `metadata.session_id`. For proxy requests, this can be passed via the `x-litellm-session-id` or `x-litellm-trace-id` HTTP header (they are interchangeable for call chaining). For Python SDK requests, you can pass it via `litellm_metadata={"session_id": "value"}` in request args. - `user_api_key_hash` is already SHA-256, and is used as-is (no double hashing). - Affinity is scoped by a stable model identifier (the model-map key, e.g. `model_map_information.model_map_key`) so model aliases map to the same stickiness bucket. - The mapping TTL is controlled by `deployment_affinity_ttl_seconds` (configured on Router init / proxy startup). diff --git a/litellm/a2a_protocol/main.py b/litellm/a2a_protocol/main.py index 642dfaf023c..401b602fef5 100644 --- a/litellm/a2a_protocol/main.py +++ b/litellm/a2a_protocol/main.py @@ -24,11 +24,7 @@ from litellm.utils import client if TYPE_CHECKING: from a2a.client import A2AClient as A2AClientType - from a2a.types import ( - AgentCard, - SendMessageRequest, - SendStreamingMessageRequest, - ) + from a2a.types import AgentCard, SendMessageRequest, SendStreamingMessageRequest # Runtime imports with availability check A2A_SDK_AVAILABLE = False @@ -124,13 +120,48 @@ def _get_a2a_model_info(a2a_client: Any, kwargs: Dict[str, Any]) -> str: litellm_logging_obj.model = model litellm_logging_obj.custom_llm_provider = custom_llm_provider litellm_logging_obj.model_call_details["model"] = model - litellm_logging_obj.model_call_details[ - "custom_llm_provider" - ] = custom_llm_provider + litellm_logging_obj.model_call_details["custom_llm_provider"] = ( + custom_llm_provider + ) return agent_name +async def _send_message_via_completion_bridge( + request: "SendMessageRequest", + custom_llm_provider: str, + api_base: Optional[str], + litellm_params: Dict[str, Any], +) -> LiteLLMSendMessageResponse: + """ + Route a send_message through the LiteLLM completion bridge (e.g. LangGraph, Bedrock AgentCore). + + Requires request; api_base is optional for providers that derive endpoint from model. + """ + verbose_logger.info( + f"A2A using completion bridge: provider={custom_llm_provider}, api_base={api_base}" + ) + + from litellm.a2a_protocol.litellm_completion_bridge.handler import ( + A2ACompletionBridgeHandler, + ) + + params = ( + request.params.model_dump(mode="json") + if hasattr(request.params, "model_dump") + else dict(request.params) + ) + + response_dict = await A2ACompletionBridgeHandler.handle_non_streaming( + request_id=str(request.id), + params=params, + litellm_params=litellm_params, + api_base=api_base, + ) + + return LiteLLMSendMessageResponse.from_dict(response_dict) + + @client async def asend_message( a2a_client: Optional["A2AClientType"] = None, @@ -193,39 +224,21 @@ async def asend_message( ``` """ litellm_params = litellm_params or {} + logging_obj = kwargs.get("litellm_logging_obj") + trace_id = getattr(logging_obj, "litellm_trace_id", None) if logging_obj else None custom_llm_provider = litellm_params.get("custom_llm_provider") # Route through completion bridge if custom_llm_provider is set if custom_llm_provider: if request is None: raise ValueError("request is required for completion bridge") - # api_base is optional for providers that derive endpoint from model (e.g., bedrock/agentcore) - - verbose_logger.info( - f"A2A using completion bridge: provider={custom_llm_provider}, api_base={api_base}" - ) - - from litellm.a2a_protocol.litellm_completion_bridge.handler import ( - A2ACompletionBridgeHandler, - ) - - # Extract params from request - params = ( - request.params.model_dump(mode="json") - if hasattr(request.params, "model_dump") - else dict(request.params) - ) - - response_dict = await A2ACompletionBridgeHandler.handle_non_streaming( - request_id=str(request.id), - params=params, - litellm_params=litellm_params, + return await _send_message_via_completion_bridge( + request=request, + custom_llm_provider=custom_llm_provider, api_base=api_base, + litellm_params=litellm_params, ) - # Convert to LiteLLMSendMessageResponse - return LiteLLMSendMessageResponse.from_dict(response_dict) - # Standard A2A client flow if request is None: raise ValueError("request is required") @@ -236,11 +249,13 @@ async def asend_message( raise ValueError( "Either a2a_client or api_base is required for standard A2A flow" ) - trace_id = str(uuid.uuid4()) + trace_id = trace_id or str(uuid.uuid4()) extra_headers = {"X-LiteLLM-Trace-Id": trace_id} if agent_id: extra_headers["X-LiteLLM-Agent-Id"] = agent_id - a2a_client = await create_a2a_client(base_url=api_base, extra_headers=extra_headers) + a2a_client = await create_a2a_client( + base_url=api_base, extra_headers=extra_headers + ) # Type assertion: a2a_client is guaranteed to be non-None here assert a2a_client is not None @@ -255,6 +270,10 @@ async def asend_message( ) card_url = getattr(agent_card, "url", None) if agent_card else None + context_id = trace_id or str(uuid.uuid4()) + if request.params.message.context_id is None: + request.params.message.context_id = context_id + # Retry loop: if connection fails due to localhost URL in agent card, retry with fixed URL a2a_response = None for _ in range(2): # max 2 attempts: original + 1 retry @@ -606,7 +625,9 @@ async def create_a2a_client( if extra_headers: httpx_client.headers.update(extra_headers) - verbose_proxy_logger.debug(f"A2A client created with extra_headers={extra_headers}") + verbose_proxy_logger.debug( + f"A2A client created with extra_headers={extra_headers}" + ) # Resolve agent card resolver = A2ACardResolver( diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index 36a8dfdb5a6..c91e4b6de1d 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -1,6 +1,5 @@ from typing import Optional - # Pre-define optional kwargs keys as frozenset for O(1) lookups # These are extracted from kwargs only if present, avoiding unnecessary .get() calls _OPTIONAL_KWARGS_KEYS = frozenset({ @@ -95,6 +94,13 @@ def get_litellm_params( litellm_request_debug: Optional[bool] = None, **kwargs, ) -> dict: + # Derive litellm_session_id / litellm_trace_id from metadata when not provided (call chaining) + _meta = metadata or {} + if litellm_session_id is None: + litellm_session_id = _meta.get("session_id") or _meta.get("trace_id") + if litellm_trace_id is None: + litellm_trace_id = _meta.get("trace_id") or _meta.get("session_id") + # Build base dict with explicit parameters (always included) litellm_params = { "acompletion": acompletion, diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 5e5a6cea1b2..6f587abcdf1 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -133,8 +133,8 @@ from ..integrations.azure_sentinel.azure_sentinel import AzureSentinelLogger from ..integrations.azure_storage.azure_storage import AzureBlobStorageLogger from ..integrations.custom_prompt_management import CustomPromptManagement from ..integrations.datadog.datadog import DataDogLogger -from ..integrations.datadog.datadog_metrics import DatadogMetricsLogger from ..integrations.datadog.datadog_llm_obs import DataDogLLMObsLogger +from ..integrations.datadog.datadog_metrics import DatadogMetricsLogger from ..integrations.dotprompt import DotpromptManager from ..integrations.dynamodb import DyanmoDBLogger from ..integrations.galileo import GalileoObserve @@ -352,9 +352,9 @@ class Logging(LiteLLMLoggingBaseClass): ) self.function_id = function_id self.streaming_chunks: List[Any] = [] # for generating complete stream response - self.sync_streaming_chunks: List[ - Any - ] = [] # for generating complete stream response + self.sync_streaming_chunks: List[Any] = ( + [] + ) # for generating complete stream response self.log_raw_request_response = log_raw_request_response # Initialize dynamic callbacks @@ -746,9 +746,9 @@ class Logging(LiteLLMLoggingBaseClass): prompt_spec=prompt_spec, dynamic_callback_params=dynamic_callback_params, ): - self.model_call_details[ - "prompt_integration" - ] = logger.__class__.__name__ + self.model_call_details["prompt_integration"] = ( + logger.__class__.__name__ + ) return logger except Exception: # If check fails, continue to next logger @@ -816,9 +816,9 @@ class Logging(LiteLLMLoggingBaseClass): if anthropic_cache_control_logger := AnthropicCacheControlHook.get_custom_logger_for_anthropic_cache_control_hook( non_default_params ): - self.model_call_details[ - "prompt_integration" - ] = anthropic_cache_control_logger.__class__.__name__ + self.model_call_details["prompt_integration"] = ( + anthropic_cache_control_logger.__class__.__name__ + ) return anthropic_cache_control_logger ######################################################### @@ -830,9 +830,9 @@ class Logging(LiteLLMLoggingBaseClass): internal_usage_cache=None, llm_router=None, ) - self.model_call_details[ - "prompt_integration" - ] = vector_store_custom_logger.__class__.__name__ + self.model_call_details["prompt_integration"] = ( + vector_store_custom_logger.__class__.__name__ + ) # Add to global callbacks so post-call hooks are invoked if ( vector_store_custom_logger @@ -892,9 +892,9 @@ class Logging(LiteLLMLoggingBaseClass): model ): # if model name was changes pre-call, overwrite the initial model call name with the new one self.model_call_details["model"] = model - self.model_call_details["litellm_params"][ - "api_base" - ] = self._get_masked_api_base(additional_args.get("api_base", "")) + self.model_call_details["litellm_params"]["api_base"] = ( + self._get_masked_api_base(additional_args.get("api_base", "")) + ) def pre_call(self, input, api_key, model=None, additional_args={}): # noqa: PLR0915 # Log the exact input to the LLM API @@ -923,10 +923,10 @@ class Logging(LiteLLMLoggingBaseClass): try: # [Non-blocking Extra Debug Information in metadata] if turn_off_message_logging is True: - _metadata[ - "raw_request" - ] = "redacted by litellm. \ + _metadata["raw_request"] = ( + "redacted by litellm. \ 'litellm.turn_off_message_logging=True'" + ) else: curl_command = self._get_request_curl_command( api_base=additional_args.get("api_base", ""), @@ -937,34 +937,34 @@ class Logging(LiteLLMLoggingBaseClass): _metadata["raw_request"] = str(curl_command) # split up, so it's easier to parse in the UI - self.model_call_details[ - "raw_request_typed_dict" - ] = RawRequestTypedDict( - raw_request_api_base=str( - additional_args.get("api_base") or "" - ), - raw_request_body=self._get_raw_request_body( - additional_args.get("complete_input_dict", {}) - ), - # NOTE: setting ignore_sensitive_headers to True will cause - # the Authorization header to be leaked when calls to the health - # endpoint are made and fail. - raw_request_headers=self._get_masked_headers( - additional_args.get("headers", {}) or {}, - ), - error=None, + self.model_call_details["raw_request_typed_dict"] = ( + RawRequestTypedDict( + raw_request_api_base=str( + additional_args.get("api_base") or "" + ), + raw_request_body=self._get_raw_request_body( + additional_args.get("complete_input_dict", {}) + ), + # NOTE: setting ignore_sensitive_headers to True will cause + # the Authorization header to be leaked when calls to the health + # endpoint are made and fail. + raw_request_headers=self._get_masked_headers( + additional_args.get("headers", {}) or {}, + ), + error=None, + ) ) except Exception as e: - self.model_call_details[ - "raw_request_typed_dict" - ] = RawRequestTypedDict( - error=str(e), + self.model_call_details["raw_request_typed_dict"] = ( + RawRequestTypedDict( + error=str(e), + ) ) - _metadata[ - "raw_request" - ] = "Unable to Log \ + _metadata["raw_request"] = ( + "Unable to Log \ raw request: {}".format( - str(e) + str(e) + ) ) if getattr(self, "logger_fn", None) and callable(self.logger_fn): try: @@ -1265,13 +1265,13 @@ class Logging(LiteLLMLoggingBaseClass): for callback in callbacks: try: if isinstance(callback, CustomLogger): - response: Optional[ - MCPPostCallResponseObject - ] = await callback.async_post_mcp_tool_call_hook( - kwargs=kwargs, - response_obj=post_mcp_tool_call_response_obj, - start_time=start_time, - end_time=end_time, + response: Optional[MCPPostCallResponseObject] = ( + await callback.async_post_mcp_tool_call_hook( + kwargs=kwargs, + response_obj=post_mcp_tool_call_response_obj, + start_time=start_time, + end_time=end_time, + ) ) ###################################################################### # if any of the callbacks modify the response, use the modified response @@ -1466,9 +1466,9 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.debug( f"response_cost_failure_debug_information: {debug_info}" ) - self.model_call_details[ - "response_cost_failure_debug_information" - ] = debug_info + self.model_call_details["response_cost_failure_debug_information"] = ( + debug_info + ) return None try: @@ -1494,9 +1494,9 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.debug( f"response_cost_failure_debug_information: {debug_info}" ) - self.model_call_details[ - "response_cost_failure_debug_information" - ] = debug_info + self.model_call_details["response_cost_failure_debug_information"] = ( + debug_info + ) return None @@ -1652,10 +1652,8 @@ class Logging(LiteLLMLoggingBaseClass): result=logging_result ) - self.model_call_details[ - "standard_logging_object" - ] = self._build_standard_logging_payload( - logging_result, start_time, end_time + self.model_call_details["standard_logging_object"] = ( + self._build_standard_logging_payload(logging_result, start_time, end_time) ) if ( @@ -1734,9 +1732,9 @@ class Logging(LiteLLMLoggingBaseClass): end_time = datetime.datetime.now() if self.completion_start_time is None: self.completion_start_time = end_time - self.model_call_details[ - "completion_start_time" - ] = self.completion_start_time + self.model_call_details["completion_start_time"] = ( + self.completion_start_time + ) self.model_call_details["log_event_type"] = "successful_api_call" self.model_call_details["end_time"] = end_time @@ -1773,10 +1771,10 @@ class Logging(LiteLLMLoggingBaseClass): end_time=end_time, ) elif isinstance(result, dict) or isinstance(result, list): - self.model_call_details[ - "standard_logging_object" - ] = self._build_standard_logging_payload( - result, start_time, end_time + self.model_call_details["standard_logging_object"] = ( + self._build_standard_logging_payload( + result, start_time, end_time + ) ) if ( standard_logging_payload := self.model_call_details.get( @@ -1785,9 +1783,9 @@ class Logging(LiteLLMLoggingBaseClass): ) is not None: emit_standard_logging_payload(standard_logging_payload) elif standard_logging_object is not None: - self.model_call_details[ - "standard_logging_object" - ] = standard_logging_object + self.model_call_details["standard_logging_object"] = ( + standard_logging_object + ) else: self.model_call_details["response_cost"] = None @@ -1945,17 +1943,17 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.debug( "Logging Details LiteLLM-Success Call streaming complete" ) - self.model_call_details[ - "complete_streaming_response" - ] = complete_streaming_response - self.model_call_details[ - "response_cost" - ] = self._response_cost_calculator(result=complete_streaming_response) + self.model_call_details["complete_streaming_response"] = ( + complete_streaming_response + ) + self.model_call_details["response_cost"] = ( + self._response_cost_calculator(result=complete_streaming_response) + ) ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = self._build_standard_logging_payload( - complete_streaming_response, start_time, end_time + self.model_call_details["standard_logging_object"] = ( + self._build_standard_logging_payload( + complete_streaming_response, start_time, end_time + ) ) if ( standard_logging_payload := self.model_call_details.get( @@ -2289,10 +2287,10 @@ class Logging(LiteLLMLoggingBaseClass): ) else: if self.stream and complete_streaming_response: - self.model_call_details[ - "complete_response" - ] = self.model_call_details.get( - "complete_streaming_response", {} + self.model_call_details["complete_response"] = ( + self.model_call_details.get( + "complete_streaming_response", {} + ) ) result = self.model_call_details["complete_response"] openMeterLogger.log_success_event( @@ -2316,10 +2314,10 @@ class Logging(LiteLLMLoggingBaseClass): ) else: if self.stream and complete_streaming_response: - self.model_call_details[ - "complete_response" - ] = self.model_call_details.get( - "complete_streaming_response", {} + self.model_call_details["complete_response"] = ( + self.model_call_details.get( + "complete_streaming_response", {} + ) ) result = self.model_call_details["complete_response"] @@ -2458,9 +2456,9 @@ class Logging(LiteLLMLoggingBaseClass): if complete_streaming_response is not None: print_verbose("Async success callbacks: Got a complete streaming response") - self.model_call_details[ - "async_complete_streaming_response" - ] = complete_streaming_response + self.model_call_details["async_complete_streaming_response"] = ( + complete_streaming_response + ) try: if self.model_call_details.get("cache_hit", False) is True: @@ -2471,10 +2469,10 @@ class Logging(LiteLLMLoggingBaseClass): model_call_details=self.model_call_details ) # base_model defaults to None if not set on model_info - self.model_call_details[ - "response_cost" - ] = self._response_cost_calculator( - result=complete_streaming_response + self.model_call_details["response_cost"] = ( + self._response_cost_calculator( + result=complete_streaming_response + ) ) verbose_logger.debug( @@ -2487,10 +2485,10 @@ class Logging(LiteLLMLoggingBaseClass): self.model_call_details["response_cost"] = None ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = self._build_standard_logging_payload( - complete_streaming_response, start_time, end_time + self.model_call_details["standard_logging_object"] = ( + self._build_standard_logging_payload( + complete_streaming_response, start_time, end_time + ) ) # print standard logging payload @@ -2517,10 +2515,8 @@ class Logging(LiteLLMLoggingBaseClass): # _success_handler_helper_fn if self.model_call_details.get("standard_logging_object") is None: ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = self._build_standard_logging_payload( - result, start_time, end_time + self.model_call_details["standard_logging_object"] = ( + self._build_standard_logging_payload(result, start_time, end_time) ) # print standard logging payload @@ -2764,18 +2760,18 @@ class Logging(LiteLLMLoggingBaseClass): ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = get_standard_logging_object_payload( - kwargs=self.model_call_details, - init_response_obj={}, - start_time=start_time, - end_time=end_time, - logging_obj=self, - status="failure", - error_str=str(exception), - original_exception=exception, - standard_built_in_tools_params=self.standard_built_in_tools_params, + self.model_call_details["standard_logging_object"] = ( + get_standard_logging_object_payload( + kwargs=self.model_call_details, + init_response_obj={}, + start_time=start_time, + end_time=end_time, + logging_obj=self, + status="failure", + error_str=str(exception), + original_exception=exception, + standard_built_in_tools_params=self.standard_built_in_tools_params, + ) ) return start_time, end_time @@ -3739,9 +3735,9 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 service_name=arize_config.project_name, ) - os.environ[ - "OTEL_EXPORTER_OTLP_TRACES_HEADERS" - ] = f"space_id={arize_config.space_key or arize_config.space_id},api_key={arize_config.api_key}" + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = ( + f"space_id={arize_config.space_key or arize_config.space_id},api_key={arize_config.api_key}" + ) for callback in _in_memory_loggers: if ( isinstance(callback, ArizeLogger) @@ -3767,13 +3763,13 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 existing_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES", "") # Add openinference.project.name attribute if existing_attrs: - os.environ[ - "OTEL_RESOURCE_ATTRIBUTES" - ] = f"{existing_attrs},openinference.project.name={arize_phoenix_config.project_name}" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + f"{existing_attrs},openinference.project.name={arize_phoenix_config.project_name}" + ) else: - os.environ[ - "OTEL_RESOURCE_ATTRIBUTES" - ] = f"openinference.project.name={arize_phoenix_config.project_name}" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + f"openinference.project.name={arize_phoenix_config.project_name}" + ) # Set Phoenix project name from environment variable phoenix_project_name = os.environ.get("PHOENIX_PROJECT_NAME", None) @@ -3781,19 +3777,19 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 existing_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES", "") # Add openinference.project.name attribute if existing_attrs: - os.environ[ - "OTEL_RESOURCE_ATTRIBUTES" - ] = f"{existing_attrs},openinference.project.name={phoenix_project_name}" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + f"{existing_attrs},openinference.project.name={phoenix_project_name}" + ) else: - os.environ[ - "OTEL_RESOURCE_ATTRIBUTES" - ] = f"openinference.project.name={phoenix_project_name}" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + f"openinference.project.name={phoenix_project_name}" + ) # auth can be disabled on local deployments of arize phoenix if arize_phoenix_config.otlp_auth_headers is not None: - os.environ[ - "OTEL_EXPORTER_OTLP_TRACES_HEADERS" - ] = arize_phoenix_config.otlp_auth_headers + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = ( + arize_phoenix_config.otlp_auth_headers + ) for callback in _in_memory_loggers: if ( @@ -3969,9 +3965,9 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 exporter="otlp_http", endpoint="https://langtrace.ai/api/trace", ) - os.environ[ - "OTEL_EXPORTER_OTLP_TRACES_HEADERS" - ] = f"api_key={os.getenv('LANGTRACE_API_KEY')}" + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = ( + f"api_key={os.getenv('LANGTRACE_API_KEY')}" + ) for callback in _in_memory_loggers: if ( isinstance(callback, OpenTelemetry) @@ -4204,8 +4200,7 @@ def _maybe_auto_initialize_arize_phoenix(_in_memory_loggers: list) -> None: litellm.logging_callback_manager.add_litellm_callback(phoenix_logger) verbose_logger.info( - "Auto-initialized Arize Phoenix logger alongside otel " - "(endpoint=%s)", + "Auto-initialized Arize Phoenix logger alongside otel " "(endpoint=%s)", arize_phoenix_config.endpoint, ) except Exception as e: @@ -4768,9 +4763,11 @@ class StandardLoggingPayloadSetup: ).model_dump() if isinstance(_raw, dict): if ResponseAPILoggingUtils._is_response_api_usage(_raw): - return ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( - _raw - ).model_dump() + return ( + ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( + _raw + ).model_dump() + ) return _raw if isinstance(_raw, Usage): return _raw.model_dump() @@ -4884,10 +4881,10 @@ class StandardLoggingPayloadSetup: for key in StandardLoggingHiddenParams.__annotations__.keys(): if key in hidden_params: if key == "additional_headers": - clean_hidden_params[ - "additional_headers" - ] = StandardLoggingPayloadSetup.get_additional_headers( - hidden_params[key] + clean_hidden_params["additional_headers"] = ( + StandardLoggingPayloadSetup.get_additional_headers( + hidden_params[key] + ) ) else: clean_hidden_params[key] = hidden_params[key] # type: ignore @@ -5039,14 +5036,22 @@ class StandardLoggingPayloadSetup: dynamic_litellm_session_id = litellm_params.get("litellm_session_id") dynamic_litellm_trace_id = litellm_params.get("litellm_trace_id") + # Note: we recommend using `litellm_session_id` for session tracking # `litellm_trace_id` is an internal litellm param if dynamic_litellm_session_id: return str(dynamic_litellm_session_id) elif dynamic_litellm_trace_id: return str(dynamic_litellm_trace_id) - else: - return logging_obj.litellm_trace_id + # Fallback: use metadata.session_id or metadata.trace_id for call chaining + metadata = litellm_params.get("metadata") or {} + metadata_session_id = metadata.get("session_id") + metadata_trace_id = metadata.get("trace_id") + if metadata_session_id: + return str(metadata_session_id) + if metadata_trace_id: + return str(metadata_trace_id) + return logging_obj.litellm_trace_id @staticmethod def _get_user_agent_tags(proxy_server_request: dict) -> Optional[List[str]]: @@ -5502,9 +5507,9 @@ def scrub_sensitive_keys_in_metadata(litellm_params: Optional[dict]): ): for k, v in metadata["user_api_key_metadata"].items(): if k == "logging": # prevent logging user logging keys - cleaned_user_api_key_metadata[ - k - ] = "scrubbed_by_litellm_for_sensitive_keys" + cleaned_user_api_key_metadata[k] = ( + "scrubbed_by_litellm_for_sensitive_keys" + ) else: cleaned_user_api_key_metadata[k] = v @@ -5616,4 +5621,3 @@ def create_dummy_standard_logging_payload() -> StandardLoggingPayload: model_parameters={"stream": True}, hidden_params=hidden_params, ) - diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 5b3d5bd60e2..cdb26acb658 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -5,7 +5,6 @@ LiteLLM MCP Server Routes import asyncio import contextlib - import traceback import uuid from datetime import datetime @@ -44,7 +43,10 @@ from litellm.proxy._experimental.mcp_server.utils import ( ) from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.auth.ip_address_utils import IPAddressUtils -from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup +from litellm.proxy.litellm_pre_call_utils import ( + LiteLLMProxyRequestSetup, + get_chain_id_from_headers, +) from litellm.types.mcp import MCPAuth from litellm.types.mcp_server.mcp_server_manager import MCPInfo, MCPServer from litellm.types.utils import CallTypes, StandardLoggingMCPToolCall @@ -331,6 +333,11 @@ if MCP_AVAILABLE: try: # Create a body date for logging body_data = {"name": name, "arguments": arguments} + # Set trace/session id from raw_headers so spend logs and logging_obj stay consistent (same as A2A) + chain_id = get_chain_id_from_headers(raw_headers) + if chain_id: + body_data["litellm_trace_id"] = chain_id + body_data["litellm_session_id"] = chain_id request = Request( scope={ @@ -884,6 +891,10 @@ if MCP_AVAILABLE: # This is intentionally minimal: only async_success_handler / post_call_failure_hook rules_obj = Rules() list_tools_call_id = str(uuid.uuid4()) + # Derive trace_id from raw_headers when not explicitly passed (same as A2A / MCP call_tool) + effective_litellm_trace_id = litellm_trace_id or get_chain_id_from_headers( + raw_headers + ) spend_logs_metadata: Dict[str, Any] = { "mcp_operation": "list_tools", } @@ -896,7 +907,7 @@ if MCP_AVAILABLE: "model": "MCP: list_tools", "call_type": CallTypes.list_mcp_tools.value, "litellm_call_id": list_tools_call_id, - "litellm_trace_id": litellm_trace_id, + "litellm_trace_id": effective_litellm_trace_id, "metadata": { "spend_logs_metadata": spend_logs_metadata, }, diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404/index.html similarity index 100% rename from litellm/proxy/_experimental/out/404.html rename to litellm/proxy/_experimental/out/404/index.html diff --git a/litellm/proxy/_experimental/out/_not-found.html b/litellm/proxy/_experimental/out/_not-found/index.html similarity index 100% rename from litellm/proxy/_experimental/out/_not-found.html rename to litellm/proxy/_experimental/out/_not-found/index.html diff --git a/litellm/proxy/_experimental/out/api-reference.html b/litellm/proxy/_experimental/out/api-reference/index.html similarity index 100% rename from litellm/proxy/_experimental/out/api-reference.html rename to litellm/proxy/_experimental/out/api-reference/index.html diff --git a/litellm/proxy/_experimental/out/experimental/api-playground.html b/litellm/proxy/_experimental/out/experimental/api-playground/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/api-playground.html rename to litellm/proxy/_experimental/out/experimental/api-playground/index.html diff --git a/litellm/proxy/_experimental/out/experimental/budgets.html b/litellm/proxy/_experimental/out/experimental/budgets/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/budgets.html rename to litellm/proxy/_experimental/out/experimental/budgets/index.html diff --git a/litellm/proxy/_experimental/out/experimental/caching.html b/litellm/proxy/_experimental/out/experimental/caching/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/caching.html rename to litellm/proxy/_experimental/out/experimental/caching/index.html diff --git a/litellm/proxy/_experimental/out/experimental/claude-code-plugins.html b/litellm/proxy/_experimental/out/experimental/claude-code-plugins/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/claude-code-plugins.html rename to litellm/proxy/_experimental/out/experimental/claude-code-plugins/index.html diff --git a/litellm/proxy/_experimental/out/experimental/old-usage.html b/litellm/proxy/_experimental/out/experimental/old-usage/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/old-usage.html rename to litellm/proxy/_experimental/out/experimental/old-usage/index.html diff --git a/litellm/proxy/_experimental/out/experimental/prompts.html b/litellm/proxy/_experimental/out/experimental/prompts/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/prompts.html rename to litellm/proxy/_experimental/out/experimental/prompts/index.html diff --git a/litellm/proxy/_experimental/out/experimental/tag-management.html b/litellm/proxy/_experimental/out/experimental/tag-management/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/tag-management.html rename to litellm/proxy/_experimental/out/experimental/tag-management/index.html diff --git a/litellm/proxy/_experimental/out/guardrails.html b/litellm/proxy/_experimental/out/guardrails/index.html similarity index 100% rename from litellm/proxy/_experimental/out/guardrails.html rename to litellm/proxy/_experimental/out/guardrails/index.html diff --git a/litellm/proxy/_experimental/out/login.html b/litellm/proxy/_experimental/out/login/index.html similarity index 100% rename from litellm/proxy/_experimental/out/login.html rename to litellm/proxy/_experimental/out/login/index.html diff --git a/litellm/proxy/_experimental/out/logs.html b/litellm/proxy/_experimental/out/logs/index.html similarity index 100% rename from litellm/proxy/_experimental/out/logs.html rename to litellm/proxy/_experimental/out/logs/index.html diff --git a/litellm/proxy/_experimental/out/mcp/oauth/callback.html b/litellm/proxy/_experimental/out/mcp/oauth/callback/index.html similarity index 100% rename from litellm/proxy/_experimental/out/mcp/oauth/callback.html rename to litellm/proxy/_experimental/out/mcp/oauth/callback/index.html diff --git a/litellm/proxy/_experimental/out/model-hub.html b/litellm/proxy/_experimental/out/model-hub/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model-hub.html rename to litellm/proxy/_experimental/out/model-hub/index.html diff --git a/litellm/proxy/_experimental/out/model_hub.html b/litellm/proxy/_experimental/out/model_hub/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub.html rename to litellm/proxy/_experimental/out/model_hub/index.html diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub_table.html rename to litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm/proxy/_experimental/out/models-and-endpoints.html b/litellm/proxy/_experimental/out/models-and-endpoints/index.html similarity index 100% rename from litellm/proxy/_experimental/out/models-and-endpoints.html rename to litellm/proxy/_experimental/out/models-and-endpoints/index.html diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding/index.html similarity index 100% rename from litellm/proxy/_experimental/out/onboarding.html rename to litellm/proxy/_experimental/out/onboarding/index.html diff --git a/litellm/proxy/_experimental/out/organizations.html b/litellm/proxy/_experimental/out/organizations/index.html similarity index 100% rename from litellm/proxy/_experimental/out/organizations.html rename to litellm/proxy/_experimental/out/organizations/index.html diff --git a/litellm/proxy/_experimental/out/playground.html b/litellm/proxy/_experimental/out/playground/index.html similarity index 100% rename from litellm/proxy/_experimental/out/playground.html rename to litellm/proxy/_experimental/out/playground/index.html diff --git a/litellm/proxy/_experimental/out/policies.html b/litellm/proxy/_experimental/out/policies/index.html similarity index 100% rename from litellm/proxy/_experimental/out/policies.html rename to litellm/proxy/_experimental/out/policies/index.html diff --git a/litellm/proxy/_experimental/out/settings/admin-settings.html b/litellm/proxy/_experimental/out/settings/admin-settings/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/admin-settings.html rename to litellm/proxy/_experimental/out/settings/admin-settings/index.html diff --git a/litellm/proxy/_experimental/out/settings/logging-and-alerts.html b/litellm/proxy/_experimental/out/settings/logging-and-alerts/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/logging-and-alerts.html rename to litellm/proxy/_experimental/out/settings/logging-and-alerts/index.html diff --git a/litellm/proxy/_experimental/out/settings/router-settings.html b/litellm/proxy/_experimental/out/settings/router-settings/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/router-settings.html rename to litellm/proxy/_experimental/out/settings/router-settings/index.html diff --git a/litellm/proxy/_experimental/out/settings/ui-theme.html b/litellm/proxy/_experimental/out/settings/ui-theme/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/ui-theme.html rename to litellm/proxy/_experimental/out/settings/ui-theme/index.html diff --git a/litellm/proxy/_experimental/out/teams.html b/litellm/proxy/_experimental/out/teams/index.html similarity index 100% rename from litellm/proxy/_experimental/out/teams.html rename to litellm/proxy/_experimental/out/teams/index.html diff --git a/litellm/proxy/_experimental/out/test-key.html b/litellm/proxy/_experimental/out/test-key/index.html similarity index 100% rename from litellm/proxy/_experimental/out/test-key.html rename to litellm/proxy/_experimental/out/test-key/index.html diff --git a/litellm/proxy/_experimental/out/tools/mcp-servers.html b/litellm/proxy/_experimental/out/tools/mcp-servers/index.html similarity index 100% rename from litellm/proxy/_experimental/out/tools/mcp-servers.html rename to litellm/proxy/_experimental/out/tools/mcp-servers/index.html diff --git a/litellm/proxy/_experimental/out/tools/vector-stores.html b/litellm/proxy/_experimental/out/tools/vector-stores/index.html similarity index 100% rename from litellm/proxy/_experimental/out/tools/vector-stores.html rename to litellm/proxy/_experimental/out/tools/vector-stores/index.html diff --git a/litellm/proxy/_experimental/out/usage.html b/litellm/proxy/_experimental/out/usage/index.html similarity index 100% rename from litellm/proxy/_experimental/out/usage.html rename to litellm/proxy/_experimental/out/usage/index.html diff --git a/litellm/proxy/_experimental/out/users.html b/litellm/proxy/_experimental/out/users/index.html similarity index 100% rename from litellm/proxy/_experimental/out/users.html rename to litellm/proxy/_experimental/out/users/index.html diff --git a/litellm/proxy/_experimental/out/virtual-keys.html b/litellm/proxy/_experimental/out/virtual-keys/index.html similarity index 100% rename from litellm/proxy/_experimental/out/virtual-keys.html rename to litellm/proxy/_experimental/out/virtual-keys/index.html diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py index 7f30277ebca..6bcee14f29e 100644 --- a/litellm/proxy/agent_endpoints/a2a_endpoints.py +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -69,6 +69,7 @@ async def _handle_stream_message( from litellm.a2a_protocol.main import A2A_SDK_AVAILABLE if not A2A_SDK_AVAILABLE: + async def _error_stream(): yield json.dumps( { @@ -106,7 +107,12 @@ async def _handle_stream_message( proxy_server_request=proxy_server_request, ) - if use_proxy_hooks and user_api_key_dict is not None and request_data is not None and proxy_logging_obj is not None: + if ( + use_proxy_hooks + and user_api_key_dict is not None + and request_data is not None + and proxy_logging_obj is not None + ): from litellm.proxy.common_request_processing import ( ProxyBaseLLMRequestProcessing, ) @@ -119,20 +125,27 @@ async def _handle_stream_message( return json.dumps(obj) + "\n" def _ndjson_error(proxy_exc: Any) -> str: - return json.dumps( - { - "jsonrpc": "2.0", - "id": request_id, - "error": { - "code": -32603, - "message": getattr( - proxy_exc, "message", f"Streaming error: {proxy_exc!s}" - ), - }, - } - ) + "\n" + return ( + json.dumps( + { + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": -32603, + "message": getattr( + proxy_exc, + "message", + f"Streaming error: {proxy_exc!s}", + ), + }, + } + ) + + "\n" + ) - async for line in ProxyBaseLLMRequestProcessing.async_streaming_data_generator( + async for ( + line + ) in ProxyBaseLLMRequestProcessing.async_streaming_data_generator( response=a2a_stream, user_api_key_dict=user_api_key_dict, request_data=request_data, @@ -151,7 +164,12 @@ async def _handle_stream_message( yield json.dumps(chunk) + "\n" except Exception as e: verbose_proxy_logger.exception(f"Error streaming A2A response: {e}") - if use_proxy_hooks and proxy_logging_obj is not None and user_api_key_dict is not None and request_data is not None: + if ( + use_proxy_hooks + and proxy_logging_obj is not None + and user_api_key_dict is not None + and request_data is not None + ): transformed_exception = await proxy_logging_obj.post_call_failure_hook( user_api_key_dict=user_api_key_dict, original_exception=e, @@ -382,6 +400,7 @@ async def invoke_agent_a2a( agent_id=agent.agent_id, metadata=data.get("metadata", {}), proxy_server_request=data.get("proxy_server_request"), + litellm_logging_obj=logging_obj, ) response = await proxy_logging_obj.post_call_success_hook( diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index c6a709534e1..4c866a24991 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -1624,11 +1624,11 @@ def _build_field_dict( # Determine the field type from annotation field_type = _get_field_type_from_annotation(field_annotation) - # Check for custom UI type override (ui_type preferred; "type" leaks into OpenAPI and breaks schema) - field_json_schema_extra = getattr(field, "json_schema_extra", {}) or {} + # Check for custom UI type override + field_json_schema_extra = getattr(field, "json_schema_extra", {}) if field_json_schema_extra and "ui_type" in field_json_schema_extra: - ut = field_json_schema_extra["ui_type"] - field_type = ut if isinstance(ut, str) else getattr(ut, "value", ut) + ui_type = field_json_schema_extra["ui_type"] + field_type = ui_type.value if hasattr(ui_type, "value") else ui_type elif field_json_schema_extra and "type" in field_json_schema_extra: field_type = field_json_schema_extra["type"] diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 92bf035a986..68f1ab114bf 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -89,6 +89,25 @@ def _get_metadata_variable_name(request: Request) -> str: return "metadata" +def get_chain_id_from_headers(headers: Optional[Dict[str, str]]) -> Optional[str]: + """ + Extract chain id for call chaining from request headers. + + x-litellm-trace-id and x-litellm-session-id are interchangeable; when both + are present, x-litellm-trace-id takes precedence. Header keys are matched + case-insensitively so this works with raw header dicts from any transport. + + Used by MCP (and other paths that have raw_headers but no Request) to set + litellm_trace_id/litellm_session_id for spend logs and logging consistency. + """ + if not headers: + return None + normalized = {k.lower(): v for k, v in headers.items() if isinstance(k, str)} + return normalized.get("x-litellm-trace-id") or normalized.get( + "x-litellm-session-id" + ) + + def safe_add_api_version_from_query_params(data: dict, request: Request): try: if hasattr(request, "query_params"): @@ -177,12 +196,12 @@ def _get_dynamic_logging_metadata( user_api_key_dict: UserAPIKeyAuth, proxy_config: ProxyConfig ) -> Optional[TeamCallbackMetadata]: callback_settings_obj: Optional[TeamCallbackMetadata] = None - key_dynamic_logging_settings: Optional[ - dict - ] = KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict) - team_dynamic_logging_settings: Optional[ - dict - ] = KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict) + key_dynamic_logging_settings: Optional[dict] = ( + KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict) + ) + team_dynamic_logging_settings: Optional[dict] = ( + KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict) + ) ######################################################################################### # Key-based callbacks ######################################################################################### @@ -576,9 +595,13 @@ class LiteLLMProxyRequestSetup: ######################################################################################### # Finally update the requests metadata with the `metadata_from_headers` ######################################################################################### + agent_id_from_header = headers.get("x-litellm-agent-id") - trace_id_from_header = headers.get("x-litellm-trace-id") - session_id_from_header = headers.get("x-litellm-session-id") + # x-litellm-trace-id and x-litellm-session-id are interchangeable for call chaining + chain_id = headers.get("x-litellm-trace-id") or headers.get( + "x-litellm-session-id" + ) + if agent_id_from_header: metadata_from_headers["agent_id"] = agent_id_from_header @@ -586,16 +609,13 @@ class LiteLLMProxyRequestSetup: f"Extracted agent_id from header: {agent_id_from_header}" ) - if trace_id_from_header: - metadata_from_headers["trace_id"] = trace_id_from_header + if chain_id: + metadata_from_headers["trace_id"] = chain_id + metadata_from_headers["session_id"] = chain_id + data["litellm_session_id"] = chain_id + data["litellm_trace_id"] = chain_id verbose_proxy_logger.debug( - f"Extracted trace_id from header: {trace_id_from_header}" - ) - - if session_id_from_header: - metadata_from_headers["session_id"] = session_id_from_header - verbose_proxy_logger.debug( - f"Extracted session_id from header: {session_id_from_header}" + f"Extracted chain_id from header (trace-id/session-id): {chain_id}" ) if isinstance(data[_metadata_variable_name], dict): @@ -702,11 +722,11 @@ class LiteLLMProxyRequestSetup: ## KEY-LEVEL SPEND LOGS / TAGS if "tags" in key_metadata and key_metadata["tags"] is not None: - data[_metadata_variable_name][ - "tags" - ] = LiteLLMProxyRequestSetup._merge_tags( - request_tags=data[_metadata_variable_name].get("tags"), - tags_to_add=key_metadata["tags"], + data[_metadata_variable_name]["tags"] = ( + LiteLLMProxyRequestSetup._merge_tags( + request_tags=data[_metadata_variable_name].get("tags"), + tags_to_add=key_metadata["tags"], + ) ) if "disable_global_guardrails" in key_metadata and isinstance( key_metadata["disable_global_guardrails"], bool @@ -839,14 +859,9 @@ async def add_litellm_data_to_request( # noqa: PLR0915 """ from litellm.proxy.proxy_server import llm_router, premium_user - from litellm.types.proxy.litellm_pre_call_utils import ( - RedactedDict, - SecretFields, - ) + from litellm.types.proxy.litellm_pre_call_utils import RedactedDict, SecretFields - _raw_headers: Dict[str, str] = RedactedDict( - _safe_get_request_headers(request) - ) + _raw_headers: Dict[str, str] = RedactedDict(_safe_get_request_headers(request)) forward_llm_auth = False if general_settings: @@ -986,9 +1001,9 @@ async def add_litellm_data_to_request( # noqa: PLR0915 data[_metadata_variable_name]["litellm_api_version"] = version if general_settings is not None: - data[_metadata_variable_name][ - "global_max_parallel_requests" - ] = general_settings.get("global_max_parallel_requests", None) + data[_metadata_variable_name]["global_max_parallel_requests"] = ( + general_settings.get("global_max_parallel_requests", None) + ) ### KEY-LEVEL Controls key_metadata = user_api_key_dict.metadata diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 31615a768d7..131841f7b59 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -11,26 +11,21 @@ from pydantic import BaseModel import litellm from litellm._logging import verbose_proxy_logger -from litellm.constants import ( - MAX_STRING_LENGTH_PROMPT_IN_DB as DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB, -) +from litellm.constants import \ + MAX_STRING_LENGTH_PROMPT_IN_DB as DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB from litellm.constants import REDACTED_BY_LITELM_STRING from litellm.litellm_core_utils.core_helpers import ( - get_litellm_metadata_from_kwargs, - reconstruct_model_name, -) + get_litellm_metadata_from_kwargs, reconstruct_model_name) from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload from litellm.proxy.utils import PrismaClient, hash_token -from litellm.types.utils import ( - CostBreakdown, - StandardLoggingGuardrailInformation, - StandardLoggingMCPToolCall, - StandardLoggingModelInformation, - StandardLoggingPayload, - StandardLoggingVectorStoreRequest, - VectorStoreSearchResponse, -) +from litellm.types.utils import (CostBreakdown, + StandardLoggingGuardrailInformation, + StandardLoggingMCPToolCall, + StandardLoggingModelInformation, + StandardLoggingPayload, + StandardLoggingVectorStoreRequest, + VectorStoreSearchResponse) from litellm.utils import get_end_user_id_for_cost_tracking @@ -116,16 +111,15 @@ def _get_spend_logs_metadata( # Filter the metadata dictionary to include only the specified keys clean_metadata = SpendLogsMetadata( **{ # type: ignore - key: metadata.get(key) - for key in SpendLogsMetadata.__annotations__.keys() + key: metadata.get(key) for key in SpendLogsMetadata.__annotations__.keys() } ) clean_metadata["applied_guardrails"] = applied_guardrails clean_metadata["batch_models"] = batch_models clean_metadata["mcp_tool_call_metadata"] = mcp_tool_call_metadata - clean_metadata[ - "vector_store_request_metadata" - ] = _get_vector_store_request_for_spend_logs_payload(vector_store_request_metadata) + clean_metadata["vector_store_request_metadata"] = ( + _get_vector_store_request_for_spend_logs_payload(vector_store_request_metadata) + ) clean_metadata["guardrail_information"] = guardrail_information clean_metadata["usage_object"] = usage_object clean_metadata["model_map_information"] = model_map_information @@ -372,9 +366,11 @@ def get_logging_payload( # noqa: PLR0915 guardrail_information=( standard_logging_payload.get("guardrail_information", None) if standard_logging_payload is not None - else metadata.get("standard_logging_guardrail_information", None) - if metadata is not None - else None + else ( + metadata.get("standard_logging_guardrail_information", None) + if metadata is not None + else None + ) ), cold_storage_object_key=( standard_logging_payload["metadata"].get("cold_storage_object_key", None) @@ -501,6 +497,7 @@ def _get_session_id_for_spend_log( """ from litellm._uuid import uuid + if ( standard_logging_payload is not None and standard_logging_payload.get("trace_id") is not None @@ -515,9 +512,7 @@ def _get_session_id_for_spend_log( return str(uuid.uuid4()) -def _get_request_duration_ms( - start_time: datetime, end_time: datetime -) -> Optional[int]: +def _get_request_duration_ms(start_time: datetime, end_time: datetime) -> Optional[int]: """Compute request duration in milliseconds from start and end times.""" try: return int((end_time - start_time).total_seconds() * 1000) @@ -709,20 +704,20 @@ def _convert_to_json_serializable_dict( if max_depth <= 0: # Return a placeholder if max depth is exceeded return "" - + if visited is None: visited = set() - + # Get the object's memory address to track visited objects obj_id = id(obj) if obj_id in visited: # Circular reference detected, return placeholder return "" - + # Only track mutable objects (dict, list, objects with __dict__) if isinstance(obj, (dict, list)) or hasattr(obj, "__dict__"): visited.add(obj_id) - + try: if isinstance(obj, BaseModel): # Use Pydantic's model_dump() instead of pickle @@ -741,7 +736,9 @@ def _convert_to_json_serializable_dict( ] elif hasattr(obj, "__dict__"): # Handle objects with __dict__ attribute - return _convert_to_json_serializable_dict(obj.__dict__, visited, max_depth - 1) + return _convert_to_json_serializable_dict( + obj.__dict__, visited, max_depth - 1 + ) else: # Primitives (str, int, float, bool, None) pass through return obj @@ -777,9 +774,7 @@ def _get_proxy_server_request_for_spend_logs_payload( # Apply message redaction if turn_off_message_logging is enabled if kwargs is not None: from litellm.litellm_core_utils.redact_messages import ( - perform_redaction, - should_redact_message_logging, - ) + perform_redaction, should_redact_message_logging) # Build model_call_details dict to check redaction settings model_call_details = { @@ -788,12 +783,12 @@ def _get_proxy_server_request_for_spend_logs_payload( "standard_callback_dynamic_params" ), } - + # If redaction is enabled, convert to serializable dict before redacting if should_redact_message_logging(model_call_details=model_call_details): _request_body = _convert_to_json_serializable_dict(_request_body) perform_redaction(model_call_details=_request_body, result=None) - + _request_body = _sanitize_request_body_for_spend_logs_payload(_request_body) _request_body_json_str = json.dumps(_request_body, default=str) return _request_body_json_str @@ -845,10 +840,8 @@ def _get_response_for_spend_logs_payload( # Apply message redaction if turn_off_message_logging is enabled if kwargs is not None: from litellm.litellm_core_utils.redact_messages import ( - perform_redaction, - should_redact_message_logging, - ) - + perform_redaction, should_redact_message_logging) + litellm_params = kwargs.get("litellm_params", {}) model_call_details = { "litellm_params": litellm_params, @@ -856,11 +849,13 @@ def _get_response_for_spend_logs_payload( "standard_callback_dynamic_params" ), } - + # If redaction is enabled, convert to serializable dict before redacting if should_redact_message_logging(model_call_details=model_call_details): response_obj = _convert_to_json_serializable_dict(response_obj) - response_obj = perform_redaction(model_call_details={}, result=response_obj) + response_obj = perform_redaction( + model_call_details={}, result=response_obj + ) sanitized_wrapper = _sanitize_request_body_for_spend_logs_payload( {"response": response_obj} @@ -882,7 +877,7 @@ def _should_store_prompts_and_responses_in_spend_logs() -> bool: # Check general_settings (from DB or proxy_config.yaml) store_prompts_value = general_settings.get("store_prompts_in_spend_logs") - + # Normalize case: handle True/true/TRUE, False/false/FALSE, None/null if store_prompts_value is True: return True @@ -890,7 +885,7 @@ def _should_store_prompts_and_responses_in_spend_logs() -> bool: # Case-insensitive string comparison if store_prompts_value.lower() == "true": return True - + # Also check environment variable return get_secret_bool("STORE_PROMPTS_IN_SPEND_LOGS") is True diff --git a/litellm/utils.py b/litellm/utils.py index d192609eead..ad1eb7aeceb 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1454,10 +1454,12 @@ def client(original_function): # noqa: PLR0915 logging_obj, kwargs = function_setup( original_function.__name__, rules_obj, start_time, *args, **kwargs ) - + # Type assertion: logging_obj is guaranteed to be non-None after function_setup - assert logging_obj is not None, "logging_obj should not be None after function_setup" - + assert ( + logging_obj is not None + ), "logging_obj should not be None after function_setup" + ## LOAD CREDENTIALS load_credentials_from_list(kwargs) kwargs["litellm_logging_obj"] = logging_obj @@ -1753,7 +1755,9 @@ def client(original_function): # noqa: PLR0915 print_args_passed_to_litellm(original_function, args, kwargs) start_time = datetime.datetime.now() result = None - _update_response_metadata = getattr(sys.modules[__name__], "update_response_metadata") + _update_response_metadata = getattr( + sys.modules[__name__], "update_response_metadata" + ) logging_obj: Optional[LiteLLMLoggingObject] = kwargs.get( "litellm_logging_obj", None ) @@ -1776,9 +1780,11 @@ def client(original_function): # noqa: PLR0915 logging_obj, kwargs = function_setup( original_function.__name__, rules_obj, start_time, *args, **kwargs ) - + # Type assertion: logging_obj is guaranteed to be non-None after function_setup - assert logging_obj is not None, "logging_obj should not be None after function_setup" + assert ( + logging_obj is not None + ), "logging_obj should not be None after function_setup" modified_kwargs = await async_pre_call_deployment_hook(kwargs, call_type) if modified_kwargs is not None: @@ -1861,6 +1867,7 @@ def client(original_function): # noqa: PLR0915 # MODEL CALL result = await original_function(*args, **kwargs) end_time = datetime.datetime.now() + if _is_streaming_request( kwargs=kwargs, call_type=call_type, @@ -2082,12 +2089,14 @@ def _is_async_request( return False -_STREAMING_CALL_TYPES = frozenset({ - CallTypes.generate_content_stream, - CallTypes.agenerate_content_stream, - CallTypes.generate_content_stream.value, - CallTypes.agenerate_content_stream.value, -}) +_STREAMING_CALL_TYPES = frozenset( + { + CallTypes.generate_content_stream, + CallTypes.agenerate_content_stream, + CallTypes.generate_content_stream.value, + CallTypes.agenerate_content_stream.value, + } +) def _is_streaming_request( @@ -2181,7 +2190,7 @@ def encode(model="", text="", custom_tokenizer: Optional[dict] = None): # Normalize: HuggingFace Tokenizer.encode() returns an Encoding object; # extract .ids so the return type is always List[int]. if hasattr(enc, "ids"): - return enc.ids + return enc.ids # type: ignore return enc @@ -5836,7 +5845,7 @@ def get_model_info( _model_info[key] = value # type: ignore # if verbose_logger.isEnabledFor(logging.DEBUG): - # verbose_logger.debug(f"model_info: {_model_info}") + # verbose_logger.debug(f"model_info: {_model_info}") returned_model_info = ModelInfo( **_model_info, supported_openai_params=supported_openai_params @@ -6179,8 +6188,10 @@ def validate_environment( # noqa: PLR0915 "AWS_ROLE_ARN" in os.environ or "AWS_PROFILE" in os.environ or "AWS_WEB_IDENTITY_TOKEN_FILE" in os.environ - or "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" in os.environ # ECS task role - or "AWS_CONTAINER_CREDENTIALS_FULL_URI" in os.environ # ECS/Fargate full URI credential delivery + or "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" + in os.environ # ECS task role + or "AWS_CONTAINER_CREDENTIALS_FULL_URI" + in os.environ # ECS/Fargate full URI credential delivery ): keys_in_environment = True else: @@ -7386,7 +7397,9 @@ class ModelResponseIterator: if convert_to_delta is True: _stream_response = ModelResponseStream() _stream_response.choices[0].delta.content = model_response.choices[0].message.content # type: ignore - self.model_response: Union[ModelResponse, ModelResponseStream] = _stream_response + self.model_response: Union[ModelResponse, ModelResponseStream] = ( + _stream_response + ) else: self.model_response = model_response self.is_done = False @@ -7457,13 +7470,13 @@ def is_cached_message(message: AllMessageValues) -> bool: Used for anthropic/gemini context caching. Follows the anthropic format {"cache_control": {"type": "ephemeral"}} - + Can be disabled globally by setting litellm.disable_anthropic_gemini_context_caching_transform = True """ # Check if context caching is disabled globally if litellm.disable_anthropic_gemini_context_caching_transform is True: return False - + if "content" not in message: return False @@ -7980,6 +7993,7 @@ class ProviderConfigManager: def _get_azure_ai_config(model: str) -> BaseConfig: """Get Azure AI config based on model type.""" from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo + return AzureFoundryModelInfo.get_azure_ai_config_for_model(model) @staticmethod diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py index 62a6e777b0d..ca224726361 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py @@ -1112,6 +1112,47 @@ async def test_get_guardrail_info_endpoint_db_guardrail(mocker): assert result.guardrail_definition_location == "db" +class TestBuildFieldDict: + """Test _build_field_dict handles both enum and string ui_type values.""" + + def test_build_field_dict_with_string_ui_type(self): + """Test that _build_field_dict works when ui_type is a plain string (e.g. BlockCodeExecutionGuardrailConfigModel).""" + from unittest.mock import MagicMock + + from litellm.proxy.guardrails.guardrail_endpoints import _build_field_dict + + field = MagicMock() + field.json_schema_extra = {"ui_type": "multiselect", "options": ["python", "javascript"]} + + result = _build_field_dict( + field=field, + field_annotation=str, + description="Test field", + required=False, + ) + + assert result["type"] == "multiselect" + assert result["description"] == "Test field" + + def test_build_field_dict_with_enum_ui_type(self): + """Test that _build_field_dict works when ui_type is a GuardrailParamUITypes enum.""" + from unittest.mock import MagicMock + + from litellm.proxy.guardrails.guardrail_endpoints import _build_field_dict + from litellm.types.guardrails import GuardrailParamUITypes + + field = MagicMock() + field.json_schema_extra = {"ui_type": GuardrailParamUITypes.BOOL} + + result = _build_field_dict( + field=field, + field_annotation=bool, + description="Test bool field", + required=True, + ) + + assert result["type"] == "bool" + assert result["required"] is True # --- Team guardrail registration (register / submissions) --- MOCK_REGISTER_REQUEST = RegisterGuardrailRequest( @@ -1571,4 +1612,4 @@ async def test_list_submissions_summary_counts_unaffected_by_filters(mocker): assert len(result.submissions) == 1 # filtered assert result.summary.total == 2 # unfiltered assert result.summary.pending_review == 1 - assert result.summary.active == 1 \ No newline at end of file + assert result.summary.active == 1 diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 8abc6bfe077..bc13cea939e 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -11,11 +11,16 @@ from fastapi import Request import litellm from litellm.proxy._types import TeamCallbackMetadata, UserAPIKeyAuth from litellm.proxy.litellm_pre_call_utils import ( - KeyAndTeamLoggingSettings, LiteLLMProxyRequestSetup, - _get_dynamic_logging_metadata, _get_enforced_params, - _get_metadata_variable_name, _update_model_if_key_alias_exists, - add_guardrails_from_policy_engine, add_litellm_data_to_request, - check_if_token_is_service_account) + KeyAndTeamLoggingSettings, + LiteLLMProxyRequestSetup, + _get_dynamic_logging_metadata, + _get_enforced_params, + _get_metadata_variable_name, + _update_model_if_key_alias_exists, + add_guardrails_from_policy_engine, + add_litellm_data_to_request, + check_if_token_is_service_account, +) sys.path.insert( 0, os.path.abspath("../../..") @@ -154,8 +159,7 @@ def test_get_enforced_params( @pytest.mark.asyncio async def test_add_litellm_data_to_request_parses_string_metadata(): - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup request_mock = MagicMock(spec=Request) @@ -201,8 +205,7 @@ async def test_add_litellm_data_to_request_parses_string_metadata(): @pytest.mark.asyncio async def test_add_litellm_data_to_request_user_spend_and_budget(): - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request request_mock = MagicMock(spec=Request) request_mock.url.path = "/v1/completions" @@ -240,8 +243,7 @@ async def test_add_litellm_data_to_request_user_spend_and_budget(): @pytest.mark.asyncio async def test_add_litellm_data_to_request_audio_transcription_multipart(): - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup request mock for /v1/audio/transcriptions request_mock = MagicMock(spec=Request) @@ -306,8 +308,7 @@ async def test_add_litellm_data_to_request_disabled_callbacks(): """ Test that litellm_disabled_callbacks from key metadata is properly added to the request data. """ - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup mock request request_mock = MagicMock(spec=Request) @@ -360,8 +361,7 @@ async def test_add_litellm_data_to_request_disabled_callbacks_empty(): """ Test that litellm_disabled_callbacks is not added when it's empty. """ - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup mock request request_mock = MagicMock(spec=Request) @@ -413,8 +413,7 @@ async def test_add_litellm_data_to_request_disabled_callbacks_not_present(): """ Test that litellm_disabled_callbacks is not added when it's not present in metadata. """ - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup mock request request_mock = MagicMock(spec=Request) @@ -466,8 +465,7 @@ async def test_add_litellm_data_to_request_disabled_callbacks_invalid_type(): """ Test that litellm_disabled_callbacks is not added when it's not a list. """ - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup mock request request_mock = MagicMock(spec=Request) @@ -519,8 +517,7 @@ async def test_add_litellm_data_to_request_disabled_callbacks_with_logging_setti """ Test that litellm_disabled_callbacks works correctly alongside logging settings. """ - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup mock request request_mock = MagicMock(spec=Request) @@ -1030,8 +1027,7 @@ from unittest.mock import AsyncMock from fastapi.responses import Response from litellm.integrations.custom_logger import CustomLogger -from litellm.proxy.common_request_processing import \ - ProxyBaseLLMRequestProcessing +from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.proxy.utils import ProxyLogging from litellm.types.utils import StandardLoggingPayload @@ -1149,6 +1145,47 @@ async def test_add_litellm_metadata_from_request_headers(): litellm.callbacks = original_callbacks +def test_add_litellm_metadata_from_request_headers_x_litellm_trace_id_sets_chain_id(): + """x-litellm-trace-id sets both metadata and top-level litellm_session_id/litellm_trace_id for call chaining.""" + headers = {"x-litellm-trace-id": "foo"} + data = {"metadata": {}} + LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( + headers=headers, data=data, _metadata_variable_name="metadata" + ) + assert data["metadata"]["trace_id"] == "foo" + assert data["metadata"]["session_id"] == "foo" + assert data["litellm_session_id"] == "foo" + assert data["litellm_trace_id"] == "foo" + + +def test_add_litellm_metadata_from_request_headers_x_litellm_session_id_sets_chain_id(): + """x-litellm-session-id sets both metadata and top-level litellm_session_id/litellm_trace_id for call chaining.""" + headers = {"x-litellm-session-id": "bar"} + data = {"metadata": {}} + LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( + headers=headers, data=data, _metadata_variable_name="metadata" + ) + assert data["metadata"]["trace_id"] == "bar" + assert data["metadata"]["session_id"] == "bar" + assert data["litellm_session_id"] == "bar" + assert data["litellm_trace_id"] == "bar" + + +def test_add_litellm_metadata_from_request_headers_both_headers_trace_id_precedence(): + """When both x-litellm-trace-id and x-litellm-session-id are present, trace-id takes precedence for chain_id.""" + headers = { + "x-litellm-trace-id": "trace-value", + "x-litellm-session-id": "session-value", + } + data = {"metadata": {}} + LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( + headers=headers, data=data, _metadata_variable_name="metadata" + ) + assert data["metadata"]["trace_id"] == "trace-value" + assert data["metadata"]["session_id"] == "trace-value" + assert data["litellm_session_id"] == "trace-value" + assert data["litellm_trace_id"] == "trace-value" + def test_get_internal_user_header_from_mapping_returns_expected_header(): mappings = [ @@ -1407,8 +1444,7 @@ async def test_embedding_header_forwarding_with_model_group(): importlib.reload(pre_call_utils_module) # Re-import the function after reload to get the fresh version - from litellm.proxy.litellm_pre_call_utils import \ - add_litellm_data_to_request + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request # Setup mock request for embeddings request_mock = MagicMock(spec=Request) @@ -1542,11 +1578,13 @@ async def test_add_guardrails_from_policy_engine(): Test that add_guardrails_from_policy_engine adds guardrails from matching policies and tracks applied policies in metadata. """ - from litellm.proxy.policy_engine.attachment_registry import \ - get_attachment_registry + from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry from litellm.proxy.policy_engine.policy_registry import get_policy_registry - from litellm.types.proxy.policy_engine import (Policy, PolicyAttachment, - PolicyGuardrails) + from litellm.types.proxy.policy_engine import ( + Policy, + PolicyAttachment, + PolicyGuardrails, + ) # Setup test data data = { @@ -1659,8 +1697,7 @@ async def test_add_guardrails_from_policy_engine_policy_version_by_id(): Test that add_guardrails_from_policy_engine executes a specific policy version when policy_ is passed in the request body. """ - from litellm.proxy.policy_engine.attachment_registry import \ - get_attachment_registry + from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry from litellm.proxy.policy_engine.policy_registry import get_policy_registry from litellm.types.proxy.policy_engine import Policy, PolicyGuardrails @@ -1729,6 +1766,7 @@ async def test_bearer_token_not_in_debug_logs(): """ import logging from io import StringIO + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request from litellm.proxy.proxy_server import ProxyConfig diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx index b1012642c38..036a24c045a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -6,9 +6,9 @@ import { LeftOutlined, RightOutlined, } from "@ant-design/icons"; -import { Sparkles, Wrench } from "lucide-react"; +import { Bot, Sparkles, Wrench } from "lucide-react"; import { LogEntry } from "../columns"; -import { MCP_CALL_TYPES } from "../constants"; +import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants"; import { getEventDisplayName } from "../utils"; import { DrawerHeader } from "./DrawerHeader"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; @@ -46,6 +46,7 @@ interface TraceEventRowProps { function TraceEventRow({ row, isSelected, onClick }: TraceEventRowProps) { const isMcp = MCP_CALL_TYPES.includes(row.call_type); + const isAgent = AGENT_CALL_TYPES.includes(row.call_type); const durationValue = row.request_duration_ms != null ? (row.request_duration_ms / 1000).toFixed(3) @@ -64,6 +65,8 @@ function TraceEventRow({ row, isSelected, onClick }: TraceEventRowProps) {
{isMcp ? ( + ) : isAgent ? ( + ) : ( )} @@ -219,7 +222,10 @@ export function LogDetailsDrawer({ : null; const sessionDurationSeconds = sessionStart && sessionEnd ? ((sessionEnd.getTime() - sessionStart.getTime()) / 1000).toFixed(2) : "0.00"; - const llmCount = sessionLogs.filter((row) => !MCP_CALL_TYPES.includes(row.call_type)).length; + const llmCount = sessionLogs.filter( + (row) => !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type), + ).length; + const agentCount = sessionLogs.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length; const mcpCount = sessionLogs.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length; const logsForList = isSessionMode ? sessionLogs : currentLog ? [currentLog] : []; const leftPanelId = isSessionMode ? sessionId || "" : currentLog?.request_id || ""; @@ -302,14 +308,25 @@ export function LogDetailsDrawer({
{logsForList.length} req - · - {isSessionMode - ? `${llmCount} LLM` - : `${logsForList.filter((row) => !MCP_CALL_TYPES.includes(row.call_type)).length} LLM`} - · - {isSessionMode - ? `${mcpCount} MCP` - : `${logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length} MCP`} + {[ + isSessionMode + ? llmCount + : logsForList.filter( + (row) => + !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type), + ).length, + isSessionMode ? agentCount : logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length, + isSessionMode ? mcpCount : logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length, + ].map((count, i) => { + const label = [" LLM", " Agent", " MCP"][i]; + return count > 0 ? ( + + · + {count} + {label} + + ) : null; + })} · {isSessionMode ? getSpendString(totalSessionCost) diff --git a/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx b/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx index e4195ece9ec..70c71c7f255 100644 --- a/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx @@ -1,5 +1,5 @@ /** - * Compact type-indicator badges for LLM and MCP log entries. + * Compact type-indicator badges for LLM, Agent, and MCP log entries. * Used in the request logs table and session type column. */ @@ -15,6 +15,18 @@ export const WrenchIcon = ({ size = 10 }: { size?: number }) => ( ); +/** Agent/bot icon for A2A and agent call types (Lucide Bot-style). */ +export const AgentIcon = ({ size = 12 }: { size?: number }) => ( + + + + + + + + +); + export const LlmBadge = ({ count }: { count?: number }) => ( @@ -28,3 +40,10 @@ export const McpBadge = ({ count }: { count?: number }) => ( {count != null ? count : "MCP"} ); + +export const AgentBadge = ({ count }: { count?: number }) => ( + + + {count != null ? count : "Agent"} + +); diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 7cea1a36383..3d9d73bb09b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -6,8 +6,8 @@ import React, { useState } from "react"; import { getProviderLogoAndName } from "../provider_info_helpers"; import { TableHeaderSortDropdown } from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown"; import { TimeCell } from "./time_cell"; -import { MCP_CALL_TYPES } from "./constants"; -import { LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges"; +import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants"; +import { AgentBadge, AgentIcon, LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges"; /** API sort field mapping for /spend/logs/ui endpoint */ export const LOGS_SORT_FIELD_MAP = { @@ -69,6 +69,7 @@ export type LogEntry = { mcp_tool_call_spend?: number; session_llm_count?: number; session_mcp_count?: number; + session_agent_count?: number; onKeyHashClick?: (keyHash: string) => void; onSessionClick?: (sessionId: string) => void; }; @@ -124,17 +125,26 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] const row = info.row.original; const sessionCount = row.session_total_count || 1; const isMcp = MCP_CALL_TYPES.includes(row.call_type); - const sessionLlmCount = row.session_llm_count ?? (isMcp ? 0 : sessionCount); + const isAgent = AGENT_CALL_TYPES.includes(row.call_type); + const sessionLlmCount = row.session_llm_count ?? (isMcp || isAgent ? 0 : sessionCount); + const sessionAgentCount = row.session_agent_count ?? (isAgent ? sessionCount : 0); const sessionMcpCount = row.session_mcp_count ?? (isMcp ? sessionCount : 0); if (isMcp) return ; + if (isAgent && sessionCount <= 1) return ; if (sessionCount <= 1) return ; - // Multi-call session — show total count, plus MCP indicator when mixed. + // Multi-call session — show total count, plus Agent/MCP indicators when mixed. const sessionTypeBadge = ( {sessionCount} + {sessionAgentCount > 0 && ( + <> + · + + + )} {sessionMcpCount > 0 && ( <> · @@ -144,8 +154,13 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] ); + const tooltipParts = [ + sessionLlmCount > 0 && `${sessionLlmCount} LLM`, + sessionAgentCount > 0 && `${sessionAgentCount} Agent`, + sessionMcpCount > 0 && `${sessionMcpCount} MCP`, + ].filter(Boolean); return ( - + {sessionTypeBadge} ); diff --git a/ui/litellm-dashboard/src/components/view_logs/constants.ts b/ui/litellm-dashboard/src/components/view_logs/constants.ts index 949dab275fe..57155feae23 100644 --- a/ui/litellm-dashboard/src/components/view_logs/constants.ts +++ b/ui/litellm-dashboard/src/components/view_logs/constants.ts @@ -15,6 +15,9 @@ export const ERROR_CODE_OPTIONS: { label: string; value: string }[] = [ /** Call types that represent MCP tool invocations (shared across columns, index, drawer). */ export const MCP_CALL_TYPES = ["call_mcp_tool", "list_mcp_tools"]; +/** Call types that represent agent/A2A requests (e.g. asend_message). */ +export const AGENT_CALL_TYPES = ["asend_message"]; + export const QUICK_SELECT_OPTIONS: { label: string; value: number; unit: string }[] = [ { label: "Last 15 Minutes", value: 15, unit: "minutes" }, { label: "Last Hour", value: 1, unit: "hours" }, diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 64dc53c0e77..ab70126a5a8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -20,7 +20,7 @@ import KeyInfoView from "../templates/key_info_view"; import AuditLogs from "./audit_logs"; import { createColumns, LogEntry, type LogsSortField } from "./columns"; import { ConfigInfoMessage } from "./ConfigInfoMessage"; -import { ERROR_CODE_OPTIONS, MCP_CALL_TYPES, QUICK_SELECT_OPTIONS } from "./constants"; +import { AGENT_CALL_TYPES, ERROR_CODE_OPTIONS, MCP_CALL_TYPES, QUICK_SELECT_OPTIONS } from "./constants"; import { CostBreakdownViewer } from "./CostBreakdownViewer"; import { ErrorViewer } from "./ErrorViewer"; import { useLogFilterLogic } from "./log_filter_logic"; @@ -309,13 +309,15 @@ export default function SpendLogsTable({ return matchesSearch; }); - const sessionCompositionById = searchedLogs.reduce>((acc, log) => { + const sessionCompositionById = searchedLogs.reduce>((acc, log) => { if (!log.session_id) return acc; if (!acc[log.session_id]) { - acc[log.session_id] = { llm: 0, mcp: 0 }; + acc[log.session_id] = { llm: 0, agent: 0, mcp: 0 }; } if (MCP_CALL_TYPES.includes(log.call_type)) { acc[log.session_id].mcp += 1; + } else if (AGENT_CALL_TYPES.includes(log.call_type)) { + acc[log.session_id].agent += 1; } else { acc[log.session_id].llm += 1; } @@ -343,6 +345,7 @@ export default function SpendLogsTable({ request_duration_ms: log.request_duration_ms, session_llm_count: sessionComposition?.llm ?? undefined, session_mcp_count: sessionComposition?.mcp ?? undefined, + session_agent_count: sessionComposition?.agent ?? undefined, onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), onSessionClick: (sessionId: string) => { if (sessionId) { diff --git a/ui/litellm-dashboard/tsconfig.json b/ui/litellm-dashboard/tsconfig.json index d24bdd340f7..5b0352feb98 100644 --- a/ui/litellm-dashboard/tsconfig.json +++ b/ui/litellm-dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { From 8baa3ae8cb5d03df43ee53e6317a4d5781ac2ac1 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 3 Mar 2026 19:56:55 -0800 Subject: [PATCH 097/171] [Feat] UI - Add Open in New Tab on leftnav Bar (#22731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add minimal dev_config.yaml for proxy development Co-authored-by: Ishaan Jaff * feat(ui): wrap left nav items in tags for open-in-new-tab support Nav items are now rendered as elements with proper href attributes, enabling right-click → 'Open in new tab', Ctrl/Cmd+click, and middle-click to open any sidebar page in a new browser tab. Normal clicks continue to use SPA navigation (no full page reload). Applied to both leftnav.tsx (query-param routing) and Sidebar2.tsx (Next.js file-based routing). Co-authored-by: Ishaan Jaff --------- Co-authored-by: Cursor Agent Co-authored-by: Ishaan Jaff --- dev_config.yaml | 13 ++++++ .../app/(dashboard)/components/Sidebar2.tsx | 25 ++++++++++- .../src/components/leftnav.tsx | 44 ++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 dev_config.yaml diff --git a/dev_config.yaml b/dev_config.yaml new file mode 100644 index 00000000000..64e3c14703e --- /dev/null +++ b/dev_config.yaml @@ -0,0 +1,13 @@ +model_list: + - model_name: fake-openai-endpoint + litellm_params: + model: openai/fake-model + api_key: fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + +general_settings: + master_key: sk-1234 + +litellm_settings: + drop_params: True + telemetry: False diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index a74d3c108d6..b3829d0a8f4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -374,6 +374,27 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect router.push(href); }; + // Wrap label in so every nav item supports right-click → "Open in new tab" + // and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks. + const renderNavLink = (label: string, page: string): React.ReactNode => { + const href = toHref(page); + return ( + { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + e.stopPropagation(); + return; + } + e.preventDefault(); + }} + style={{ color: "inherit", textDecoration: "none" }} + > + {label} + + ); + }; + return ( = ({ accessToken, userRole, defaultSelect items={filteredMenuItems.map((item) => ({ key: item.key, icon: item.icon, - label: item.label, + label: renderNavLink(item.label, item.page), children: item.children?.map((child) => ({ key: child.key, icon: child.icon, - label: child.label, + label: renderNavLink(child.label, child.page), onClick: () => goTo(child.page), })), onClick: !item.children ? () => goTo(item.page) : undefined, diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index e84b6e86e4a..e09a95d15b7 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -374,6 +374,46 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse setPage(page); }; + // Wrap label in so every nav item supports right-click → "Open in new tab" + // and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks. + const renderNavLink = ( + label: React.ReactNode, + page: string, + externalUrl?: string, + ): React.ReactNode => { + if (externalUrl) { + return ( + e.stopPropagation()} + style={{ color: "inherit", textDecoration: "none" }} + > + {label} + + ); + } + const params = new URLSearchParams(window.location.search); + params.set("page", page); + const href = `?${params.toString()}`; + return ( + { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + e.stopPropagation(); + return; + } + e.preventDefault(); + }} + style={{ color: "inherit", textDecoration: "none" }} + > + {label} + + ); + }; + // Filter items based on user role and enabled pages for internal users const filterItemsByRole = (items: MenuItem[]): MenuItem[] => { const isAdmin = isAdminRole(userRole); @@ -469,11 +509,11 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse children: filteredItems.map((item) => ({ key: item.key, icon: item.icon, - label: item.label, + label: renderNavLink(item.label, item.page, item.external_url), children: item.children?.map((child) => ({ key: child.key, icon: child.icon, - label: child.label, + label: renderNavLink(child.label, child.page, child.external_url), onClick: () => { if (child.external_url) { window.open(child.external_url, "_blank"); From 1f412bc6d840e9b567ce07c8b26db3bf36008de7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 3 Mar 2026 20:22:20 -0800 Subject: [PATCH 098/171] [Feat] Add Tool Policies for AI Gateway (#22732) * fix: fix ui render * fix: fix minor bugs * refactor: use prisma functions instead of raw sql (safer) * fix(add-new-tiles-to-tool-policies): allow developer to see what's available * feat: ensure tool allowlist runs correctly for tool names + mcp's * refactor: more ui improvements * feat: working key tool blocking * feat(tools): show tool logs * refactor: backend code improvements * refactor: improve log viewer for tools * fix: address PR review feedback for tool access control - Add missing blocked_tools column to root schema.prisma (schema drift) - Invalidate ToolPolicyRegistry after policy mutations so changes take effect immediately - Remove dead code: unused get_effective_policies, get_tool_policies_cached, and helpers Co-Authored-By: Claude Opus 4.6 * fix: race condition in permission resolution and remove duplicate allowlist check - Use atomic update_many with object_permission_id=None to prevent concurrent requests from creating orphaned permission rows and losing tool blocks - Remove duplicate allowed_tools enforcement from guardrail (already enforced in auth layer via check_tools_allowlist) - Move inline uuid import to module level Co-Authored-By: Claude Opus 4.6 * update to account for userAgent * UI - Add ToolDetails * input/output policy * LiteLLM_PolicyAttachmentTable * LiteLLM_PolicyAttachmentTable * fix: add _enqueue_tool_registry_upsert * fix: tool mgmt endpoints * tool mgmt endpoints * Update tests/test_litellm/proxy/db/test_tool_registry_writer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update tests/test_litellm/proxy/db/test_tool_registry_writer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update tests/test_litellm/proxy/db/test_tool_registry_writer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: sync root schema.prisma and fix test_tool_registry_writer for input/output policy - Migrate root schema.prisma LiteLLM_ToolTable from call_policy to input_policy/output_policy, add missing user_agent and last_used_at columns (now consistent with litellm/proxy/schema.prisma and litellm-proxy-extras) - Fix SpendLogToolIndex comment across all three schema files - Fix all call_policy references in test_tool_registry_writer.py: swapped update_tool_policy arguments, wrong get_tools_by_names return type assertions, _mock_tool_row setting call_policy instead of input_policy Addresses Greptile review feedback on PR #22732. Made-with: Cursor --------- Co-authored-by: Krrish Dholakia Co-authored-by: Claude Opus 4.6 Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- AGENTS.md | 3 + CLAUDE.md | 4 + .../migration.sql | 2 + .../migration.sql | 11 + .../litellm_proxy_extras/schema.prisma | 46 +- .../chat/guardrail_translation/handler.py | 10 +- .../guardrail_translation/base_translation.py | 7 + .../chat/guardrail_translation/handler.py | 13 + .../guardrail_translation/handler.py | 39 +- ...odel_prices_and_context_window_backup.json | 418 +++++++++++++-- litellm/proxy/_new_secret_config.yaml | 30 +- litellm/proxy/_types.py | 9 +- litellm/proxy/auth/auth_checks.py | 55 +- litellm/proxy/db/db_spend_update_writer.py | 155 +++--- litellm/proxy/db/spend_log_tool_index.py | 147 +++++ litellm/proxy/db/tool_registry_writer.py | 409 +++++++++++--- .../tool_policy/tool_policy_guardrail.py | 208 +++++--- .../proxy/guardrails/tool_name_extraction.py | 85 +++ litellm/proxy/litellm_pre_call_utils.py | 9 + .../tool_management_endpoints.py | 501 +++++++++++++++++- litellm/proxy/proxy_server.py | 31 +- litellm/proxy/schema.prisma | 45 +- litellm/proxy/utils.py | 18 +- litellm/types/tool_management.py | 66 ++- schema.prisma | 45 +- scripts/test_tool_allowlist_script.py | 116 ++++ .../proxy/db/test_tool_registry_writer.py | 272 ++++++---- .../test_tool_policy_guardrail.py | 85 +-- .../proxy/test_tools_allowlist_enforcement.py | 200 +++++++ ui/litellm-dashboard/src/app/page.tsx | 4 +- .../src/components/ToolDetail.tsx | 445 ++++++++++++++++ .../src/components/ToolPolicies.tsx | 304 +++++++---- .../components/ToolPolicies/PolicySelect.tsx | 92 ++++ .../src/components/ToolPoliciesView.tsx | 44 ++ .../src/components/networking.tsx | 166 +++++- 35 files changed, 3467 insertions(+), 627 deletions(-) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260226000000_add_blocked_tools_to_object_permission/migration.sql create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260226120000_add_spend_log_tool_index/migration.sql create mode 100644 litellm/proxy/db/spend_log_tool_index.py create mode 100644 litellm/proxy/guardrails/tool_name_extraction.py create mode 100644 scripts/test_tool_allowlist_script.py create mode 100644 tests/test_litellm/proxy/test_tools_allowlist_enforcement.py create mode 100644 ui/litellm-dashboard/src/components/ToolDetail.tsx create mode 100644 ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx create mode 100644 ui/litellm-dashboard/src/components/ToolPoliciesView.tsx diff --git a/AGENTS.md b/AGENTS.md index d43f41dbe30..546f2997bf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,6 +109,8 @@ Key files: - `litellm/proxy/auth/` - Authentication logic - `litellm/proxy/management_endpoints/` - Admin API endpoints +**Database (proxy)**: Use Prisma model methods (`prisma_client.db..upsert`, `.find_many`, `.find_unique`, etc.), not raw SQL (`execute_raw`/`query_raw`). See COMMON PITFALLS for details. + ## MCP (MODEL CONTEXT PROTOCOL) SUPPORT LiteLLM supports MCP for agent workflows: @@ -176,6 +178,7 @@ When opening issues or pull requests, follow these templates: 5. **Dependencies**: Keep dependencies minimal and well-justified 6. **UI/Backend Contract Mismatch**: When adding a new entity type to the UI, always check whether the backend endpoint accepts a single value or an array. Match the UI control accordingly (single-select vs. multi-select) to avoid silently dropping user selections 7. **Missing Tests for New Entity Types**: When adding a new entity type (e.g., in `EntityUsage`, `UsageViewSelect`), always add corresponding tests in the existing test files and update any icon/component mocks +8. **Raw SQL in proxy DB code**: Do not use `execute_raw` or `query_raw` for proxy database access. Use Prisma model methods (e.g. `prisma_client.db.litellm_tooltable.upsert()`, `.find_many()`, `.find_unique()`) so behavior stays consistent with the schema, the client stays mockable in tests, and you avoid the pitfalls of hand-written SQL (parameter ordering, type casting, schema drift) 8. **Do not hardcode model-specific flags**: Put model-specific capability flags in `model_prices_and_context_window.json` and read them via `get_model_info` (or existing helpers like `supports_reasoning`). This prevents users from needing to upgrade LiteLLM each time a new model supports a feature. diff --git a/CLAUDE.md b/CLAUDE.md index 3b597fb8a90..c1eb75d2515 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,6 +107,10 @@ LiteLLM is a unified interface for 100+ LLM providers with two main components: - Migration files auto-generated with `prisma migrate dev` - Always test migrations against both PostgreSQL and SQLite +### Proxy database access +- **Do not write raw SQL** for proxy DB operations. Use Prisma model methods instead of `execute_raw` / `query_raw`. +- Use the generated client: `prisma_client.db.` (e.g. `litellm_tooltable`, `litellm_usertable`) with `.upsert()`, `.find_many()`, `.find_unique()`, `.update()`, `.update_many()` as appropriate. This avoids schema/client drift, keeps code testable with simple mocks, and matches patterns used in spend logs and other proxy code. + ### Enterprise Features - Enterprise-specific code in `enterprise/` directory - Optional features enabled via environment variables diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226000000_add_blocked_tools_to_object_permission/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226000000_add_blocked_tools_to_object_permission/migration.sql new file mode 100644 index 00000000000..cba06684193 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226000000_add_blocked_tools_to_object_permission/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "LiteLLM_ObjectPermissionTable" ADD COLUMN "blocked_tools" TEXT[] DEFAULT ARRAY[]::TEXT[]; diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226120000_add_spend_log_tool_index/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226120000_add_spend_log_tool_index/migration.sql new file mode 100644 index 00000000000..e3199679ce2 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260226120000_add_spend_log_tool_index/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "LiteLLM_SpendLogToolIndex" ( + "request_id" TEXT NOT NULL, + "tool_name" TEXT NOT NULL, + "start_time" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LiteLLM_SpendLogToolIndex_pkey" PRIMARY KEY ("request_id","tool_name") +); + +-- CreateIndex +CREATE INDEX "LiteLLM_SpendLogToolIndex_tool_name_start_time_idx" ON "LiteLLM_SpendLogToolIndex"("tool_name", "start_time"); diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index e0b28a4e012..5abe7a0a2b1 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -260,6 +260,7 @@ model LiteLLM_ObjectPermissionTable { vector_stores String[] @default([]) agents String[] @default([]) agent_access_groups String[] @default([]) + blocked_tools String[] @default([]) // Tool names blocked for any key/team/user with this permission teams LiteLLM_TeamTable[] projects LiteLLM_ProjectTable[] verification_tokens LiteLLM_VerificationToken[] @@ -928,6 +929,16 @@ model LiteLLM_SpendLogGuardrailIndex { @@index([policy_id, start_time]) } +// Index for fast "last N logs for tool" from SpendLogs – see how a tool is called in production +model LiteLLM_SpendLogToolIndex { + request_id String + tool_name String // matches LiteLLM_ToolTable.tool_name; join for input_policy/output_policy etc. + start_time DateTime + + @@id([request_id, tool_name]) + @@index([tool_name, start_time]) +} + // Prompt table for storing prompt configurations model LiteLLM_PromptTable { id String @id @default(uuid()) @@ -1065,26 +1076,31 @@ model LiteLLM_PolicyAttachmentTable { updated_by String? } -// Global tool registry - auto-discovered from LLM responses; admins set call_policy here +// Global tool registry - auto-discovered from LLM responses; admins set input_policy/output_policy here model LiteLLM_ToolTable { - tool_id String @id @default(uuid()) - tool_name String @unique // e.g. "huggingface_remote-mcp__dynamic_space" - origin String? // MCP server name or "user_defined" - call_policy String @default("untrusted") // "trusted" | "untrusted" | "dual_llm" | "blocked" - call_count Int @default(0) // cumulative number of times this tool was seen - assignments Json? @default("{}") - key_hash String? // hash of the virtual key that first called this tool - team_id String? // team that first called this tool - key_alias String? // human-readable alias of the virtual key - created_at DateTime @default(now()) - created_by String? - updated_at DateTime @default(now()) @updatedAt - updated_by String? + tool_id String @id @default(uuid()) + tool_name String @unique // e.g. "huggingface_remote-mcp__dynamic_space" + origin String? // MCP server name or "user_defined" + input_policy String @default("untrusted") // "trusted" | "untrusted" | "blocked" + output_policy String @default("untrusted") // "trusted" | "untrusted" + call_count Int @default(0) // cumulative number of times this tool was seen + assignments Json? @default("{}") + key_hash String? // hash of the virtual key that first called this tool + team_id String? // team that first called this tool + key_alias String? // human-readable alias of the virtual key + user_agent String? // user-agent of the first request that discovered this tool + last_used_at DateTime? // timestamp of the most recent call + created_at DateTime @default(now()) + created_by String? + updated_at DateTime @default(now()) @updatedAt + updated_by String? - @@index([call_policy]) + @@index([input_policy]) + @@index([output_policy]) @@index([team_id]) } +// Per-(tool, team/key) policy overrides. When present, override replaces global tool policy for that scope. //Unified Access Groups table for storing unified access groups model LiteLLM_AccessGroupTable { access_group_id String @id @default(uuid()) diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 98650a238e9..a6df346e8a8 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -75,7 +75,7 @@ class AnthropicMessagesHandler(BaseTranslation): if messages is None: return data - chat_completion_compatible_request, tool_name_mapping = ( + chat_completion_compatible_request, _tool_name_mapping = ( LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai( # Use a shallow copy to avoid mutating request data (pop on litellm_metadata). anthropic_message_request=cast(AnthropicMessagesRequest, data.copy()) @@ -141,6 +141,14 @@ class AnthropicMessagesHandler(BaseTranslation): return data + def extract_request_tool_names(self, data: dict) -> List[str]: + """Extract tool names from Anthropic messages request (tools[].name).""" + names: List[str] = [] + for tool in data.get("tools") or []: + if isinstance(tool, dict) and tool.get("name"): + names.append(str(tool["name"])) + return names + def _extract_input_text_and_images( self, message: Dict[str, Any], diff --git a/litellm/llms/base_llm/guardrail_translation/base_translation.py b/litellm/llms/base_llm/guardrail_translation/base_translation.py index 7106c207bd6..a7982cb606e 100644 --- a/litellm/llms/base_llm/guardrail_translation/base_translation.py +++ b/litellm/llms/base_llm/guardrail_translation/base_translation.py @@ -98,3 +98,10 @@ class BaseTranslation(ABC): Optional to override in subclasses. """ return responses_so_far + + def extract_request_tool_names(self, data: dict) -> List[str]: + """ + Extract tool names from the request body for allowlist/policy checks. + Override in tool-capable handlers; default returns []. + """ + return [] diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index 67e9e42bc30..10b0b58b6ac 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -135,6 +135,19 @@ class OpenAIChatCompletionsHandler(BaseTranslation): return data + def extract_request_tool_names(self, data: dict) -> List[str]: + """Extract tool names from OpenAI chat completions request (tools[].function.name, functions[].name).""" + names: List[str] = [] + for tool in data.get("tools") or []: + if isinstance(tool, dict) and tool.get("type") == "function": + fn = tool.get("function") + if isinstance(fn, dict) and fn.get("name"): + names.append(str(fn["name"])) + for fn in data.get("functions") or []: + if isinstance(fn, dict) and fn.get("name"): + names.append(str(fn["name"])) + return names + def _extract_inputs( self, message: Dict[str, Any], diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 6b092911d3c..7c3354cf88e 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -30,27 +30,22 @@ Output: response.output is List[GenericResponseOutputItem] where each has: from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast -from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall +from openai.types.responses.response_function_tool_call import \ + ResponseFunctionToolCall from pydantic import BaseModel from litellm._logging import verbose_proxy_logger from litellm.completion_extras.litellm_responses_transformation.transformation import ( LiteLLMResponsesTransformationHandler, - OpenAiResponsesToChatCompletionStreamIterator, -) -from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation -from litellm.responses.litellm_completion_transformation.transformation import ( - LiteLLMCompletionResponsesConfig, -) -from litellm.types.llms.openai import ( - ChatCompletionToolCallChunk, - ChatCompletionToolParam, -) -from litellm.types.responses.main import ( - GenericResponseOutputItem, - OutputFunctionToolCall, - OutputText, -) + OpenAiResponsesToChatCompletionStreamIterator) +from litellm.llms.base_llm.guardrail_translation.base_translation import \ + BaseTranslation +from litellm.responses.litellm_completion_transformation.transformation import \ + LiteLLMCompletionResponsesConfig +from litellm.types.llms.openai import (ChatCompletionToolCallChunk, + ChatCompletionToolParam) +from litellm.types.responses.main import (GenericResponseOutputItem, + OutputFunctionToolCall, OutputText) from litellm.types.utils import GenericGuardrailAPIInputs if TYPE_CHECKING: @@ -188,6 +183,18 @@ class OpenAIResponsesHandler(BaseTranslation): return data + def extract_request_tool_names(self, data: dict) -> List[str]: + """Extract tool names from Responses API request (tools[].name for function, tools[].server_label for mcp).""" + names: List[str] = [] + for tool in data.get("tools") or []: + if not isinstance(tool, dict): + continue + if tool.get("type") == "function" and tool.get("name"): + names.append(str(tool["name"])) + elif tool.get("type") == "mcp" and tool.get("server_label"): + names.append(str(tool["server_label"])) + return names + def _extract_and_transform_tools( self, tools: List[Dict[str, Any]], diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 3dfbc5cb219..b92e2727979 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -9779,6 +9779,122 @@ } ] }, + "dashscope/qwen3-max-2026-01-23": { + "litellm_provider": "dashscope", + "max_input_tokens": 258048, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 6e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 2.4e-06, + "output_cost_per_token": 1.2e-05, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "range": [ + 128000.0, + 252000.0 + ] + } + ] + }, + "dashscope/qwen3-next-80b-a3b-instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "dashscope/qwen3-next-80b-a3b-thinking": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen3-vl-235b-a22b-instruct": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-235b-a22b-thinking": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-32b-instruct": { + "input_cost_per_token": 1.6e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6.4e-07, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-32b-thinking": { + "input_cost_per_token": 1.6e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 2.87e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, "dashscope/qwen3-vl-plus": { "litellm_provider": "dashscope", "max_input_tokens": 260096, @@ -10844,7 +10960,8 @@ "output_cost_per_token": 9e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/NousResearch/Hermes-3-Llama-3.1-405B": { "max_tokens": 131072, @@ -10854,7 +10971,8 @@ "output_cost_per_token": 1e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/NousResearch/Hermes-3-Llama-3.1-70B": { "max_tokens": 131072, @@ -10874,7 +10992,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen2.5-72B-Instruct": { "max_tokens": 32768, @@ -10884,7 +11003,8 @@ "output_cost_per_token": 3.9e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen2.5-7B-Instruct": { "max_tokens": 32768, @@ -10905,7 +11025,8 @@ "litellm_provider": "deepinfra", "mode": "chat", "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-14B": { "max_tokens": 40960, @@ -10915,7 +11036,8 @@ "output_cost_per_token": 2.4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-235B-A22B": { "max_tokens": 40960, @@ -10925,7 +11047,8 @@ "output_cost_per_token": 5.4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-235B-A22B-Instruct-2507": { "max_tokens": 262144, @@ -10935,7 +11058,8 @@ "output_cost_per_token": 6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-235B-A22B-Thinking-2507": { "max_tokens": 262144, @@ -10945,7 +11069,8 @@ "output_cost_per_token": 2.9e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-30B-A3B": { "max_tokens": 40960, @@ -10955,7 +11080,8 @@ "output_cost_per_token": 2.9e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-32B": { "max_tokens": 40960, @@ -10965,7 +11091,8 @@ "output_cost_per_token": 2.8e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct": { "max_tokens": 262144, @@ -10975,7 +11102,8 @@ "output_cost_per_token": 1.6e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo": { "max_tokens": 262144, @@ -10985,7 +11113,8 @@ "output_cost_per_token": 1.2e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Next-80B-A3B-Instruct": { "max_tokens": 262144, @@ -10995,7 +11124,8 @@ "output_cost_per_token": 1.4e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Qwen/Qwen3-Next-80B-A3B-Thinking": { "max_tokens": 262144, @@ -11005,7 +11135,8 @@ "output_cost_per_token": 1.4e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/Sao10K/L3-8B-Lunaris-v1-Turbo": { "max_tokens": 8192, @@ -11056,7 +11187,8 @@ "cache_read_input_token_cost": 3.3e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/anthropic/claude-4-opus": { "max_tokens": 200000, @@ -11066,7 +11198,8 @@ "output_cost_per_token": 8.25e-05, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/anthropic/claude-4-sonnet": { "max_tokens": 200000, @@ -11076,7 +11209,8 @@ "output_cost_per_token": 1.65e-05, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1": { "max_tokens": 163840, @@ -11086,7 +11220,8 @@ "output_cost_per_token": 2.4e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-0528": { "max_tokens": 163840, @@ -11097,7 +11232,8 @@ "cache_read_input_token_cost": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-0528-Turbo": { "max_tokens": 32768, @@ -11107,7 +11243,8 @@ "output_cost_per_token": 3e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-Distill-Llama-70B": { "max_tokens": 131072, @@ -11127,7 +11264,8 @@ "output_cost_per_token": 2.7e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-R1-Turbo": { "max_tokens": 40960, @@ -11137,7 +11275,8 @@ "output_cost_per_token": 3e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3": { "max_tokens": 163840, @@ -11147,7 +11286,8 @@ "output_cost_per_token": 8.9e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3-0324": { "max_tokens": 163840, @@ -11157,7 +11297,8 @@ "output_cost_per_token": 8.8e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3.1": { "max_tokens": 163840, @@ -11169,7 +11310,8 @@ "litellm_provider": "deepinfra", "mode": "chat", "supports_tool_choice": true, - "supports_reasoning": true + "supports_reasoning": true, + "supports_function_calling": true }, "deepinfra/deepseek-ai/DeepSeek-V3.1-Terminus": { "max_tokens": 163840, @@ -11180,7 +11322,8 @@ "cache_read_input_token_cost": 2.16e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemini-2.0-flash-001": { "deprecation_date": "2026-06-01", @@ -11191,7 +11334,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemini-2.5-flash": { "max_tokens": 1000000, @@ -11201,7 +11345,8 @@ "output_cost_per_token": 2.5e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemini-2.5-pro": { "max_tokens": 1000000, @@ -11211,7 +11356,8 @@ "output_cost_per_token": 1e-05, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemma-3-12b-it": { "max_tokens": 131072, @@ -11221,7 +11367,8 @@ "output_cost_per_token": 1e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemma-3-27b-it": { "max_tokens": 131072, @@ -11231,7 +11378,8 @@ "output_cost_per_token": 1.6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/google/gemma-3-4b-it": { "max_tokens": 131072, @@ -11241,7 +11389,8 @@ "output_cost_per_token": 8e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-3.2-11B-Vision-Instruct": { "max_tokens": 131072, @@ -11261,7 +11410,8 @@ "output_cost_per_token": 2e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-3.3-70B-Instruct": { "max_tokens": 131072, @@ -11271,7 +11421,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo": { "max_tokens": 131072, @@ -11281,6 +11432,7 @@ "output_cost_per_token": 3.9e-07, "litellm_provider": "deepinfra", "mode": "chat", + "supports_function_calling": true, "supports_tool_choice": true }, "deepinfra/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { @@ -11291,7 +11443,8 @@ "output_cost_per_token": 6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-4-Scout-17B-16E-Instruct": { "max_tokens": 327680, @@ -11301,7 +11454,8 @@ "output_cost_per_token": 3e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Llama-Guard-3-8B": { "max_tokens": 131072, @@ -11331,7 +11485,8 @@ "output_cost_per_token": 6e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct": { "max_tokens": 131072, @@ -11341,7 +11496,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": { "max_tokens": 131072, @@ -11351,7 +11507,8 @@ "output_cost_per_token": 2.8e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct": { "max_tokens": 131072, @@ -11361,7 +11518,8 @@ "output_cost_per_token": 5e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": { "max_tokens": 131072, @@ -11371,7 +11529,8 @@ "output_cost_per_token": 3e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/microsoft/WizardLM-2-8x22B": { "max_tokens": 65536, @@ -11391,7 +11550,8 @@ "output_cost_per_token": 1.4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mistral-Nemo-Instruct-2407": { "max_tokens": 131072, @@ -11401,7 +11561,8 @@ "output_cost_per_token": 4e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mistral-Small-24B-Instruct-2501": { "max_tokens": 32768, @@ -11411,7 +11572,8 @@ "output_cost_per_token": 8e-08, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mistral-Small-3.2-24B-Instruct-2506": { "max_tokens": 128000, @@ -11421,7 +11583,8 @@ "output_cost_per_token": 2e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/mistralai/Mixtral-8x7B-Instruct-v0.1": { "max_tokens": 32768, @@ -11431,7 +11594,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/moonshotai/Kimi-K2-Instruct": { "max_tokens": 131072, @@ -11441,7 +11605,8 @@ "output_cost_per_token": 2e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/moonshotai/Kimi-K2-Instruct-0905": { "max_tokens": 262144, @@ -11452,7 +11617,8 @@ "cache_read_input_token_cost": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/nvidia/Llama-3.1-Nemotron-70B-Instruct": { "max_tokens": 131072, @@ -11462,7 +11628,8 @@ "output_cost_per_token": 6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/nvidia/Llama-3.3-Nemotron-Super-49B-v1.5": { "max_tokens": 131072, @@ -11472,7 +11639,8 @@ "output_cost_per_token": 4e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/nvidia/NVIDIA-Nemotron-Nano-9B-v2": { "max_tokens": 131072, @@ -11482,7 +11650,8 @@ "output_cost_per_token": 1.6e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/openai/gpt-oss-120b": { "max_tokens": 131072, @@ -11492,7 +11661,8 @@ "output_cost_per_token": 4.5e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/openai/gpt-oss-20b": { "max_tokens": 131072, @@ -11502,7 +11672,8 @@ "output_cost_per_token": 1.5e-07, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepinfra/zai-org/GLM-4.5": { "max_tokens": 131072, @@ -11512,7 +11683,8 @@ "output_cost_per_token": 1.6e-06, "litellm_provider": "deepinfra", "mode": "chat", - "supports_tool_choice": true + "supports_tool_choice": true, + "supports_function_calling": true }, "deepseek/deepseek-chat": { "cache_creation_input_token_cost": 0.0, @@ -25806,6 +25978,30 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "openrouter/anthropic/claude-sonnet-4.6": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost": 3e-07, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "source": "https://openrouter.ai/anthropic/claude-sonnet-4.6", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "openrouter/anthropic/claude-opus-4.5": { "cache_creation_input_token_cost": 6.25e-06, "cache_read_input_token_cost": 5e-07, @@ -26156,6 +26352,39 @@ "supports_web_search": true, "tpm": 800000 }, + "openrouter/google/gemini-3.1-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "source": "https://openrouter.ai/google/gemini-3.1-pro-preview", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "openrouter/gryphe/mythomax-l2-13b": { "input_cost_per_token": 1.875e-06, "litellm_provider": "openrouter", @@ -26533,6 +26762,29 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/openai/gpt-5.1-codex-max": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "source": "https://openrouter.ai/openai/gpt-5.1-codex-max", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, "openrouter/openai/gpt-5.2": { "input_cost_per_image": 0, "cache_read_input_token_cost": 1.75e-07, @@ -26687,6 +26939,19 @@ "supports_tool_choice": true, "supports_function_calling": true }, + "openrouter/qwen/qwen3-coder-plus": { + "input_cost_per_token": 1e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 997952, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://openrouter.ai/qwen/qwen3-coder-plus", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/qwen/qwen3-235b-a22b-2507": { "input_cost_per_token": 7.1e-08, "litellm_provider": "openrouter", @@ -26822,6 +27087,19 @@ "supports_vision": true, "supports_prompt_caching": false }, + "openrouter/z-ai/glm-5": { + "input_cost_per_token": 8e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 202752, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.56e-06, + "source": "https://openrouter.ai/z-ai/glm-5", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/minimax/minimax-m2.1": { "input_cost_per_token": 2.7e-07, "output_cost_per_token": 1.2e-06, @@ -34327,6 +34605,36 @@ "supports_tool_choice": true, "source": "https://aws.amazon.com/bedrock/pricing/" }, + "zai/glm-5": { + "cache_creation_input_token_cost": 0, + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-5-code": { + "cache_creation_input_token_cost": 0, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 5e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, "zai/glm-4.7": { "cache_creation_input_token_cost": 0, "cache_read_input_token_cost": 1.1e-07, diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index 6b84d90a327..508c1c94659 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -23,33 +23,11 @@ model_list: guardrails: - - guardrail_name: "airline-competitor-intent" - guardrail_id: "airline-competitor-intent" + - guardrail_name: "tool_policy" litellm_params: - guardrail: litellm_content_filter - mode: pre_call - default_on: false - competitor_intent_config: - brand_self: - - emirates - - ek - competitors: - - qatar airways - - qatar - - etihad - locations: - - qatar - - doha - - doh - competitor_aliases: - qatar airways: [qr, doha airline] - qatar: [qr] - policy: - competitor_comparison: refuse - possible_competitor_comparison: reframe - threshold_high: 0.70 - threshold_medium: 0.45 - threshold_low: 0.30 + guardrail: tool_policy + mode: [pre_call, post_call] + default_on: true mcp_servers: my_http_server: diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 8d49020461d..42b48446e7a 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -77,6 +77,7 @@ class SupportedDBObjectType(str, enum.Enum): PASS_THROUGH_ENDPOINTS = "pass_through_endpoints" PROMPTS = "prompts" MODEL_COST_MAP = "model_cost_map" + TOOLS = "tools" def __str__(self): return str(self.value) @@ -2133,7 +2134,7 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): user_header_mappings: Optional[List[UserHeaderMapping]] = None supported_db_objects: Optional[List[SupportedDBObjectType]] = Field( None, - description="Fine-grained control over which object types to load from the database when store_model_in_db is True. Available types: 'models', 'mcp', 'guardrails', 'vector_stores', 'pass_through_endpoints', 'prompts', 'model_cost_map'. If not set, all objects are loaded (default behavior).", + description="Fine-grained control over which object types to load from the database when store_model_in_db is True. Available types: 'models', 'mcp', 'guardrails', 'vector_stores', 'pass_through_endpoints', 'prompts', 'model_cost_map', 'tools'. If not set, all objects are loaded (default behavior).", ) user_mcp_management_mode: Optional[UserMCPManagementMode] = Field( None, @@ -3377,6 +3378,11 @@ class ProxyErrorTypes(str, enum.Enum): Team member is already in team """ + tool_access_denied = "tool_access_denied" + """ + Tool is not in the allowed tools list for this key/team + """ + @classmethod def get_model_access_error_type_for_object( cls, object_type: Literal["key", "user", "team", "org", "project"] @@ -4161,6 +4167,7 @@ class ToolDiscoveryQueueItem(TypedDict, total=False): key_hash: Optional[str] # hash of virtual key that triggered discovery team_id: Optional[str] # team that triggered discovery key_alias: Optional[str] # human-readable key alias + user_agent: Optional[str] # HTTP User-Agent of the caller class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase): diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 91ac58215ab..79e5f78f68e 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -58,6 +58,10 @@ from litellm.proxy._types import ( ) from litellm.proxy.auth.route_checks import RouteChecks from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler +from litellm.proxy.guardrails.tool_name_extraction import ( + TOOL_CAPABLE_CALL_TYPES, + extract_request_tool_names, +) from litellm.proxy.route_llm_request import route_request from litellm.proxy.utils import PrismaClient, ProxyLogging, log_db_metrics from litellm.router import Router @@ -220,7 +224,48 @@ async def _run_project_checks( ) -async def common_checks( +async def check_tools_allowlist( + request_body: dict, + valid_token: Optional[UserAPIKeyAuth], + team_object: Optional[LiteLLM_TeamTable], + route: str, +) -> None: + """ + Enforce key/team tool allowlist (metadata.allowed_tools). No DB in hot path — + effective allowlist is read from valid_token.metadata and valid_token.team_metadata. + Raises ProxyException with tool_access_denied if a tool is not allowed. + """ + from litellm.litellm_core_utils.api_route_to_call_types import ( + get_call_types_for_route, + ) + + if valid_token is None: + return + call_types = get_call_types_for_route(route) + if not call_types or not any(ct.value in TOOL_CAPABLE_CALL_TYPES for ct in call_types): + return + tool_names = extract_request_tool_names(route, request_body) + if not tool_names: + return + key_meta = (valid_token.metadata or {}) if isinstance(valid_token.metadata, dict) else {} + team_meta = (valid_token.team_metadata or {}) if isinstance(valid_token.team_metadata, dict) else {} + key_allowed = key_meta.get("allowed_tools") + team_allowed = team_meta.get("allowed_tools") + effective = key_allowed if (isinstance(key_allowed, list) and len(key_allowed) > 0) else team_allowed + if not isinstance(effective, list) or len(effective) == 0: + return + allowed_set = {str(t) for t in effective} + disallowed = [n for n in tool_names if n not in allowed_set] + if disallowed: + raise ProxyException( + message=f"Tool(s) {disallowed} are not in the allowed tools list for this key/team.", + type=ProxyErrorTypes.tool_access_denied, + param="tools", + code=status.HTTP_403_FORBIDDEN, + ) + + +async def common_checks( # noqa: PLR0915 request_body: dict, team_object: Optional[LiteLLM_TeamTable], user_object: Optional[LiteLLM_UserTable], @@ -477,6 +522,14 @@ async def common_checks( valid_token=valid_token, ) + # 12. [OPTIONAL] Tool allowlist - key/team allowed_tools (no DB in hot path) + await check_tools_allowlist( + request_body=request_body, + valid_token=valid_token, + team_object=team_object, + route=route, + ) + return True diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 0c25424ceaa..4c96e079c9e 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -13,49 +13,36 @@ import random import time import traceback from datetime import datetime, timedelta, timezone -from typing import ( - TYPE_CHECKING, - Any, - Dict, - List, - Literal, - Optional, - Union, - cast, - overload, -) +from typing import (TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, + cast, overload) import litellm from litellm._logging import verbose_proxy_logger from litellm.caching import DualCache, RedisCache from litellm.constants import DB_SPEND_UPDATE_JOB_NAME from litellm.litellm_core_utils.safe_json_loads import safe_json_loads -from litellm.proxy._types import ( - DB_CONNECTION_ERROR_TYPES, - BaseDailySpendTransaction, - DailyAgentSpendTransaction, - DailyEndUserSpendTransaction, - DailyOrganizationSpendTransaction, - DailyTagSpendTransaction, - DailyTeamSpendTransaction, - DailyUserSpendTransaction, - DBSpendUpdateTransactions, - Litellm_EntityType, - LiteLLM_UserTable, - SpendLogsMetadata, - SpendLogsPayload, - SpendUpdateQueueItem, - ToolDiscoveryQueueItem, -) -from litellm.proxy.db.db_transaction_queue.daily_spend_update_queue import ( - DailySpendUpdateQueue, -) -from litellm.proxy.db.db_transaction_queue.pod_lock_manager import PodLockManager -from litellm.proxy.db.db_transaction_queue.redis_update_buffer import RedisUpdateBuffer -from litellm.proxy.db.db_transaction_queue.spend_update_queue import SpendUpdateQueue -from litellm.proxy.db.db_transaction_queue.tool_discovery_queue import ( - ToolDiscoveryQueue, -) +from litellm.proxy._types import (DB_CONNECTION_ERROR_TYPES, + BaseDailySpendTransaction, + DailyAgentSpendTransaction, + DailyEndUserSpendTransaction, + DailyOrganizationSpendTransaction, + DailyTagSpendTransaction, + DailyTeamSpendTransaction, + DailyUserSpendTransaction, + DBSpendUpdateTransactions, + Litellm_EntityType, LiteLLM_UserTable, + SpendLogsMetadata, SpendLogsPayload, + SpendUpdateQueueItem, ToolDiscoveryQueueItem) +from litellm.proxy.db.db_transaction_queue.daily_spend_update_queue import \ + DailySpendUpdateQueue +from litellm.proxy.db.db_transaction_queue.pod_lock_manager import \ + PodLockManager +from litellm.proxy.db.db_transaction_queue.redis_update_buffer import \ + RedisUpdateBuffer +from litellm.proxy.db.db_transaction_queue.spend_update_queue import \ + SpendUpdateQueue +from litellm.proxy.db.db_transaction_queue.tool_discovery_queue import \ + ToolDiscoveryQueue from litellm.proxy.route_llm_request import ROUTE_ENDPOINT_MAPPING if TYPE_CHECKING: @@ -104,12 +91,10 @@ class DBSpendUpdateWriter: end_time: Optional[datetime], response_cost: Optional[float], ): - from litellm.proxy.proxy_server import ( - disable_spend_logs, - litellm_proxy_budget_name, - prisma_client, - user_api_key_cache, - ) + from litellm.proxy.proxy_server import (disable_spend_logs, + litellm_proxy_budget_name, + prisma_client, + user_api_key_cache) from litellm.proxy.utils import ProxyUpdateSpend, hash_token try: @@ -124,9 +109,8 @@ class DBSpendUpdateWriter: hashed_token = token ## CREATE SPEND LOG PAYLOAD ## - from litellm.proxy.spend_tracking.spend_tracking_utils import ( - get_logging_payload, - ) + from litellm.proxy.spend_tracking.spend_tracking_utils import \ + get_logging_payload payload = get_logging_payload( kwargs=kwargs, @@ -230,6 +214,7 @@ class DBSpendUpdateWriter: _litellm_params = kwargs.get("litellm_params") or {} _metadata = _litellm_params.get("metadata") or {} key_alias = _metadata.get("user_api_key_alias") or None + user_agent = _metadata.get("user_agent") or None def _enqueue(tool_name: str, origin: str = "user_defined") -> None: self.tool_discovery_queue.add_update( @@ -239,17 +224,20 @@ class DBSpendUpdateWriter: key_hash=hashed_token, team_id=team_id, key_alias=key_alias, + user_agent=user_agent, ) ) # --- MCP tool calls --- sl_object = kwargs.get("standard_logging_object") if sl_object is not None: - mcp_metadata = ( - sl_object.get("metadata", {}) or {} - ).get("mcp_tool_call_metadata") + mcp_metadata = (sl_object.get("metadata", {}) or {}).get( + "mcp_tool_call_metadata" + ) if mcp_metadata and isinstance(mcp_metadata, dict): - tool_name = mcp_metadata.get("namespaced_tool_name") or mcp_metadata.get("name") + tool_name = mcp_metadata.get( + "namespaced_tool_name" + ) or mcp_metadata.get("name") mcp_server_name = mcp_metadata.get("mcp_server_name") if tool_name: _enqueue(tool_name, origin=mcp_server_name or "user_defined") @@ -280,7 +268,9 @@ class DBSpendUpdateWriter: _enqueue(name) # --- Response tool_calls (OpenAI format; Anthropic pass-through converts tool_use here) --- - if completion_response is not None and hasattr(completion_response, "choices"): + if completion_response is not None and hasattr( + completion_response, "choices" + ): for choice in completion_response.choices or []: message = getattr(choice, "message", None) if message is None: @@ -768,19 +758,46 @@ class DBSpendUpdateWriter: daily_end_user_spend_update_transactions, daily_agent_spend_update_transactions, daily_tag_spend_update_transactions, - ) = await self.redis_update_buffer.get_all_transactions_from_redis_buffer_pipeline() + ) = ( + await self.redis_update_buffer.get_all_transactions_from_redis_buffer_pipeline() + ) if db_spend_update_transactions is not None: verbose_proxy_logger.info( "Spend tracking - committing spend updates from Redis to DB: " "keys=%d, users=%d, teams=%d, orgs=%d, end_users=%d, team_members=%d, tags=%d", - len(db_spend_update_transactions.get("key_list_transactions") or {}), - len(db_spend_update_transactions.get("user_list_transactions") or {}), - len(db_spend_update_transactions.get("team_list_transactions") or {}), - len(db_spend_update_transactions.get("org_list_transactions") or {}), - len(db_spend_update_transactions.get("end_user_list_transactions") or {}), - len(db_spend_update_transactions.get("team_member_list_transactions") or {}), - len(db_spend_update_transactions.get("tag_list_transactions") or {}), + len( + db_spend_update_transactions.get("key_list_transactions") + or {} + ), + len( + db_spend_update_transactions.get("user_list_transactions") + or {} + ), + len( + db_spend_update_transactions.get("team_list_transactions") + or {} + ), + len( + db_spend_update_transactions.get("org_list_transactions") + or {} + ), + len( + db_spend_update_transactions.get( + "end_user_list_transactions" + ) + or {} + ), + len( + db_spend_update_transactions.get( + "team_member_list_transactions" + ) + or {} + ), + len( + db_spend_update_transactions.get("tag_list_transactions") + or {} + ), ) await self._commit_spend_updates_to_db( prisma_client=prisma_client, @@ -985,10 +1002,8 @@ class DBSpendUpdateWriter: Commits all the spend `UPDATE` transactions to the Database """ - from litellm.proxy.utils import ( - ProxyUpdateSpend, - _raise_failed_update_spend_exception, - ) + from litellm.proxy.utils import (ProxyUpdateSpend, + _raise_failed_update_spend_exception) ### UPDATE USER TABLE ### user_list_transactions = db_spend_update_transactions["user_list_transactions"] @@ -1523,14 +1538,14 @@ class DBSpendUpdateWriter: # Add cache-related fields if they exist if "cache_read_input_tokens" in transaction: - common_data[ - "cache_read_input_tokens" - ] = transaction.get("cache_read_input_tokens", 0) + common_data["cache_read_input_tokens"] = ( + transaction.get("cache_read_input_tokens", 0) + ) if "cache_creation_input_tokens" in transaction: - common_data[ - "cache_creation_input_tokens" - ] = transaction.get( - "cache_creation_input_tokens", 0 + common_data["cache_creation_input_tokens"] = ( + transaction.get( + "cache_creation_input_tokens", 0 + ) ) if entity_type == "tag" and "request_id" in transaction: diff --git a/litellm/proxy/db/spend_log_tool_index.py b/litellm/proxy/db/spend_log_tool_index.py new file mode 100644 index 00000000000..6e8c63675e6 --- /dev/null +++ b/litellm/proxy/db/spend_log_tool_index.py @@ -0,0 +1,147 @@ +""" +Track tool usage for the dashboard: insert into SpendLogToolIndex when spend logs +are written, so "last N requests for tool X" and "how is this tool called in production" +queries are fast. +""" + +from datetime import datetime, timezone +from typing import Any, Dict, List, Set + +from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.safe_json_loads import safe_json_loads +from litellm.proxy.utils import PrismaClient + + +def _add_tool_calls_to_set(tool_calls: Any, out: Set[str]) -> None: + """Extract tool names from OpenAI-style tool_calls list into out.""" + if not isinstance(tool_calls, list): + return + for tc in tool_calls: + if not isinstance(tc, dict): + continue + fn = tc.get("function") + if isinstance(fn, dict): + name = fn.get("name") + if name and isinstance(name, str) and name.strip(): + out.add(name.strip()) + + +def _parse_tool_names_from_payload(payload: Dict[str, Any]) -> Set[str]: + """ + Extract deduplicated tool names from a spend log payload. + Sources: mcp_namespaced_tool_name, response (tool_calls), proxy_server_request (tools). + """ + tool_names: Set[str] = set() + + # Top-level MCP tool name (single tool per request for that flow) + mcp_name = payload.get("mcp_namespaced_tool_name") + if mcp_name and isinstance(mcp_name, str) and mcp_name.strip(): + tool_names.add(mcp_name.strip()) + + # Response: OpenAI-style tool_calls[].function.name or choices[0].message.tool_calls + response_raw = payload.get("response") + if response_raw: + response_obj = ( + safe_json_loads(response_raw, default=None) + if isinstance(response_raw, str) + else response_raw + ) + if isinstance(response_obj, dict): + _add_tool_calls_to_set(response_obj.get("tool_calls"), tool_names) + choices = response_obj.get("choices") + if isinstance(choices, list) and choices: + msg = choices[0].get("message") if isinstance(choices[0], dict) else None + if isinstance(msg, dict): + _add_tool_calls_to_set(msg.get("tool_calls"), tool_names) + + # Request body: tools[].function.name + request_raw = payload.get("proxy_server_request") + if request_raw: + request_obj = ( + safe_json_loads(request_raw, default=None) + if isinstance(request_raw, str) + else request_raw + ) + if isinstance(request_obj, dict): + body = request_obj.get("body", request_obj) + if isinstance(body, dict): + request_obj = body + if isinstance(request_obj, dict): + tools = request_obj.get("tools") + if isinstance(tools, list): + for t in tools: + if isinstance(t, dict): + fn = t.get("function") + if isinstance(fn, dict): + name = fn.get("name") + if name and isinstance(name, str) and name.strip(): + tool_names.add(name.strip()) + + return tool_names + + +async def process_spend_logs_tool_usage( + prisma_client: PrismaClient, + logs_to_process: List[Dict[str, Any]], +) -> None: + """ + After spend logs are written: insert SpendLogToolIndex rows from each payload. + Extracts tool names from mcp_namespaced_tool_name, response tool_calls, and + proxy_server_request tools. + """ + if not logs_to_process: + return + + index_rows: List[Dict[str, Any]] = [] + + for payload in logs_to_process: + request_id = payload.get("request_id") + start_time = payload.get("startTime") + if not request_id or not start_time: + continue + if isinstance(start_time, str): + try: + start_time = datetime.fromisoformat( + start_time.replace("Z", "+00:00") + ) + except (ValueError, TypeError): + continue + if start_time.tzinfo is None: + start_time = start_time.replace(tzinfo=timezone.utc) + + tool_names = _parse_tool_names_from_payload(payload) + for tool_name in tool_names: + index_rows.append({ + "request_id": request_id, + "tool_name": tool_name, + "start_time": start_time, + }) + + if not index_rows: + return + + try: + index_data = [] + for r in index_rows: + st = r["start_time"] + if isinstance(st, str): + try: + st = datetime.fromisoformat(st.replace("Z", "+00:00")) + except (ValueError, TypeError): + continue + if st.tzinfo is None: + st = st.replace(tzinfo=timezone.utc) + index_data.append({ + "request_id": r["request_id"], + "tool_name": r["tool_name"], + "start_time": st, + }) + if index_data: + await prisma_client.db.litellm_spendlogtoolindex.create_many( + data=index_data, + skip_duplicates=True, + ) + except Exception as e: + verbose_proxy_logger.warning( + "Tool usage tracking (SpendLogToolIndex) failed (non-fatal): %s", e + ) diff --git a/litellm/proxy/db/tool_registry_writer.py b/litellm/proxy/db/tool_registry_writer.py index 4e0a8095a08..0eda012d515 100644 --- a/litellm/proxy/db/tool_registry_writer.py +++ b/litellm/proxy/db/tool_registry_writer.py @@ -2,36 +2,64 @@ DB helpers for LiteLLM_ToolTable — the global tool registry. Tools are auto-discovered from LLM responses and upserted here. -Admins use the management endpoints to read and update call_policy. - -NOTE: Uses raw SQL (query_raw / execute_raw) instead of Prisma model methods -because the generated Prisma Python client may not have LiteLLM_ToolTable -when running against an older generated schema. +Admins use the management endpoints to read and update input_policy / output_policy. """ import uuid from datetime import datetime, timezone -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from litellm._logging import verbose_proxy_logger from litellm.proxy._types import ToolDiscoveryQueueItem -from litellm.types.tool_management import LiteLLM_ToolTableRow, ToolCallPolicy +from litellm.types.tool_management import ( + LiteLLM_ToolTableRow, + ToolPolicyOverrideRow, +) if TYPE_CHECKING: from litellm.proxy.utils import PrismaClient -def _row_to_model(row: dict) -> LiteLLM_ToolTableRow: +def _row_to_model(row: Union[dict, Any]) -> LiteLLM_ToolTableRow: + """Convert a Prisma model instance or dict to LiteLLM_ToolTableRow.""" + model_dump = getattr(row, "model_dump", None) + if callable(model_dump): + row = model_dump() + elif not isinstance(row, dict): + row = { + k: getattr(row, k, None) + for k in ( + "tool_id", + "tool_name", + "origin", + "input_policy", + "output_policy", + "call_count", + "assignments", + "key_hash", + "team_id", + "key_alias", + "user_agent", + "last_used_at", + "created_at", + "updated_at", + "created_by", + "updated_by", + ) + } return LiteLLM_ToolTableRow( tool_id=row.get("tool_id", ""), tool_name=row.get("tool_name", ""), origin=row.get("origin"), - call_policy=row.get("call_policy", "untrusted"), + input_policy=row.get("input_policy") or "untrusted", + output_policy=row.get("output_policy") or "untrusted", call_count=int(row.get("call_count") or 0), assignments=row.get("assignments"), key_hash=row.get("key_hash"), team_id=row.get("team_id"), key_alias=row.get("key_alias"), + user_agent=row.get("user_agent"), + last_used_at=row.get("last_used_at"), created_at=row.get("created_at"), updated_at=row.get("updated_at"), created_by=row.get("created_by"), @@ -44,10 +72,10 @@ async def batch_upsert_tools( items: List[ToolDiscoveryQueueItem], ) -> None: """ - Batch-upsert tool registry rows via raw SQL. + Batch-upsert tool registry rows via Prisma. - On first insert: sets call_policy = "untrusted" (schema default), call_count = 1. - On conflict: increments call_count; preserves existing call_policy. + On first insert: sets input_policy/output_policy = "untrusted" (default), call_count = 1. + On conflict: increments call_count; preserves existing policies. """ if not items: return @@ -55,6 +83,8 @@ async def batch_upsert_tools( data = [item for item in items if item.get("tool_name")] if not data: return + now = datetime.now(timezone.utc) + table = prisma_client.db.litellm_tooltable for item in data: tool_name = item.get("tool_name", "") origin = item.get("origin") or "user_defined" @@ -62,49 +92,52 @@ async def batch_upsert_tools( key_hash = item.get("key_hash") team_id = item.get("team_id") key_alias = item.get("key_alias") - now = datetime.now(timezone.utc).isoformat() - await prisma_client.db.execute_raw( - 'INSERT INTO "LiteLLM_ToolTable" ' - "(tool_id, tool_name, origin, call_policy, call_count, created_by, updated_by, key_hash, team_id, key_alias, created_at, updated_at) " - "VALUES ($7, $1, $2, 'untrusted', 1, $3, $3, $4, $5, $6, $8, $8) " - "ON CONFLICT (tool_name) DO UPDATE SET " - "call_count = \"LiteLLM_ToolTable\".call_count + 1, " - "updated_at = $8", - tool_name, - origin, - created_by, - key_hash, - team_id, - key_alias, - str(uuid.uuid4()), - now, + user_agent = item.get("user_agent") + await table.upsert( + where={"tool_name": tool_name}, + data={ + "create": { + "tool_id": str(uuid.uuid4()), + "tool_name": tool_name, + "origin": origin, + "input_policy": "untrusted", + "output_policy": "untrusted", + "call_count": 1, + "created_by": created_by, + "updated_by": created_by, + "key_hash": key_hash, + "team_id": team_id, + "key_alias": key_alias, + "user_agent": user_agent, + "last_used_at": now, + }, + "update": { + "call_count": {"increment": 1}, + "updated_at": now, + "last_used_at": now, + }, + }, ) verbose_proxy_logger.debug( "tool_registry_writer: upserted %d tool(s)", len(data) ) except Exception as e: - verbose_proxy_logger.error("tool_registry_writer batch_upsert_tools error: %s", e) + verbose_proxy_logger.error( + "tool_registry_writer batch_upsert_tools error: %s", e + ) async def list_tools( prisma_client: "PrismaClient", - call_policy: Optional[ToolCallPolicy] = None, + input_policy: Optional[str] = None, ) -> List[LiteLLM_ToolTableRow]: - """Return all tools, optionally filtered by call_policy.""" + """Return all tools, optionally filtered by input_policy.""" try: - if call_policy is not None: - rows = await prisma_client.db.query_raw( - 'SELECT tool_id, tool_name, origin, call_policy, call_count, assignments, ' - 'key_hash, team_id, key_alias, created_at, updated_at, created_by, updated_by ' - 'FROM "LiteLLM_ToolTable" WHERE call_policy = $1 ORDER BY created_at DESC', - call_policy, - ) - else: - rows = await prisma_client.db.query_raw( - 'SELECT tool_id, tool_name, origin, call_policy, call_count, assignments, ' - 'key_hash, team_id, key_alias, created_at, updated_at, created_by, updated_by ' - 'FROM "LiteLLM_ToolTable" ORDER BY created_at DESC', - ) + where = {"input_policy": input_policy} if input_policy is not None else {} + rows = await prisma_client.db.litellm_tooltable.find_many( + where=where, + order={"created_at": "desc"}, + ) return [_row_to_model(row) for row in rows] except Exception as e: verbose_proxy_logger.error("tool_registry_writer list_tools error: %s", e) @@ -117,15 +150,12 @@ async def get_tool( ) -> Optional[LiteLLM_ToolTableRow]: """Return a single tool row by tool_name.""" try: - rows = await prisma_client.db.query_raw( - 'SELECT tool_id, tool_name, origin, call_policy, call_count, assignments, ' - 'key_hash, team_id, key_alias, created_at, updated_at, created_by, updated_by ' - 'FROM "LiteLLM_ToolTable" WHERE tool_name = $1', - tool_name, + row = await prisma_client.db.litellm_tooltable.find_unique( + where={"tool_name": tool_name}, ) - if not rows: + if row is None: return None - return _row_to_model(rows[0]) + return _row_to_model(row) except Exception as e: verbose_proxy_logger.error("tool_registry_writer get_tool error: %s", e) return None @@ -134,46 +164,279 @@ async def get_tool( async def update_tool_policy( prisma_client: "PrismaClient", tool_name: str, - call_policy: ToolCallPolicy, updated_by: Optional[str], + input_policy: Optional[str] = None, + output_policy: Optional[str] = None, ) -> Optional[LiteLLM_ToolTableRow]: - """Update the call_policy for a tool. Upserts the row if it does not exist yet.""" + """Update input_policy and/or output_policy for a tool. Upserts the row if it does not exist yet.""" try: _updated_by = updated_by or "system" - now = datetime.now(timezone.utc).isoformat() - await prisma_client.db.execute_raw( - 'INSERT INTO "LiteLLM_ToolTable" (tool_id, tool_name, call_policy, created_by, updated_by, created_at, updated_at) ' - "VALUES ($4, $1, $2, $3, $3, $5, $5) " - "ON CONFLICT (tool_name) DO UPDATE SET call_policy = $2, updated_by = $3, updated_at = $5", - tool_name, - call_policy, - _updated_by, - str(uuid.uuid4()), - now, + now = datetime.now(timezone.utc) + + create_data: dict = { + "tool_id": str(uuid.uuid4()), + "tool_name": tool_name, + "input_policy": input_policy or "untrusted", + "output_policy": output_policy or "untrusted", + "created_by": _updated_by, + "updated_by": _updated_by, + "created_at": now, + "updated_at": now, + } + update_data: dict = { + "updated_by": _updated_by, + "updated_at": now, + } + if input_policy is not None: + update_data["input_policy"] = input_policy + if output_policy is not None: + update_data["output_policy"] = output_policy + + await prisma_client.db.litellm_tooltable.upsert( + where={"tool_name": tool_name}, + data={ + "create": create_data, + "update": update_data, + }, ) return await get_tool(prisma_client, tool_name) except Exception as e: - verbose_proxy_logger.error("tool_registry_writer update_tool_policy error: %s", e) + verbose_proxy_logger.error( + "tool_registry_writer update_tool_policy error: %s", e + ) return None async def get_tools_by_names( prisma_client: "PrismaClient", tool_names: List[str], -) -> Dict[str, str]: +) -> Dict[str, Tuple[str, str]]: """ - Return a {tool_name: call_policy} map for the given tool names. - Used by the policy enforcement guardrail — single batch query, never N+1. + Return a {tool_name: (input_policy, output_policy)} map for the given tool names. """ if not tool_names: return {} try: - placeholders = ", ".join(f"${i+1}" for i in range(len(tool_names))) - rows = await prisma_client.db.query_raw( - f'SELECT tool_name, call_policy FROM "LiteLLM_ToolTable" WHERE tool_name IN ({placeholders})', - *tool_names, + rows = await prisma_client.db.litellm_tooltable.find_many( + where={"tool_name": {"in": tool_names}}, ) - return {row["tool_name"]: row["call_policy"] for row in rows} + return { + row.tool_name: ( + getattr(row, "input_policy", "untrusted") or "untrusted", + getattr(row, "output_policy", "untrusted") or "untrusted", + ) + for row in rows + } except Exception as e: - verbose_proxy_logger.error("tool_registry_writer get_tools_by_names error: %s", e) + verbose_proxy_logger.error( + "tool_registry_writer get_tools_by_names error: %s", e + ) return {} + + +async def list_overrides_for_tool( + prisma_client: "PrismaClient", + tool_name: str, +) -> List[ToolPolicyOverrideRow]: + """ + Return override-like rows for a tool by finding object permissions that have + this tool in blocked_tools, then resolving each permission to key/team scope for display. + """ + out: List[ToolPolicyOverrideRow] = [] + try: + perms = await prisma_client.db.litellm_objectpermissiontable.find_many( + where={"blocked_tools": {"has": tool_name}}, + include={ + "verification_tokens": True, + "teams": True, + }, + ) + for perm in perms: + op_id = getattr(perm, "object_permission_id", None) or "" + tokens = getattr(perm, "verification_tokens", []) or [] + teams = getattr(perm, "teams", []) or [] + for t in tokens: + out.append( + ToolPolicyOverrideRow( + override_id=op_id, + tool_name=tool_name, + team_id=None, + key_hash=getattr(t, "token", None), + input_policy="blocked", + key_alias=getattr(t, "key_alias", None), + created_at=None, + updated_at=None, + ) + ) + for team in teams: + out.append( + ToolPolicyOverrideRow( + override_id=op_id, + tool_name=tool_name, + team_id=getattr(team, "team_id", None), + key_hash=None, + input_policy="blocked", + key_alias=getattr(team, "team_alias", None), + created_at=None, + updated_at=None, + ) + ) + return out + except Exception as e: + verbose_proxy_logger.error( + "tool_registry_writer list_overrides_for_tool error: %s", e + ) + return [] + + +class ToolPolicyRegistry: + """ + In-memory registry of tool policies synced from DB. + Hot path uses get_effective_policies only — no DB, no cache. + """ + + def __init__(self) -> None: + self._tool_input_policies: Dict[str, str] = {} + self._tool_output_policies: Dict[str, str] = {} + self._blocked_tools_by_op_id: Dict[str, List[str]] = {} + self._initialized: bool = False + + def is_initialized(self) -> bool: + return self._initialized + + async def sync_tool_policy_from_db(self, prisma_client: "PrismaClient") -> None: + """Load all tool policies and object-permission blocked_tools from DB.""" + try: + tools = await prisma_client.db.litellm_tooltable.find_many() + self._tool_input_policies = { + row.tool_name: getattr(row, "input_policy", "untrusted") or "untrusted" + for row in tools + } + self._tool_output_policies = { + row.tool_name: getattr(row, "output_policy", "untrusted") or "untrusted" + for row in tools + } + + perms = await prisma_client.db.litellm_objectpermissiontable.find_many() + self._blocked_tools_by_op_id = {} + for row in perms: + op_id = getattr(row, "object_permission_id", None) + blocked = getattr(row, "blocked_tools", None) or [] + if op_id: + self._blocked_tools_by_op_id[op_id] = list(blocked) + + self._initialized = True + verbose_proxy_logger.info( + "ToolPolicyRegistry: synced %d tool policies and %d object permissions from DB", + len(self._tool_input_policies), + len(self._blocked_tools_by_op_id), + ) + except Exception as e: + verbose_proxy_logger.exception( + "ToolPolicyRegistry sync_tool_policy_from_db error: %s", e + ) + raise + + def get_input_policy(self, tool_name: str) -> str: + return self._tool_input_policies.get(tool_name, "untrusted") + + def get_output_policy(self, tool_name: str) -> str: + return self._tool_output_policies.get(tool_name, "untrusted") + + def get_effective_policies( + self, + tool_names: List[str], + object_permission_id: Optional[str] = None, + team_object_permission_id: Optional[str] = None, + ) -> Dict[str, str]: + """ + Return effective input_policy per tool from in-memory state. + If tool is in key or team blocked_tools -> "blocked", else global input_policy or "untrusted". + """ + if not tool_names: + return {} + blocked: set = set() + for op_id in (object_permission_id, team_object_permission_id): + if op_id and op_id.strip(): + blocked.update( + self._blocked_tools_by_op_id.get(op_id.strip(), []) + ) + result: Dict[str, str] = {} + for name in tool_names: + if name in blocked: + result[name] = "blocked" + else: + result[name] = self._tool_input_policies.get(name, "untrusted") + return result + + +_tool_policy_registry: Optional[ToolPolicyRegistry] = None + + +def get_tool_policy_registry() -> ToolPolicyRegistry: + """Return the global ToolPolicyRegistry singleton.""" + global _tool_policy_registry + if _tool_policy_registry is None: + _tool_policy_registry = ToolPolicyRegistry() + return _tool_policy_registry + + +async def add_tool_to_object_permission_blocked( + prisma_client: "PrismaClient", + object_permission_id: str, + tool_name: str, +) -> bool: + """Add tool_name to the permission's blocked_tools if not already present.""" + if not object_permission_id or not tool_name: + return False + try: + row = await prisma_client.db.litellm_objectpermissiontable.find_unique( + where={"object_permission_id": object_permission_id}, + ) + if row is None: + return False + current = list(getattr(row, "blocked_tools", []) or []) + if tool_name in current: + return True + current.append(tool_name) + await prisma_client.db.litellm_objectpermissiontable.update( + where={"object_permission_id": object_permission_id}, + data={"blocked_tools": current}, + ) + return True + except Exception as e: + verbose_proxy_logger.error( + "tool_registry_writer add_tool_to_object_permission_blocked error: %s", e + ) + return False + + +async def remove_tool_from_object_permission_blocked( + prisma_client: "PrismaClient", + object_permission_id: str, + tool_name: str, +) -> bool: + """Remove tool_name from the permission's blocked_tools. Returns False if tool was not in list.""" + if not object_permission_id or not tool_name: + return False + try: + row = await prisma_client.db.litellm_objectpermissiontable.find_unique( + where={"object_permission_id": object_permission_id}, + ) + if row is None: + return False + current = list(getattr(row, "blocked_tools", []) or []) + if tool_name not in current: + return False + current = [t for t in current if t != tool_name] + await prisma_client.db.litellm_objectpermissiontable.update( + where={"object_permission_id": object_permission_id}, + data={"blocked_tools": current}, + ) + return True + except Exception as e: + verbose_proxy_logger.error( + "tool_registry_writer remove_tool_from_object_permission_blocked error: %s", + e, + ) + return False diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_policy/tool_policy_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/tool_policy/tool_policy_guardrail.py index 87558566c42..368948414e9 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_policy/tool_policy_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_policy/tool_policy_guardrail.py @@ -1,13 +1,16 @@ """ Tool Policy Guardrail -Reads call_policy from LiteLLM_ToolTable and enforces it on LLM requests/responses. +Reads input_policy / output_policy from LiteLLM_ToolTable and enforces them. -Policy values: - "trusted" - allow through (no action) - "untrusted" - allow through (no action; default for newly discovered tools) +Input policy values: + "untrusted" - allow through (default for newly discovered tools) + "trusted" - only allow if conversation contains no untrusted tool output "blocked" - raise HTTPException, preventing the tool call - "dual_llm" - (Phase 3) send to second LLM for verification; currently treated as allowed + +Output policy values: + "untrusted" - output may be tainted (default) + "trusted" - output is verified safe Configuration in proxy config YAML: guardrails: @@ -15,25 +18,18 @@ Configuration in proxy config YAML: litellm_params: guardrail: tool_policy mode: post_call - -or both pre and post call: - - guardrail_name: "tool_policy" - litellm_params: - guardrail: tool_policy - mode: during_call # runs before LLM and on response """ -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple from fastapi import HTTPException from litellm._logging import verbose_proxy_logger -from litellm.caching.dual_cache import DualCache -from litellm.constants import TOOL_POLICY_CACHE_TTL_SECONDS from litellm.integrations.custom_guardrail import ( CustomGuardrail, log_guardrail_information, ) +from litellm.proxy.guardrails.tool_name_extraction import extract_request_tool_names from litellm.types.guardrails import GuardrailEventHooks from litellm.types.utils import GenericGuardrailAPIInputs @@ -43,12 +39,71 @@ if TYPE_CHECKING: GUARDRAIL_NAME = "tool_policy" +def _get_request_object_permission_ids( + request_data: dict, +) -> Tuple[Optional[str], Optional[str]]: + """Extract object_permission_id and team_object_permission_id from request_data.""" + if not request_data: + return None, None + for key in ("litellm_metadata", "metadata"): + meta = request_data.get(key) + if not isinstance(meta, dict): + continue + auth = meta.get("user_api_key_auth") + if auth is not None and hasattr(auth, "object_permission_id"): + key_op = getattr(auth, "object_permission_id", None) + team_op = getattr(auth, "team_object_permission_id", None) + if key_op is not None or team_op is not None: + return ( + str(key_op).strip() if key_op else None, + str(team_op).strip() if team_op else None, + ) + key_op = meta.get("user_api_key_object_permission_id") + team_op = meta.get("user_api_key_team_object_permission_id") + if key_op is not None or team_op is not None: + return ( + str(key_op).strip() if key_op else None, + str(team_op).strip() if team_op else None, + ) + return None, None + + +def _get_request_route_from_data(request_data: dict) -> Optional[str]: + """Get request route from request_data (metadata or top-level).""" + route = request_data.get("user_api_key_request_route") + if route: + return route + meta = request_data.get("metadata") or request_data.get("litellm_metadata") or {} + return meta.get("user_api_key_request_route") + + +def _resolve_tool_names_from_messages(messages: List[dict]) -> Dict[str, str]: + """ + Build a map of tool_call_id -> tool_name from assistant messages' tool_calls. + Used to resolve which tool produced each tool result in the conversation. + """ + mapping: Dict[str, str] = {} + for msg in messages: + if msg.get("role") != "assistant": + continue + tool_calls = msg.get("tool_calls") or [] + for tc in tool_calls: + if isinstance(tc, dict): + tc_id = tc.get("id") + fn = (tc.get("function") or {}).get("name") + else: + tc_id = getattr(tc, "id", None) + fn_obj = getattr(tc, "function", None) + fn = getattr(fn_obj, "name", None) if fn_obj else None + if tc_id and fn: + mapping[tc_id] = fn + return mapping + + class ToolPolicyGuardrail(CustomGuardrail): """ - Guardrail that enforces per-tool call policies stored in LiteLLM_ToolTable. - - Tools with call_policy="blocked" are rejected before/after the LLM call. - Tools with call_policy="trusted" or "untrusted" pass through unchanged. + Guardrail that enforces per-tool input/output policies from the in-memory + ToolPolicyRegistry (synced from DB). """ def __init__(self, **kwargs: Any) -> None: @@ -59,7 +114,6 @@ class ToolPolicyGuardrail(CustomGuardrail): GuardrailEventHooks.during_call, ] super().__init__(**kwargs) - self._policy_cache: DualCache = DualCache() @log_guardrail_information async def apply_guardrail( @@ -70,12 +124,7 @@ class ToolPolicyGuardrail(CustomGuardrail): logging_obj: Optional["LiteLLMLoggingObj"] = None, ) -> GenericGuardrailAPIInputs: """ - Enforce tool policies on both request tools and response tool_calls. - - - input_type="request": check inputs["tools"] (tool definitions in the LLM request) - - input_type="response": check inputs["tool_calls"] (tool_calls in the LLM response) - - Raises HTTPException (400) if any tool is "blocked". + Enforce input_policy and output_policy trust chain on request tools / response tool_calls. """ if input_type == "request": tools = inputs.get("tools") or [] @@ -86,7 +135,11 @@ class ToolPolicyGuardrail(CustomGuardrail): and isinstance(t.get("function"), dict) and t["function"].get("name") ] - else: # response + if not tool_names: + route = _get_request_route_from_data(request_data) + if route: + tool_names = extract_request_tool_names(route, request_data) + else: tool_calls = inputs.get("tool_calls") or [] tool_names = [] for tc in tool_calls: @@ -101,12 +154,25 @@ class ToolPolicyGuardrail(CustomGuardrail): if not tool_names: return inputs - policy_map = await self._get_policies_cached(tool_names) + object_permission_id, team_object_permission_id = ( + _get_request_object_permission_ids(request_data) + ) + from litellm.proxy.db.tool_registry_writer import get_tool_policy_registry + registry = get_tool_policy_registry() + if not registry.is_initialized(): + return inputs + + # Stage 1: Check for blocked tools (input_policy=blocked or per-key/team override) + policy_map = registry.get_effective_policies( + tool_names, + object_permission_id=object_permission_id, + team_object_permission_id=team_object_permission_id, + ) blocked = [name for name in tool_names if policy_map.get(name) == "blocked"] if blocked: verbose_proxy_logger.warning( - "ToolPolicyGuardrail: blocking tool(s) %s (policy=blocked)", blocked + "ToolPolicyGuardrail: blocking tool(s) %s (input_policy=blocked)", blocked ) raise HTTPException( status_code=400, @@ -117,47 +183,47 @@ class ToolPolicyGuardrail(CustomGuardrail): }, ) + # Stage 2: Trust chain enforcement (response path only) + # For each tool with input_policy=trusted, check if conversation + # contains output from tools with output_policy=untrusted + if input_type == "response": + trusted_input_tools = [ + name for name in tool_names if policy_map.get(name) == "trusted" + ] + if trusted_input_tools: + messages = request_data.get("messages") or [] + tc_id_to_name = _resolve_tool_names_from_messages(messages) + + untrusted_sources: List[str] = [] + for msg in messages: + if msg.get("role") != "tool": + continue + tool_call_id = msg.get("tool_call_id") + source_tool = tc_id_to_name.get(tool_call_id, "") if tool_call_id else "" + if not source_tool: + continue + if registry.get_output_policy(source_tool) == "untrusted": + if source_tool not in untrusted_sources: + untrusted_sources.append(source_tool) + + if untrusted_sources: + verbose_proxy_logger.warning( + "ToolPolicyGuardrail: trust chain violation — %s require trusted input " + "but conversation has untrusted output from %s", + trusted_input_tools, + untrusted_sources, + ) + raise HTTPException( + status_code=400, + detail={ + "error": "Violated tool policy", + "blocked_tools": trusted_input_tools, + "untrusted_sources": untrusted_sources, + "message": ( + f"{', '.join(trusted_input_tools)} requires trusted input but " + f"conversation contains untrusted output from {', '.join(untrusted_sources)}." + ), + }, + ) + return inputs - - async def _get_policies_cached(self, tool_names: List[str]) -> Dict[str, str]: - """ - Batch-fetch call_policy for the given tool names. - - Caches per individual tool name (not per combination) so that adding - a new tool to a request doesn't invalidate the cached policies for all - the other tools already in the cache. - """ - from litellm.proxy.db.tool_registry_writer import get_tools_by_names - from litellm.proxy.proxy_server import prisma_client - - if not tool_names or prisma_client is None: - return {} - - result: Dict[str, str] = {} - cache_misses: List[str] = [] - - for name in tool_names: - cached = await self._policy_cache.async_get_cache(f"tool_policy:{name}") - if cached is not None and isinstance(cached, str): - result[name] = cached - else: - cache_misses.append(name) - - if cache_misses: - fetched = await get_tools_by_names( - prisma_client=prisma_client, tool_names=cache_misses - ) - for name, policy in fetched.items(): - result[name] = policy - await self._policy_cache.async_set_cache( - key=f"tool_policy:{name}", - value=policy, - ttl=TOOL_POLICY_CACHE_TTL_SECONDS, - ) - verbose_proxy_logger.debug( - "ToolPolicyGuardrail: fetched %d policies from DB (cache hits: %d)", - len(cache_misses), - len(tool_names) - len(cache_misses), - ) - - return result diff --git a/litellm/proxy/guardrails/tool_name_extraction.py b/litellm/proxy/guardrails/tool_name_extraction.py new file mode 100644 index 00000000000..db24fa2277c --- /dev/null +++ b/litellm/proxy/guardrails/tool_name_extraction.py @@ -0,0 +1,85 @@ +""" +Extract tool names from request body by route/call type. + +Used by auth (check_tools_allowlist) and ToolPolicyGuardrail so tool-format +knowledge lives in one place. Uses guardrail translation handlers where available, +with standalone extractors for generate_content and MCP. +""" + +from typing import Any, Dict, List + +from litellm.litellm_core_utils.api_route_to_call_types import get_call_types_for_route +from litellm.llms import load_guardrail_translation_mappings +from litellm.types.utils import CallTypes + +# Call types that have no guardrail translation handler; we use standalone extractors +STANDALONE_EXTRACTORS: Dict[str, Any] = {} + + +def _extract_generate_content_tool_names(data: dict) -> List[str]: + """Google generateContent: tools[].functionDeclarations[].name""" + names: List[str] = [] + for tool in data.get("tools") or []: + if not isinstance(tool, dict): + continue + for decl in tool.get("functionDeclarations") or []: + if isinstance(decl, dict) and decl.get("name"): + names.append(str(decl["name"])) + return names + + +def _extract_mcp_tool_names(data: dict) -> List[str]: + """MCP call_tool: name or mcp_tool_name in body""" + names: List[str] = [] + name = data.get("name") or data.get("mcp_tool_name") + if name: + names.append(str(name)) + return names + + +def _register_standalone_extractors() -> None: + if STANDALONE_EXTRACTORS: + return + STANDALONE_EXTRACTORS[CallTypes.generate_content.value] = _extract_generate_content_tool_names + STANDALONE_EXTRACTORS[CallTypes.agenerate_content.value] = _extract_generate_content_tool_names + STANDALONE_EXTRACTORS[CallTypes.call_mcp_tool.value] = _extract_mcp_tool_names + + +# Tool-capable call types (routes that can send tools in the request) +TOOL_CAPABLE_CALL_TYPES = frozenset({ + CallTypes.completion.value, + CallTypes.acompletion.value, + CallTypes.responses.value, + CallTypes.aresponses.value, + CallTypes.anthropic_messages.value, + CallTypes.generate_content.value, + CallTypes.agenerate_content.value, + CallTypes.call_mcp_tool.value, +}) + + +def extract_request_tool_names(route: str, data: dict) -> List[str]: + """ + Extract tool names from the request body for the given route. + Uses guardrail translation handlers when available, else standalone extractors + for generate_content and MCP. Returns [] for non-tool-capable routes or when + no tools are present. + """ + call_types = get_call_types_for_route(route) + if not call_types: + return [] + _register_standalone_extractors() + mappings = load_guardrail_translation_mappings() + for call_type in call_types: + if not isinstance(call_type, CallTypes): + continue + if call_type.value not in TOOL_CAPABLE_CALL_TYPES: + continue + if call_type.value in STANDALONE_EXTRACTORS: + return STANDALONE_EXTRACTORS[call_type.value](data) + handler_cls = mappings.get(call_type) + if handler_cls is not None: + names = handler_cls().extract_request_tool_names(data) + if names: + return names + return [] diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 68f1ab114bf..32eab99fb99 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1091,6 +1091,15 @@ async def add_litellm_data_to_request( # noqa: PLR0915 ] = user_api_key_dict.user_max_budget data[_metadata_variable_name]["user_api_key_metadata"] = user_api_key_dict.metadata + data[_metadata_variable_name]["user_api_key_team_metadata"] = ( + user_api_key_dict.team_metadata + ) + data[_metadata_variable_name]["user_api_key_object_permission_id"] = ( + getattr(user_api_key_dict, "object_permission_id", None) + ) + data[_metadata_variable_name]["user_api_key_team_object_permission_id"] = ( + getattr(user_api_key_dict, "team_object_permission_id", None) + ) data[_metadata_variable_name]["headers"] = _headers data[_metadata_variable_name]["endpoint"] = str(request.url) diff --git a/litellm/proxy/management_endpoints/tool_management_endpoints.py b/litellm/proxy/management_endpoints/tool_management_endpoints.py index 89880c9a4ec..7fdd3475c04 100644 --- a/litellm/proxy/management_endpoints/tool_management_endpoints.py +++ b/litellm/proxy/management_endpoints/tool_management_endpoints.py @@ -4,27 +4,87 @@ TOOL POLICY MANAGEMENT All /tool management endpoints GET /v1/tool/list - List all discovered tools and their policies +GET /v1/tool/policy/options - List available input/output policy options with descriptions GET /v1/tool/{tool_name} - Get a single tool's details -POST /v1/tool/policy - Update the call_policy for a tool +POST /v1/tool/policy - Update the input_policy / output_policy for a tool """ -from typing import Optional +import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, List, Optional -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query + +if TYPE_CHECKING: + from litellm.proxy.utils import PrismaClient from litellm._logging import verbose_proxy_logger from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.types.tool_management import ( LiteLLM_ToolTableRow, - ToolCallPolicy, + ToolDetailResponse, + ToolInputPolicy, ToolListResponse, + ToolOutputPolicy, + ToolPolicyOption, + ToolPolicyOptionsResponse, ToolPolicyUpdateRequest, ToolPolicyUpdateResponse, + ToolUsageLogEntry, + ToolUsageLogsResponse, ) router = APIRouter() +TOOL_POLICY_OPTIONS = ToolPolicyOptionsResponse( + input_policies=[ + ToolPolicyOption( + value="untrusted", + label="Untrusted", + description="Tool accepts any input, including data from untrusted tool outputs. Default for newly discovered tools.", + ), + ToolPolicyOption( + value="trusted", + label="Trusted", + description="Tool requires trusted input. Blocked if the conversation contains output from any tool with output_policy=untrusted.", + ), + ToolPolicyOption( + value="blocked", + label="Blocked", + description="Tool is completely prohibited. Any attempt to call it is rejected.", + ), + ], + output_policies=[ + ToolPolicyOption( + value="untrusted", + label="Untrusted", + description="Tool output may contain unsafe content (prompt injection, risky code). Downstream tools with input_policy=trusted will be blocked.", + ), + ToolPolicyOption( + value="trusted", + label="Trusted", + description="Tool output is verified safe. Will not trigger trust-chain blocks on downstream tools.", + ), + ], +) + + +@router.get( + "/v1/tool/policy/options", + tags=["tool management"], + dependencies=[Depends(user_api_key_auth)], + response_model=ToolPolicyOptionsResponse, +) +async def get_tool_policy_options( + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Return the available input and output policy options with descriptions. + Static data — no DB call. + """ + return TOOL_POLICY_OPTIONS + @router.get( "/v1/tool/list", @@ -33,14 +93,14 @@ router = APIRouter() response_model=ToolListResponse, ) async def list_tools( - call_policy: Optional[ToolCallPolicy] = None, + input_policy: Optional[ToolInputPolicy] = None, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - List all auto-discovered tools and their call policies. + List all auto-discovered tools and their policies. Parameters: - - call_policy: Optional filter — one of "trusted", "untrusted", "dual_llm", "blocked" + - input_policy: Optional filter — one of "trusted", "untrusted", "blocked" """ from litellm.proxy.db.tool_registry_writer import list_tools as db_list_tools from litellm.proxy.proxy_server import prisma_client @@ -51,13 +111,201 @@ async def list_tools( ) try: - tools = await db_list_tools(prisma_client=prisma_client, call_policy=call_policy) + tools = await db_list_tools( + prisma_client=prisma_client, input_policy=input_policy + ) return ToolListResponse(tools=tools, total=len(tools)) except Exception as e: verbose_proxy_logger.exception("Error listing tools: %s", e) raise HTTPException(status_code=500, detail=str(e)) +@router.get( + "/v1/tool/{tool_name:path}/detail", + tags=["tool management"], + dependencies=[Depends(user_api_key_auth)], + response_model=ToolDetailResponse, +) +async def get_tool_detail( + tool_name: str, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get a single tool with its policy overrides (for UI detail view). + """ + from litellm.proxy.db.tool_registry_writer import get_tool as db_get_tool + from litellm.proxy.db.tool_registry_writer import list_overrides_for_tool + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException( + status_code=500, detail=CommonProxyErrors.db_not_connected_error.value + ) + + try: + tool = await db_get_tool(prisma_client=prisma_client, tool_name=tool_name) + if tool is None: + raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") + overrides = await list_overrides_for_tool( + prisma_client=prisma_client, tool_name=tool_name + ) + return ToolDetailResponse(tool=tool, overrides=overrides) + except HTTPException: + raise + except Exception as e: + verbose_proxy_logger.exception("Error getting tool detail: %s", e) + raise HTTPException(status_code=500, detail=str(e)) + + +def _input_snippet_for_tool_log(sl: Any, max_len: int = 200) -> Optional[str]: + """Short snippet from messages or proxy_server_request for tool usage log row.""" + if sl is None: + return None + messages = getattr(sl, "messages", None) + if messages is not None: + s = _snippet_str(messages, max_len) + if s: + return s + psr = getattr(sl, "proxy_server_request", None) + if not psr: + return None + if isinstance(psr, str): + import json + + try: + psr = json.loads(psr) + except Exception: + return _snippet_str(psr, max_len) + if isinstance(psr, dict): + msgs = psr.get("messages") + if msgs is None and isinstance(psr.get("body"), dict): + msgs = psr["body"].get("messages") + s = _snippet_str(msgs, max_len) + if s: + return s + return _snippet_str(psr, max_len) + + +def _snippet_str(text: Any, max_len: int = 200) -> Optional[str]: + if text is None: + return None + if isinstance(text, str): + s = text + elif isinstance(text, list): + parts = [] + for item in text: + if isinstance(item, dict) and "content" in item: + c = item["content"] + parts.append(c if isinstance(c, str) else str(c)) + else: + parts.append(str(item)) + s = " ".join(parts) + else: + s = str(text) + if not s or s == "{}": + return None + return (s[:max_len] + "...") if len(s) > max_len else s + + +@router.get( + "/v1/tool/{tool_name:path}/logs", + tags=["tool management"], + dependencies=[Depends(user_api_key_auth)], + response_model=ToolUsageLogsResponse, +) +async def get_tool_usage_logs( + tool_name: str, + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=100), + start_date: Optional[str] = Query(None, description="YYYY-MM-DD"), + end_date: Optional[str] = Query(None, description="YYYY-MM-DD"), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Return paginated spend logs for requests that used this tool (from SpendLogToolIndex). + """ + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException( + status_code=500, detail=CommonProxyErrors.db_not_connected_error.value + ) + + try: + where: dict = {"tool_name": tool_name} + if start_date or end_date: + start_time_filter: Optional[datetime] = None + end_time_filter: Optional[datetime] = None + if start_date: + try: + start_time_filter = datetime.strptime( + start_date + "T00:00:00", "%Y-%m-%dT%H:%M:%S" + ).replace(tzinfo=timezone.utc) + except ValueError: + pass + if end_date: + try: + end_time_filter = datetime.strptime( + end_date + "T23:59:59", "%Y-%m-%dT%H:%M:%S" + ).replace(tzinfo=timezone.utc) + except ValueError: + pass + if start_time_filter is not None or end_time_filter is not None: + where["start_time"] = {} + if start_time_filter is not None: + where["start_time"]["gte"] = start_time_filter + if end_time_filter is not None: + where["start_time"]["lte"] = end_time_filter + + total = await prisma_client.db.litellm_spendlogtoolindex.count(where=where) + index_rows = await prisma_client.db.litellm_spendlogtoolindex.find_many( + where=where, + order={"start_time": "desc"}, + skip=(page - 1) * page_size, + take=page_size, + ) + request_ids = [r.request_id for r in index_rows] + if not request_ids: + return ToolUsageLogsResponse( + logs=[], total=total, page=page, page_size=page_size + ) + + spend_logs = await prisma_client.db.litellm_spendlogs.find_many( + where={"request_id": {"in": request_ids}} + ) + log_by_id = {s.request_id: s for s in spend_logs} + + logs_out: List[ToolUsageLogEntry] = [] + for r in index_rows: + sl = log_by_id.get(r.request_id) + if not sl: + continue + ts = ( + sl.startTime.isoformat() + if hasattr(sl.startTime, "isoformat") + else str(sl.startTime) + ) + logs_out.append( + ToolUsageLogEntry( + id=sl.request_id, + timestamp=ts, + model=getattr(sl, "model", None) or None, + spend=getattr(sl, "spend", None), + total_tokens=getattr(sl, "total_tokens", None), + input_snippet=_input_snippet_for_tool_log(sl), + ) + ) + + return ToolUsageLogsResponse( + logs=logs_out, total=total, page=page, page_size=page_size + ) + except HTTPException: + raise + except Exception as e: + verbose_proxy_logger.exception("Error getting tool usage logs: %s", e) + raise HTTPException(status_code=500, detail=str(e)) + + @router.get( "/v1/tool/{tool_name:path}", tags=["tool management"], @@ -70,9 +318,6 @@ async def get_tool( ): """ Get details for a single tool. - - Parameters: - - tool_name: The tool name (supports namespaced names with slashes) """ from litellm.proxy.db.tool_registry_writer import get_tool as db_get_tool from litellm.proxy.proxy_server import prisma_client @@ -85,9 +330,7 @@ async def get_tool( try: tool = await db_get_tool(prisma_client=prisma_client, tool_name=tool_name) if tool is None: - raise HTTPException( - status_code=404, detail=f"Tool '{tool_name}' not found" - ) + raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") return tool except HTTPException: raise @@ -96,6 +339,80 @@ async def get_tool( raise HTTPException(status_code=500, detail=str(e)) +async def _resolve_key_hash_to_object_permission_id( + prisma_client: "PrismaClient", + key_hash: str, +) -> Optional[str]: + """Resolve key (hash or raw) to object_permission_id; create permission if key has none.""" + from litellm.proxy.proxy_server import hash_token + + hashed = key_hash if "sk-" not in (key_hash or "") else hash_token(key_hash) + if not hashed: + return None + row = await prisma_client.db.litellm_verificationtoken.find_unique( + where={"token": hashed} + ) + if row is None: + return None + op_id = getattr(row, "object_permission_id", None) + if op_id: + return op_id + new_id = str(uuid.uuid4()) + await prisma_client.db.litellm_objectpermissiontable.create( + data={"object_permission_id": new_id, "blocked_tools": []} + ) + updated_count = await prisma_client.db.litellm_verificationtoken.update_many( + where={"token": hashed, "object_permission_id": None}, + data={"object_permission_id": new_id}, + ) + if updated_count == 0: + await prisma_client.db.litellm_objectpermissiontable.delete( + where={"object_permission_id": new_id} + ) + row = await prisma_client.db.litellm_verificationtoken.find_unique( + where={"token": hashed} + ) + return getattr(row, "object_permission_id", None) if row else None + return new_id + + +async def _resolve_team_id_to_object_permission_id( + prisma_client: "PrismaClient", + team_id: str, +) -> Optional[str]: + """Resolve team_id to object_permission_id; create permission if team has none.""" + if not team_id or not team_id.strip(): + return None + team_id_clean = team_id.strip() + row = await prisma_client.db.litellm_teamtable.find_unique( + where={"team_id": team_id_clean}, + select={"object_permission_id": True}, + ) + if row is None: + return None + op_id = getattr(row, "object_permission_id", None) + if op_id: + return op_id + new_id = str(uuid.uuid4()) + await prisma_client.db.litellm_objectpermissiontable.create( + data={"object_permission_id": new_id, "blocked_tools": []} + ) + updated_count = await prisma_client.db.litellm_teamtable.update_many( + where={"team_id": team_id_clean, "object_permission_id": None}, + data={"object_permission_id": new_id}, + ) + if updated_count == 0: + await prisma_client.db.litellm_objectpermissiontable.delete( + where={"object_permission_id": new_id} + ) + row = await prisma_client.db.litellm_teamtable.find_unique( + where={"team_id": team_id_clean}, + select={"object_permission_id": True}, + ) + return getattr(row, "object_permission_id", None) if row else None + return new_id + + @router.post( "/v1/tool/policy", tags=["tool management"], @@ -107,15 +424,20 @@ async def update_tool_policy( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - Set the call policy for a tool. + Set the input_policy and/or output_policy for a tool (global), or block for a specific team/key (override). Parameters: - tool_name: str - The tool to update - - call_policy: "trusted" | "untrusted" | "dual_llm" | "blocked" - - Setting a tool to "blocked" will cause the ToolPolicyGuardrail to remove - that tool_call from LLM responses before returning them to the client. + - input_policy: optional - "trusted" | "untrusted" | "blocked" + - output_policy: optional - "trusted" | "untrusted" + - team_id: optional - if set, create/update override for this team only + - key_hash: optional - if set, create/update override for this key only """ + from litellm.proxy.db.tool_registry_writer import ( + add_tool_to_object_permission_blocked, + get_tool_policy_registry, + remove_tool_from_object_permission_blocked, + ) from litellm.proxy.db.tool_registry_writer import ( update_tool_policy as db_update_tool_policy, ) @@ -127,19 +449,80 @@ async def update_tool_policy( ) try: + if data.team_id is not None or data.key_hash is not None: + if data.team_id is not None and data.key_hash is not None: + raise HTTPException( + status_code=400, + detail="Provide either team_id or key_hash, not both", + ) + if data.key_hash is not None: + op_id = await _resolve_key_hash_to_object_permission_id( + prisma_client, data.key_hash + ) + else: + op_id = await _resolve_team_id_to_object_permission_id( + prisma_client, data.team_id or "" + ) + if op_id is None: + raise HTTPException( + status_code=404, + detail="Key or team not found for the given identifier", + ) + is_blocking = data.input_policy == "blocked" + if is_blocking: + ok = await add_tool_to_object_permission_blocked( + prisma_client=prisma_client, + object_permission_id=op_id, + tool_name=data.tool_name, + ) + else: + ok = await remove_tool_from_object_permission_blocked( + prisma_client=prisma_client, + object_permission_id=op_id, + tool_name=data.tool_name, + ) + if not ok: + raise HTTPException( + status_code=500, + detail=f"Failed to update policy override for tool '{data.tool_name}'", + ) + registry = get_tool_policy_registry() + if registry.is_initialized(): + await registry.sync_tool_policy_from_db(prisma_client) + return ToolPolicyUpdateResponse( + tool_name=data.tool_name, + input_policy=data.input_policy, + output_policy=data.output_policy, + updated=True, + team_id=data.team_id, + key_hash=data.key_hash, + ) + + if data.input_policy is None and data.output_policy is None: + raise HTTPException( + status_code=400, + detail="At least one of input_policy or output_policy must be provided", + ) + updated = await db_update_tool_policy( prisma_client=prisma_client, tool_name=data.tool_name, - call_policy=data.call_policy, updated_by=user_api_key_dict.user_id, + input_policy=data.input_policy, + output_policy=data.output_policy, ) if updated is None: raise HTTPException( - status_code=500, detail=f"Failed to update policy for tool '{data.tool_name}'" + status_code=500, + detail=f"Failed to update policy for tool '{data.tool_name}'", ) + registry = get_tool_policy_registry() + if registry.is_initialized(): + await registry.sync_tool_policy_from_db(prisma_client) return ToolPolicyUpdateResponse( tool_name=updated.tool_name, - call_policy=updated.call_policy, + input_policy=updated.input_policy, + output_policy=updated.output_policy, updated=True, ) except HTTPException: @@ -147,3 +530,77 @@ async def update_tool_policy( except Exception as e: verbose_proxy_logger.exception("Error updating tool policy: %s", e) raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete( + "/v1/tool/{tool_name:path}/overrides", + tags=["tool management"], + dependencies=[Depends(user_api_key_auth)], +) +async def delete_tool_policy_override( + tool_name: str, + team_id: Optional[str] = Query( + None, description="Team ID of the override to remove" + ), + key_hash: Optional[str] = Query( + None, description="Key hash of the override to remove" + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Remove a policy override for a tool. Specify the override by team_id or key_hash + (exactly one required). + """ + from litellm.proxy.db.tool_registry_writer import ( + get_tool_policy_registry, + remove_tool_from_object_permission_blocked, + ) + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException( + status_code=500, detail=CommonProxyErrors.db_not_connected_error.value + ) + if team_id is None and key_hash is None: + raise HTTPException( + status_code=400, + detail="At least one of team_id or key_hash is required to identify the override", + ) + if team_id is not None and key_hash is not None: + raise HTTPException( + status_code=400, + detail="Provide either team_id or key_hash, not both", + ) + try: + if key_hash is not None: + op_id = await _resolve_key_hash_to_object_permission_id( + prisma_client, key_hash + ) + else: + op_id = await _resolve_team_id_to_object_permission_id( + prisma_client, team_id or "" + ) + if op_id is None: + raise HTTPException( + status_code=404, + detail="Key or team not found for the given identifier", + ) + deleted = await remove_tool_from_object_permission_blocked( + prisma_client=prisma_client, + object_permission_id=op_id, + tool_name=tool_name, + ) + if not deleted: + raise HTTPException( + status_code=404, + detail=f"No override found for tool '{tool_name}' with the given scope", + ) + registry = get_tool_policy_registry() + if registry.is_initialized(): + await registry.sync_tool_policy_from_db(prisma_client) + return {"deleted": True, "tool_name": tool_name} + except HTTPException: + raise + except Exception as e: + verbose_proxy_logger.exception("Error deleting tool policy override: %s", e) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index b3d707b1aa2..6a2b0accb0e 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4411,6 +4411,9 @@ class ProxyConfig: if self._should_load_db_object(object_type="search_tools"): await self._init_search_tools_in_db(prisma_client=prisma_client) + if self._should_load_db_object(object_type="tools"): + await self._init_tool_policy_in_db(prisma_client=prisma_client) + if self._should_load_db_object(object_type="model_cost_map"): await self._check_and_reload_model_cost_map(prisma_client=prisma_client) @@ -4847,6 +4850,24 @@ class ProxyConfig: ) ) + async def _init_tool_policy_in_db(self, prisma_client: PrismaClient): + """ + Initialize tool policy from database into the in-memory registry. + Synced periodically by add_deployment -> _init_non_llm_objects_in_db. + """ + from litellm.proxy.db.tool_registry_writer import get_tool_policy_registry + + try: + registry = get_tool_policy_registry() + await registry.sync_tool_policy_from_db(prisma_client=prisma_client) + verbose_proxy_logger.debug("Successfully synced tool policy from DB") + except Exception as e: + verbose_proxy_logger.exception( + "litellm.proxy.proxy_server.py::ProxyConfig:_init_tool_policy_in_db - {}".format( + str(e) + ) + ) + async def _init_vector_stores_in_db(self, prisma_client: PrismaClient): from litellm.vector_stores.vector_store_registry import VectorStoreRegistry @@ -10577,6 +10598,12 @@ async def async_queue_request( data["metadata"]["user_api_key_team_id"] = getattr( user_api_key_dict, "team_id", None ) + data["metadata"]["user_api_key_object_permission_id"] = getattr( + user_api_key_dict, "object_permission_id", None + ) + data["metadata"]["user_api_key_team_object_permission_id"] = getattr( + user_api_key_dict, "team_object_permission_id", None + ) data["metadata"]["endpoint"] = str(request.url) global user_temperature, user_request_timeout, user_max_tokens, user_api_base @@ -11093,9 +11120,7 @@ async def get_favicon(): if favicon_url.startswith(("http://", "https://")): try: - from litellm.llms.custom_httpx.http_handler import ( - get_async_httpx_client, - ) + from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.types.llms.custom_http import httpxSpecialProvider async_client = get_async_httpx_client( diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index e0b28a4e012..25ee2750548 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -260,6 +260,7 @@ model LiteLLM_ObjectPermissionTable { vector_stores String[] @default([]) agents String[] @default([]) agent_access_groups String[] @default([]) + blocked_tools String[] @default([]) // Tool names blocked for any key/team/user with this permission teams LiteLLM_TeamTable[] projects LiteLLM_ProjectTable[] verification_tokens LiteLLM_VerificationToken[] @@ -928,6 +929,16 @@ model LiteLLM_SpendLogGuardrailIndex { @@index([policy_id, start_time]) } +// Index for fast "last N logs for tool" from SpendLogs – see how a tool is called in production +model LiteLLM_SpendLogToolIndex { + request_id String + tool_name String // matches LiteLLM_ToolTable.tool_name; join for input_policy/output_policy etc. + start_time DateTime + + @@id([request_id, tool_name]) + @@index([tool_name, start_time]) +} + // Prompt table for storing prompt configurations model LiteLLM_PromptTable { id String @id @default(uuid()) @@ -1065,23 +1076,27 @@ model LiteLLM_PolicyAttachmentTable { updated_by String? } -// Global tool registry - auto-discovered from LLM responses; admins set call_policy here +// Global tool registry - auto-discovered from LLM responses; admins set input/output policies here model LiteLLM_ToolTable { - tool_id String @id @default(uuid()) - tool_name String @unique // e.g. "huggingface_remote-mcp__dynamic_space" - origin String? // MCP server name or "user_defined" - call_policy String @default("untrusted") // "trusted" | "untrusted" | "dual_llm" | "blocked" - call_count Int @default(0) // cumulative number of times this tool was seen - assignments Json? @default("{}") - key_hash String? // hash of the virtual key that first called this tool - team_id String? // team that first called this tool - key_alias String? // human-readable alias of the virtual key - created_at DateTime @default(now()) - created_by String? - updated_at DateTime @default(now()) @updatedAt - updated_by String? + tool_id String @id @default(uuid()) + tool_name String @unique // e.g. "huggingface_remote-mcp__dynamic_space" + origin String? // MCP server name or "user_defined" + input_policy String @default("untrusted") // "trusted" | "untrusted" | "blocked" + output_policy String @default("untrusted") // "trusted" | "untrusted" + call_count Int @default(0) // cumulative number of times this tool was seen + assignments Json? @default("{}") + key_hash String? // hash of the virtual key that first called this tool + team_id String? // team that first called this tool + key_alias String? // human-readable alias of the virtual key + user_agent String? // user-agent of the first request that discovered this tool + last_used_at DateTime? // timestamp of the most recent call + created_at DateTime @default(now()) + created_by String? + updated_at DateTime @default(now()) @updatedAt + updated_by String? - @@index([call_policy]) + @@index([input_policy]) + @@index([output_policy]) @@index([team_id]) } diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index afcdd9d0c50..e6da95bb78f 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -3583,8 +3583,9 @@ class PrismaClient: def _get_engine_pid(self) -> int: try: engine = self.db._original_prisma._engine # type: ignore[attr-defined] - if engine is not None and engine.process is not None: - return engine.process.pid + process = getattr(engine, "process", None) if engine is not None else None + if process is not None: + return process.pid except (AttributeError, TypeError): pass return 0 @@ -4688,6 +4689,19 @@ async def update_spend_logs_job( guardrail_tracking_err, ) + # Tool usage tracking (same batch): SpendLogToolIndex for "last N requests for tool X" + try: + from litellm.proxy.db.spend_log_tool_index import process_spend_logs_tool_usage + await process_spend_logs_tool_usage( + prisma_client=prisma_client, + logs_to_process=logs_to_process, + ) + except Exception as tool_tracking_err: + verbose_proxy_logger.warning( + "Spend tracking - tool usage tracking failed (non-fatal): %s", + tool_tracking_err, + ) + async def _monitor_spend_logs_queue( prisma_client: PrismaClient, diff --git a/litellm/types/tool_management.py b/litellm/types/tool_management.py index 8704ff27759..1c5e1df9e9a 100644 --- a/litellm/types/tool_management.py +++ b/litellm/types/tool_management.py @@ -5,21 +5,27 @@ Pydantic models for Tool Policy management endpoints. from datetime import datetime from typing import Dict, List, Literal, Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field ToolCallPolicy = Literal["trusted", "untrusted", "dual_llm", "blocked"] +ToolInputPolicy = Literal["trusted", "untrusted", "blocked"] +ToolOutputPolicy = Literal["trusted", "untrusted"] + class LiteLLM_ToolTableRow(BaseModel): tool_id: str tool_name: str origin: Optional[str] = None - call_policy: ToolCallPolicy = "untrusted" + input_policy: ToolInputPolicy = "untrusted" + output_policy: ToolOutputPolicy = "untrusted" call_count: int = 0 assignments: Optional[Dict] = None key_hash: Optional[str] = None team_id: Optional[str] = None key_alias: Optional[str] = None + user_agent: Optional[str] = None + last_used_at: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None created_by: Optional[str] = None @@ -33,10 +39,62 @@ class ToolListResponse(BaseModel): class ToolPolicyUpdateRequest(BaseModel): tool_name: str - call_policy: ToolCallPolicy + input_policy: Optional[ToolInputPolicy] = None + output_policy: Optional[ToolOutputPolicy] = None + team_id: Optional[str] = None + key_hash: Optional[str] = None + key_alias: Optional[str] = None class ToolPolicyUpdateResponse(BaseModel): tool_name: str - call_policy: ToolCallPolicy + input_policy: Optional[ToolInputPolicy] = None + output_policy: Optional[ToolOutputPolicy] = None updated: bool + team_id: Optional[str] = None + key_hash: Optional[str] = None + + +class ToolPolicyOverrideRow(BaseModel): + override_id: str + tool_name: str + team_id: Optional[str] = None + key_hash: Optional[str] = None + input_policy: ToolInputPolicy = "blocked" + key_alias: Optional[str] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class ToolPolicyOption(BaseModel): + value: str + label: str + description: str + + +class ToolPolicyOptionsResponse(BaseModel): + input_policies: List[ToolPolicyOption] + output_policies: List[ToolPolicyOption] + + +class ToolDetailResponse(BaseModel): + tool: LiteLLM_ToolTableRow + overrides: List[ToolPolicyOverrideRow] = Field(default_factory=list) + + +class ToolUsageLogEntry(BaseModel): + """One spend log row for a tool call (for UI "recent logs" table).""" + + id: str # request_id + timestamp: str + model: Optional[str] = None + spend: Optional[float] = None + total_tokens: Optional[int] = None + input_snippet: Optional[str] = None + + +class ToolUsageLogsResponse(BaseModel): + logs: List[ToolUsageLogEntry] + total: int + page: int + page_size: int diff --git a/schema.prisma b/schema.prisma index e0b28a4e012..25ee2750548 100644 --- a/schema.prisma +++ b/schema.prisma @@ -260,6 +260,7 @@ model LiteLLM_ObjectPermissionTable { vector_stores String[] @default([]) agents String[] @default([]) agent_access_groups String[] @default([]) + blocked_tools String[] @default([]) // Tool names blocked for any key/team/user with this permission teams LiteLLM_TeamTable[] projects LiteLLM_ProjectTable[] verification_tokens LiteLLM_VerificationToken[] @@ -928,6 +929,16 @@ model LiteLLM_SpendLogGuardrailIndex { @@index([policy_id, start_time]) } +// Index for fast "last N logs for tool" from SpendLogs – see how a tool is called in production +model LiteLLM_SpendLogToolIndex { + request_id String + tool_name String // matches LiteLLM_ToolTable.tool_name; join for input_policy/output_policy etc. + start_time DateTime + + @@id([request_id, tool_name]) + @@index([tool_name, start_time]) +} + // Prompt table for storing prompt configurations model LiteLLM_PromptTable { id String @id @default(uuid()) @@ -1065,23 +1076,27 @@ model LiteLLM_PolicyAttachmentTable { updated_by String? } -// Global tool registry - auto-discovered from LLM responses; admins set call_policy here +// Global tool registry - auto-discovered from LLM responses; admins set input/output policies here model LiteLLM_ToolTable { - tool_id String @id @default(uuid()) - tool_name String @unique // e.g. "huggingface_remote-mcp__dynamic_space" - origin String? // MCP server name or "user_defined" - call_policy String @default("untrusted") // "trusted" | "untrusted" | "dual_llm" | "blocked" - call_count Int @default(0) // cumulative number of times this tool was seen - assignments Json? @default("{}") - key_hash String? // hash of the virtual key that first called this tool - team_id String? // team that first called this tool - key_alias String? // human-readable alias of the virtual key - created_at DateTime @default(now()) - created_by String? - updated_at DateTime @default(now()) @updatedAt - updated_by String? + tool_id String @id @default(uuid()) + tool_name String @unique // e.g. "huggingface_remote-mcp__dynamic_space" + origin String? // MCP server name or "user_defined" + input_policy String @default("untrusted") // "trusted" | "untrusted" | "blocked" + output_policy String @default("untrusted") // "trusted" | "untrusted" + call_count Int @default(0) // cumulative number of times this tool was seen + assignments Json? @default("{}") + key_hash String? // hash of the virtual key that first called this tool + team_id String? // team that first called this tool + key_alias String? // human-readable alias of the virtual key + user_agent String? // user-agent of the first request that discovered this tool + last_used_at DateTime? // timestamp of the most recent call + created_at DateTime @default(now()) + created_by String? + updated_at DateTime @default(now()) @updatedAt + updated_by String? - @@index([call_policy]) + @@index([input_policy]) + @@index([output_policy]) @@index([team_id]) } diff --git a/scripts/test_tool_allowlist_script.py b/scripts/test_tool_allowlist_script.py new file mode 100644 index 00000000000..75a50d09b84 --- /dev/null +++ b/scripts/test_tool_allowlist_script.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Standalone script to test tool allowlist enforcement and tool name extraction. + +Run from repo root: + poetry run python scripts/test_tool_allowlist_script.py + +Or run the unit tests: + poetry run pytest tests/test_litellm/proxy/test_tools_allowlist_enforcement.py -v +""" + +import asyncio +import sys +from pathlib import Path + +# Ensure repo root is on path +repo_root = Path(__file__).resolve().parent.parent +if str(repo_root) not in sys.path: + sys.path.insert(0, str(repo_root)) + + +def test_extraction(): + """Test extract_request_tool_names for each API shape.""" + from litellm.proxy.guardrails.tool_name_extraction import extract_request_tool_names + + cases = [ + ("OpenAI chat tools", "/v1/chat/completions", {"tools": [{"type": "function", "function": {"name": "get_weather"}}]}), + ("OpenAI chat functions", "/v1/chat/completions", {"functions": [{"name": "run_sql"}]}), + ("OpenAI responses function", "/v1/responses", {"tools": [{"type": "function", "name": "get_current_weather"}]}), + ("OpenAI responses MCP", "/v1/responses", {"tools": [{"type": "mcp", "server_label": "dmcp"}]}), + ("Anthropic", "/v1/messages", {"tools": [{"name": "get_weather"}, {"name": "run_sql"}]}), + ("Google generateContent", "/generate_content", {"tools": [{"functionDeclarations": [{"name": "schedule_meeting"}]}]}), + ("MCP call_tool", "/mcp/call_tool", {"name": "my_tool", "arguments": {}}), + ("Non-tool route", "/v1/embeddings", {"tools": [{"type": "function", "function": {"name": "x"}}]}), + ] + print("=== extract_request_tool_names(route, data) ===\n") + for label, route, data in cases: + names = extract_request_tool_names(route, data) + print(f" {label}: {names}") + print() + + +async def test_check_tools_allowlist(): + """Test check_tools_allowlist with mock tokens.""" + from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth + from litellm.proxy.auth.auth_checks import check_tools_allowlist + + def token(metadata=None, team_metadata=None): + return UserAPIKeyAuth( + api_key="test-key", + user_id="user", + team_id="team", + org_id=None, + models=["*"], + metadata=metadata or {}, + team_metadata=team_metadata or {}, + ) + + print("=== check_tools_allowlist (auth) ===\n") + + # No allowlist -> pass + await check_tools_allowlist( + request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]}, + valid_token=token(), + team_object=None, + route="/v1/chat/completions", + ) + print(" No allowlist, body has tools: PASS") + + # Allowed tool -> pass + await check_tools_allowlist( + request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]}, + valid_token=token(metadata={"allowed_tools": ["get_weather"]}), + team_object=None, + route="/v1/chat/completions", + ) + print(" allowed_tools=['get_weather'], body has get_weather: PASS") + + # Disallowed tool -> raise + try: + await check_tools_allowlist( + request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]}, + valid_token=token(metadata={"allowed_tools": ["other_tool"]}), + team_object=None, + route="/v1/chat/completions", + ) + print(" DISALLOWED: expected ProxyException") + except ProxyException as e: + if e.type == ProxyErrorTypes.tool_access_denied: + print(" allowed_tools=['other_tool'], body has get_weather: PASS (raised tool_access_denied)") + else: + print(f" Unexpected ProxyException type: {e.type}") + except Exception as e: + print(f" Unexpected: {e}") + + # Team allowlist when key empty + await check_tools_allowlist( + request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]}, + valid_token=token(team_metadata={"allowed_tools": ["get_weather"]}), + team_object=None, + route="/v1/chat/completions", + ) + print(" team_metadata.allowed_tools=['get_weather']: PASS") + print() + + +def main(): + print("Tool allowlist / tool name extraction – script checks\n") + test_extraction() + asyncio.run(test_check_tools_allowlist()) + print("Done. For full unit tests run:") + print(" poetry run pytest tests/test_litellm/proxy/test_tools_allowlist_enforcement.py -v") + + +if __name__ == "__main__": + main() diff --git a/tests/test_litellm/proxy/db/test_tool_registry_writer.py b/tests/test_litellm/proxy/db/test_tool_registry_writer.py index 44f9e32058a..1b1ee7afcba 100644 --- a/tests/test_litellm/proxy/db/test_tool_registry_writer.py +++ b/tests/test_litellm/proxy/db/test_tool_registry_writer.py @@ -1,6 +1,6 @@ """ Unit tests for tool_registry_writer.py — uses a mock prisma client -that exposes execute_raw / query_raw (matching the actual raw-SQL implementation). +that exposes litellm_tooltable.upsert / find_many / find_unique. """ import os @@ -13,21 +13,28 @@ import pytest sys.path.insert(0, os.path.abspath("../../..")) from litellm.proxy.db.tool_registry_writer import ( + ToolPolicyRegistry, batch_upsert_tools, get_tool, + get_tool_policy_registry, get_tools_by_names, list_tools, update_tool_policy, ) -def _make_prisma(query_rows=None): - """Return a minimal mock prisma_client with execute_raw / query_raw.""" - default_row = { +def _mock_row(**kwargs): + """Build a row-like object with real attributes (no MagicMock) for _row_to_model.""" + + class Row: + pass + + default = { "tool_id": "uuid-1", "tool_name": "my_tool", "origin": "user_defined", - "call_policy": "untrusted", + "input_policy": "untrusted", + "output_policy": "untrusted", "call_count": 1, "assignments": {}, "key_hash": None, @@ -38,31 +45,54 @@ def _make_prisma(query_rows=None): "created_by": None, "updated_by": None, } - rows = query_rows if query_rows is not None else [default_row] + default.update(kwargs) + row = Row() + for k, v in default.items(): + setattr(row, k, v) + return row + +def _make_prisma( + *, + upsert_return=None, + find_many_rows=None, + find_unique_row=None, +): + """Return a mock prisma_client with litellm_tooltable.upsert, find_many, find_unique.""" prisma = MagicMock() - prisma.db.execute_raw = AsyncMock(return_value=None) - prisma.db.query_raw = AsyncMock(return_value=rows) + prisma.db.litellm_tooltable = MagicMock() + prisma.db.litellm_tooltable.upsert = AsyncMock(return_value=upsert_return) + prisma.db.litellm_tooltable.find_many = AsyncMock( + return_value=find_many_rows if find_many_rows is not None else [] + ) + prisma.db.litellm_tooltable.find_unique = AsyncMock( + return_value=find_unique_row + ) return prisma @pytest.mark.asyncio -async def test_batch_upsert_tools_calls_execute_raw(): +async def test_batch_upsert_tools_calls_upsert(): prisma = _make_prisma() items = [{"tool_name": "tool_a", "origin": "mcp_server", "created_by": None}] await batch_upsert_tools(prisma, items) - prisma.db.execute_raw.assert_awaited_once() - call_args = prisma.db.execute_raw.call_args - sql = call_args.args[0] - assert "LiteLLM_ToolTable" in sql - assert "ON CONFLICT" in sql + prisma.db.litellm_tooltable.upsert.assert_awaited_once() + call_kw = prisma.db.litellm_tooltable.upsert.call_args.kwargs + assert call_kw["where"] == {"tool_name": "tool_a"} + assert call_kw["data"]["create"]["tool_name"] == "tool_a" + assert call_kw["data"]["create"]["origin"] == "mcp_server" + assert call_kw["data"]["create"]["input_policy"] == "untrusted" + assert call_kw["data"]["create"]["output_policy"] == "untrusted" + assert call_kw["data"]["create"]["call_count"] == 1 + assert call_kw["data"]["update"]["call_count"] == {"increment": 1} + assert "updated_at" in call_kw["data"]["update"] @pytest.mark.asyncio async def test_batch_upsert_tools_empty_list(): prisma = _make_prisma() await batch_upsert_tools(prisma, []) - prisma.db.execute_raw.assert_not_awaited() + prisma.db.litellm_tooltable.upsert.assert_not_awaited() @pytest.mark.asyncio @@ -70,123 +100,120 @@ async def test_batch_upsert_tools_skips_empty_names(): prisma = _make_prisma() items = [{"tool_name": "", "origin": None}, {"tool_name": None}] # type: ignore[list-item] await batch_upsert_tools(prisma, items) - prisma.db.execute_raw.assert_not_awaited() + prisma.db.litellm_tooltable.upsert.assert_not_awaited() @pytest.mark.asyncio -async def test_batch_upsert_multiple_tools_calls_execute_raw_per_tool(): +async def test_batch_upsert_multiple_tools_calls_upsert_per_tool(): prisma = _make_prisma() items = [ {"tool_name": "tool_a", "origin": "mcp_server", "created_by": None}, {"tool_name": "tool_b", "origin": "user_defined", "created_by": "alice"}, ] await batch_upsert_tools(prisma, items) - assert prisma.db.execute_raw.await_count == 2 + assert prisma.db.litellm_tooltable.upsert.await_count == 2 + calls = prisma.db.litellm_tooltable.upsert.call_args_list + assert calls[0].kwargs["where"]["tool_name"] == "tool_a" + assert calls[1].kwargs["where"]["tool_name"] == "tool_b" @pytest.mark.asyncio async def test_list_tools_no_filter(): - row = { - "tool_id": "id1", - "tool_name": "tool_a", - "origin": "mcp", - "call_policy": "untrusted", - "call_count": 5, - "assignments": {}, - "key_hash": None, - "team_id": None, - "key_alias": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), - "created_by": None, - "updated_by": None, - } - prisma = _make_prisma(query_rows=[row]) + row = _mock_row( + tool_id="id1", + tool_name="tool_a", + origin="mcp", + input_policy="untrusted", + output_policy="untrusted", + call_count=5, + ) + prisma = _make_prisma(find_many_rows=[row]) result = await list_tools(prisma) assert len(result) == 1 assert result[0].tool_name == "tool_a" assert result[0].call_count == 5 - prisma.db.query_raw.assert_awaited_once() + prisma.db.litellm_tooltable.find_many.assert_awaited_once() + call_kw = prisma.db.litellm_tooltable.find_many.call_args.kwargs + assert call_kw["where"] == {} + assert call_kw["order"] == {"created_at": "desc"} @pytest.mark.asyncio -async def test_list_tools_with_policy_filter(): - row = { - "tool_id": "id1", - "tool_name": "blocked_tool", - "origin": None, - "call_policy": "blocked", - "call_count": 2, - "assignments": None, - "key_hash": None, - "team_id": None, - "key_alias": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), - "created_by": None, - "updated_by": None, - } - prisma = _make_prisma(query_rows=[row]) - result = await list_tools(prisma, call_policy="blocked") - assert result[0].call_policy == "blocked" - call_args = prisma.db.query_raw.call_args - sql = call_args.args[0] - assert "WHERE call_policy" in sql +async def test_list_tools_with_input_policy_filter(): + row = _mock_row( + tool_id="id1", + tool_name="blocked_tool", + origin=None, + input_policy="blocked", + output_policy="untrusted", + call_count=2, + assignments=None, + ) + prisma = _make_prisma(find_many_rows=[row]) + result = await list_tools(prisma, input_policy="blocked") + assert result[0].input_policy == "blocked" + call_kw = prisma.db.litellm_tooltable.find_many.call_args.kwargs + assert call_kw["where"] == {"input_policy": "blocked"} @pytest.mark.asyncio async def test_get_tool_found(): - prisma = _make_prisma() + row = _mock_row(tool_name="my_tool") + prisma = _make_prisma(find_unique_row=row) result = await get_tool(prisma, "my_tool") assert result is not None assert result.tool_name == "my_tool" - prisma.db.query_raw.assert_awaited_once() + prisma.db.litellm_tooltable.find_unique.assert_awaited_once_with( + where={"tool_name": "my_tool"} + ) @pytest.mark.asyncio async def test_get_tool_not_found(): - prisma = _make_prisma(query_rows=[]) + prisma = _make_prisma(find_unique_row=None) result = await get_tool(prisma, "nonexistent") assert result is None @pytest.mark.asyncio -async def test_update_tool_policy_calls_execute_raw(): - row = { - "tool_id": "uuid-1", - "tool_name": "my_tool", - "origin": "user_defined", - "call_policy": "blocked", - "call_count": 1, - "assignments": {}, - "key_hash": None, - "team_id": None, - "key_alias": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), - "created_by": None, - "updated_by": "admin", - } - prisma = _make_prisma(query_rows=[row]) - result = await update_tool_policy(prisma, "my_tool", "blocked", "admin") +async def test_update_tool_policy_calls_upsert_then_get_tool(): + row = _mock_row( + tool_name="my_tool", + input_policy="blocked", + output_policy="untrusted", + updated_by="admin", + ) + prisma = _make_prisma(find_unique_row=row) + result = await update_tool_policy( + prisma, "my_tool", updated_by="admin", input_policy="blocked" + ) assert result is not None - assert result.call_policy == "blocked" - prisma.db.execute_raw.assert_awaited_once() - call_args = prisma.db.execute_raw.call_args - sql = call_args.args[0] - assert "ON CONFLICT" in sql - assert "call_policy" in sql + assert result.input_policy == "blocked" + prisma.db.litellm_tooltable.upsert.assert_awaited_once() + call_kw = prisma.db.litellm_tooltable.upsert.call_args.kwargs + assert call_kw["where"] == {"tool_name": "my_tool"} + assert call_kw["data"]["update"]["input_policy"] == "blocked" + assert call_kw["data"]["update"]["updated_by"] == "admin" + prisma.db.litellm_tooltable.find_unique.assert_awaited_with( + where={"tool_name": "my_tool"} + ) @pytest.mark.asyncio async def test_get_tools_by_names_returns_policy_map(): rows = [ - {"tool_name": "tool_a", "call_policy": "trusted"}, - {"tool_name": "tool_b", "call_policy": "blocked"}, + _mock_row(tool_name="tool_a", input_policy="trusted", output_policy="untrusted"), + _mock_row(tool_name="tool_b", input_policy="blocked", output_policy="untrusted"), ] - prisma = _make_prisma(query_rows=rows) + prisma = _make_prisma(find_many_rows=rows) result = await get_tools_by_names(prisma, ["tool_a", "tool_b"]) - assert result == {"tool_a": "trusted", "tool_b": "blocked"} + assert result == { + "tool_a": ("trusted", "untrusted"), + "tool_b": ("blocked", "untrusted"), + } + prisma.db.litellm_tooltable.find_many.assert_awaited_once_with( + where={"tool_name": {"in": ["tool_a", "tool_b"]}} + ) @pytest.mark.asyncio @@ -194,4 +221,71 @@ async def test_get_tools_by_names_empty_list(): prisma = _make_prisma() result = await get_tools_by_names(prisma, []) assert result == {} - prisma.db.query_raw.assert_not_awaited() + prisma.db.litellm_tooltable.find_many.assert_not_awaited() + + +# --- ToolPolicyRegistry --- + + +def _mock_tool_row( + tool_name: str, + input_policy: str = "untrusted", + output_policy: str = "untrusted", +): + row = MagicMock() + row.tool_name = tool_name + row.input_policy = input_policy + row.output_policy = output_policy + return row + + +def _mock_perm_row(object_permission_id: str, blocked_tools: list): + row = MagicMock() + row.object_permission_id = object_permission_id + row.blocked_tools = blocked_tools + return row + + +@pytest.mark.asyncio +async def test_tool_policy_registry_sync_and_get_effective_policies(): + """Registry syncs from DB; get_effective_policies returns merged blocked + global.""" + prisma = MagicMock() + prisma.db.litellm_tooltable.find_many = AsyncMock( + return_value=[ + _mock_tool_row("tool_a", input_policy="trusted"), + _mock_tool_row("tool_b", input_policy="blocked"), + _mock_tool_row("tool_c", input_policy="untrusted"), + ] + ) + prisma.db.litellm_objectpermissiontable.find_many = AsyncMock( + return_value=[ + _mock_perm_row("op-key-1", ["tool_a"]), + _mock_perm_row("op-team-1", ["tool_c"]), + ] + ) + registry = get_tool_policy_registry() + await registry.sync_tool_policy_from_db(prisma) + assert registry.is_initialized() + # Key blocked: tool_a. Team blocked: tool_c. Global: tool_b blocked. + result = registry.get_effective_policies( + ["tool_a", "tool_b", "tool_c"], + object_permission_id="op-key-1", + team_object_permission_id="op-team-1", + ) + assert result["tool_a"] == "blocked" + assert result["tool_b"] == "blocked" + assert result["tool_c"] == "blocked" + # No op ids: only global + result_global = registry.get_effective_policies(["tool_a", "tool_b", "tool_c"]) + assert result_global["tool_a"] == "trusted" + assert result_global["tool_b"] == "blocked" + assert result_global["tool_c"] == "untrusted" + + +@pytest.mark.asyncio +async def test_tool_policy_registry_not_initialized_returns_untrusted(): + """When not synced, get_effective_policies still returns untrusted for unknown tools.""" + registry = ToolPolicyRegistry() + assert not registry.is_initialized() + result = registry.get_effective_policies(["unknown_tool"]) + assert result == {"unknown_tool": "untrusted"} diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_policy_guardrail.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_policy_guardrail.py index c6a81efbf0b..943a8d4be75 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_policy_guardrail.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_policy_guardrail.py @@ -12,9 +12,8 @@ from fastapi import HTTPException sys.path.insert(0, os.path.abspath("../../../../../..")) -from litellm.proxy.guardrails.guardrail_hooks.tool_policy.tool_policy_guardrail import ( - ToolPolicyGuardrail, -) +from litellm.proxy.guardrails.guardrail_hooks.tool_policy.tool_policy_guardrail import \ + ToolPolicyGuardrail from litellm.types.guardrails import GuardrailEventHooks @@ -70,10 +69,21 @@ async def test_no_tool_calls_in_response_passes_through(guardrail): assert result is inputs +def _registry_mock(policy_map: dict): + """Return a mock registry with is_initialized=True and get_effective_policies returning policy_map.""" + reg = MagicMock() + reg.is_initialized.return_value = True + reg.get_effective_policies.return_value = policy_map + return reg + + @pytest.mark.asyncio async def test_untrusted_tools_pass_through(guardrail): policy_map = {"search": "untrusted", "read_file": "trusted"} - with patch.object(guardrail, "_get_policies_cached", new=AsyncMock(return_value=policy_map)): + with patch( + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=_registry_mock(policy_map), + ): inputs: Any = _tool_request_inputs(["search", "read_file"]) result = await guardrail.apply_guardrail( inputs=inputs, request_data={}, input_type="request" @@ -84,7 +94,10 @@ async def test_untrusted_tools_pass_through(guardrail): @pytest.mark.asyncio async def test_blocked_tool_in_request_raises_http_exception(guardrail): policy_map = {"dangerous_tool": "blocked"} - with patch.object(guardrail, "_get_policies_cached", new=AsyncMock(return_value=policy_map)): + with patch( + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=_registry_mock(policy_map), + ): inputs: Any = _tool_request_inputs(["dangerous_tool"]) with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( @@ -97,7 +110,10 @@ async def test_blocked_tool_in_request_raises_http_exception(guardrail): @pytest.mark.asyncio async def test_blocked_tool_in_response_raises_http_exception(guardrail): policy_map = {"exfil_tool": "blocked"} - with patch.object(guardrail, "_get_policies_cached", new=AsyncMock(return_value=policy_map)): + with patch( + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=_registry_mock(policy_map), + ): inputs: Any = _tool_response_inputs(["exfil_tool"]) with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( @@ -110,7 +126,10 @@ async def test_blocked_tool_in_response_raises_http_exception(guardrail): @pytest.mark.asyncio async def test_mixed_blocked_and_allowed_raises_for_blocked(guardrail): policy_map = {"safe_tool": "trusted", "bad_tool": "blocked"} - with patch.object(guardrail, "_get_policies_cached", new=AsyncMock(return_value=policy_map)): + with patch( + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=_registry_mock(policy_map), + ): inputs: Any = _tool_request_inputs(["safe_tool", "bad_tool"]) with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( @@ -123,8 +142,11 @@ async def test_mixed_blocked_and_allowed_raises_for_blocked(guardrail): @pytest.mark.asyncio async def test_tool_not_in_db_passes_through(guardrail): - """Tools not found in the DB (no entry) should not be blocked.""" - with patch.object(guardrail, "_get_policies_cached", new=AsyncMock(return_value={})): + """When registry returns no policy (or empty), tools are not blocked.""" + with patch( + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=_registry_mock({}), + ): inputs: Any = _tool_request_inputs(["unknown_tool"]) result = await guardrail.apply_guardrail( inputs=inputs, request_data={}, input_type="request" @@ -133,43 +155,30 @@ async def test_tool_not_in_db_passes_through(guardrail): @pytest.mark.asyncio -async def test_get_policies_cached_uses_cache(guardrail): - """Second call with same tool names should return the cached result.""" - policy_map = {"tool_a": "trusted"} +async def test_registry_not_initialized_passes_through(guardrail): + """When registry is not initialized, no tools are blocked (empty policy map).""" + reg = MagicMock() + reg.is_initialized.return_value = False with patch( - "litellm.proxy.db.tool_registry_writer.get_tools_by_names", - new=AsyncMock(return_value=policy_map), - ) as mock_db, patch( - "litellm.proxy.proxy_server.prisma_client", - new=MagicMock(), + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=reg, ): - # first call — should hit DB - result1 = await guardrail._get_policies_cached(["tool_a"]) - assert result1 == policy_map - - # second call — should hit cache, not DB again - result2 = await guardrail._get_policies_cached(["tool_a"]) - assert result2 == policy_map - - assert mock_db.call_count == 1 - - -@pytest.mark.asyncio -async def test_get_policies_cached_no_prisma(guardrail): - """Without a prisma client, returns empty dict.""" - with patch( - "litellm.proxy.proxy_server.prisma_client", - None, - ): - result = await guardrail._get_policies_cached(["tool_a"]) - assert result == {} + inputs: Any = _tool_request_inputs(["any_tool"]) + result = await guardrail.apply_guardrail( + inputs=inputs, request_data={}, input_type="request" + ) + assert result is inputs + reg.get_effective_policies.assert_not_called() @pytest.mark.asyncio async def test_response_tool_calls_as_objects(guardrail): """tool_calls that are objects (not dicts) with .function.name should work.""" policy_map = {"obj_tool": "blocked"} - with patch.object(guardrail, "_get_policies_cached", new=AsyncMock(return_value=policy_map)): + with patch( + "litellm.proxy.db.tool_registry_writer.get_tool_policy_registry", + return_value=_registry_mock(policy_map), + ): fn = MagicMock() fn.name = "obj_tool" tc = MagicMock() diff --git a/tests/test_litellm/proxy/test_tools_allowlist_enforcement.py b/tests/test_litellm/proxy/test_tools_allowlist_enforcement.py new file mode 100644 index 00000000000..4adc5acde8b --- /dev/null +++ b/tests/test_litellm/proxy/test_tools_allowlist_enforcement.py @@ -0,0 +1,200 @@ +""" +Tests for tool allowlist enforcement (key/team metadata.allowed_tools). + +Covers: +- check_tools_allowlist: allowed, disallowed, no allowlist, non-tool routes +- extract_request_tool_names: OpenAI chat, responses, Anthropic, generate_content, MCP +""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from litellm.proxy._types import (ProxyErrorTypes, ProxyException, + UserAPIKeyAuth) +from litellm.proxy.auth.auth_checks import check_tools_allowlist +from litellm.proxy.guardrails.tool_name_extraction import ( + TOOL_CAPABLE_CALL_TYPES, extract_request_tool_names) + + +def _token(metadata=None, team_metadata=None): + return UserAPIKeyAuth( + api_key="test-key", + user_id="user", + team_id="team", + org_id=None, + models=["*"], + metadata=metadata or {}, + team_metadata=team_metadata or {}, + ) + + +class TestExtractRequestToolNames: + """Test tool name extraction per API format.""" + + def test_openai_chat_tools(self): + data = { + "tools": [ + {"type": "function", "function": {"name": "get_weather"}}, + {"type": "function", "function": {"name": "run_sql"}}, + ] + } + assert extract_request_tool_names("/v1/chat/completions", data) == [ + "get_weather", + "run_sql", + ] + + def test_openai_chat_functions_legacy(self): + data = {"functions": [{"name": "get_weather"}, {"name": "run_sql"}]} + assert extract_request_tool_names("/v1/chat/completions", data) == [ + "get_weather", + "run_sql", + ] + + def test_openai_responses_function_tools(self): + data = { + "tools": [ + {"type": "function", "name": "get_current_weather", "description": "x"}, + ] + } + assert extract_request_tool_names("/v1/responses", data) == [ + "get_current_weather" + ] + + def test_openai_responses_mcp_tools(self): + data = { + "tools": [ + {"type": "mcp", "server_label": "dmcp", "server_url": "http://x"}, + ] + } + assert extract_request_tool_names("/v1/responses", data) == ["dmcp"] + + def test_anthropic_tools(self): + data = {"tools": [{"name": "get_weather"}, {"name": "run_sql"}]} + assert extract_request_tool_names("/v1/messages", data) == [ + "get_weather", + "run_sql", + ] + + def test_generate_content_tools(self): + data = { + "tools": [ + { + "functionDeclarations": [ + {"name": "schedule_meeting", "description": "x"}, + ] + }, + ] + } + assert extract_request_tool_names("/generate_content", data) == [ + "schedule_meeting" + ] + + def test_mcp_call_tool_name(self): + data = {"name": "my_tool", "arguments": {}} + assert extract_request_tool_names("/mcp/call_tool", data) == ["my_tool"] + + def test_mcp_call_tool_mcp_tool_name(self): + data = {"mcp_tool_name": "other_tool"} + assert extract_request_tool_names("/mcp/call_tool", data) == ["other_tool"] + + def test_non_tool_route_returns_empty(self): + data = {"tools": [{"type": "function", "function": {"name": "x"}}]} + assert extract_request_tool_names("/v1/embeddings", data) == [] + + +class TestCheckToolsAllowlist: + """Test allowlist enforcement in auth (no DB in hot path).""" + + @pytest.mark.asyncio + async def test_no_allowlist_passes(self): + token = _token(metadata={}, team_metadata={}) + body = { + "tools": [{"type": "function", "function": {"name": "get_weather"}}] + } + await check_tools_allowlist( + request_body=body, + valid_token=token, + team_object=None, + route="/v1/chat/completions", + ) + + @pytest.mark.asyncio + async def test_allowed_tool_passes(self): + token = _token(metadata={"allowed_tools": ["get_weather"]}) + body = { + "tools": [{"type": "function", "function": {"name": "get_weather"}}] + } + await check_tools_allowlist( + request_body=body, + valid_token=token, + team_object=None, + route="/v1/chat/completions", + ) + + @pytest.mark.asyncio + async def test_disallowed_tool_raises(self): + token = _token(metadata={"allowed_tools": ["other_tool"]}) + body = { + "tools": [{"type": "function", "function": {"name": "get_weather"}}] + } + with pytest.raises(ProxyException) as exc_info: + await check_tools_allowlist( + request_body=body, + valid_token=token, + team_object=None, + route="/v1/chat/completions", + ) + assert exc_info.value.type == ProxyErrorTypes.tool_access_denied + assert "get_weather" in str(exc_info.value.message) + + @pytest.mark.asyncio + async def test_team_allowlist_used_when_key_empty(self): + token = _token( + metadata={}, + team_metadata={"allowed_tools": ["get_weather"]}, + ) + body = { + "tools": [{"type": "function", "function": {"name": "get_weather"}}] + } + await check_tools_allowlist( + request_body=body, + valid_token=token, + team_object=None, + route="/v1/chat/completions", + ) + + @pytest.mark.asyncio + async def test_key_allowlist_overrides_team(self): + token = _token( + metadata={"allowed_tools": ["get_weather"]}, + team_metadata={"allowed_tools": ["other_tool"]}, + ) + body = { + "tools": [{"type": "function", "function": {"name": "get_weather"}}] + } + await check_tools_allowlist( + request_body=body, + valid_token=token, + team_object=None, + route="/v1/chat/completions", + ) + + @pytest.mark.asyncio + async def test_valid_token_none_skips(self): + await check_tools_allowlist( + request_body={"tools": [{"type": "function", "function": {"name": "x"}}]}, + valid_token=None, + team_object=None, + route="/v1/chat/completions", + ) + + @pytest.mark.asyncio + async def test_no_tools_in_body_passes(self): + token = _token(metadata={"allowed_tools": ["get_weather"]}) + await check_tools_allowlist( + request_body={"messages": []}, + valid_token=token, + team_object=None, + route="/v1/chat/completions", + ) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 622c3bf70a9..b927f312df8 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -39,7 +39,7 @@ import UserDashboard from "@/components/user_dashboard"; import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage"; import { ProjectsPage } from "@/components/Projects/ProjectsPage"; import VectorStoreManagement from "@/components/vector_store_management"; -import ToolPolicies from "@/components/ToolPolicies"; +import ToolPoliciesView from "@/components/ToolPoliciesView"; import SpendLogsTable from "@/components/view_logs"; import ViewUserDashboard from "@/components/view_users"; import { ThemeProvider } from "@/contexts/ThemeContext"; @@ -549,7 +549,7 @@ function CreateKeyPageContent() { ) : page == "vector-stores" ? ( ) : page == "tool-policies" ? ( - + ) : page == "guardrails-monitor" ? ( ) : page == "new_usage" ? ( diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx new file mode 100644 index 00000000000..ed0f866acb8 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx @@ -0,0 +1,445 @@ +"use client"; + +import { ArrowLeftOutlined, HistoryOutlined, ToolOutlined } from "@ant-design/icons"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Button, Select, Spin } from "antd"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import TeamDropdown from "@/components/common_components/team_dropdown"; +import { LogViewer } from "@/components/GuardrailsMonitor/LogViewer"; +import type { LogEntry } from "@/components/GuardrailsMonitor/mockData"; +import { PolicySelect } from "@/components/ToolPolicies/PolicySelect"; +import { + deleteToolPolicyOverride, + fetchToolDetail, + fetchToolPolicyOptions, + getToolUsageLogs, + keyListCall, + teamListCall, + updateToolPolicy, + type ToolPolicyOption, + type ToolPolicyOverrideRow, +} from "@/components/networking"; +import type { Team } from "@/components/key_team_helpers/key_list"; + +interface ToolDetailProps { + toolName: string; + onBack: () => void; + accessToken: string | null; +} + +interface KeyOption { + token: string; + key_alias?: string; +} + +const TOOL_DETAIL_QUERY_KEY = "tool-detail"; + +const LOGS_PAGE_SIZE = 50; + +function getDefaultLogsDateRange(): { start: string; end: string } { + const end = new Date(); + const start = new Date(); + start.setDate(start.getDate() - 90); + const fmt = (d: Date) => + d.toISOString().slice(0, 19).replace("T", " "); + return { start: fmt(start), end: fmt(end) }; +} + +export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { + const queryClient = useQueryClient(); + const [overrideSaving, setOverrideSaving] = useState(false); + const [inputPolicySaving, setInputPolicySaving] = useState(false); + const [outputPolicySaving, setOutputPolicySaving] = useState(false); + const [blockScope, setBlockScope] = useState<"team" | "key">("team"); + const [blockTeamId, setBlockTeamId] = useState(null); + const [blockKey, setBlockKey] = useState(null); + + const logsDateRange = useMemo(() => getDefaultLogsDateRange(), []); + + const { data: detail, isLoading: detailLoading, error: detailError } = useQuery({ + queryKey: [TOOL_DETAIL_QUERY_KEY, toolName], + queryFn: () => fetchToolDetail(accessToken!, toolName), + enabled: !!accessToken && !!toolName, + }); + + const { data: policyOptions } = useQuery({ + queryKey: ["tool-policy-options"], + queryFn: () => fetchToolPolicyOptions(accessToken!), + enabled: !!accessToken, + staleTime: 60_000, + }); + + const { data: teamsData } = useQuery({ + queryKey: ["teams-list-tool-detail"], + queryFn: () => teamListCall(accessToken!, null, null), + enabled: !!accessToken, + }); + + const { data: keysData } = useQuery({ + queryKey: ["keys-list-tool-detail"], + queryFn: () => keyListCall(accessToken!, null, null, null, null, null, 1, 100), + enabled: !!accessToken, + }); + + const { data: logsData, isLoading: logsLoading } = useQuery({ + queryKey: ["tool-usage-logs", toolName, logsDateRange.start, logsDateRange.end], + queryFn: () => + getToolUsageLogs(accessToken!, toolName, { + page: 1, + pageSize: LOGS_PAGE_SIZE, + startDate: logsDateRange.start, + endDate: logsDateRange.end, + }), + enabled: !!accessToken && !!toolName, + }); + + const logs: LogEntry[] = useMemo(() => { + const list = logsData?.logs ?? []; + return list.map((l) => ({ + id: l.id, + timestamp: l.timestamp, + action: "passed" as const, + model: l.model ?? undefined, + input_snippet: l.input_snippet ?? undefined, + })); + }, [logsData?.logs]); + + const teams: Team[] = useMemo(() => { + const arr = Array.isArray(teamsData) ? teamsData : teamsData?.data ?? []; + return arr.map((t: { team_id?: string; id?: string; team_alias?: string }) => ({ + team_id: t.team_id ?? t.id ?? "", + team_alias: t.team_alias ?? t.team_id ?? "", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "", + created_at: "", + keys: [], + members_with_roles: [], + spend: 0, + })); + }, [teamsData]); + + const keys: KeyOption[] = useMemo(() => { + const keysRes = keysData?.keys ?? keysData?.data ?? []; + return keysRes.map((k: { token?: string; api_key?: string; key_hash?: string; key_alias?: string }) => ({ + token: k.token ?? k.api_key ?? k.key_hash ?? "", + key_alias: k.key_alias ?? (k.token ?? k.api_key ?? k.key_hash)?.toString?.()?.substring?.(0, 8), + })); + }, [keysData]); + + const invalidateDetail = useCallback(() => { + queryClient.invalidateQueries({ queryKey: [TOOL_DETAIL_QUERY_KEY, toolName] }); + }, [queryClient, toolName]); + + const handleInputPolicyChange = useCallback( + async (_name: string, newPolicy: string) => { + if (!accessToken) return; + setInputPolicySaving(true); + try { + await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy }); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to update input policy: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setInputPolicySaving(false); + } + }, + [accessToken, toolName, invalidateDetail] + ); + + const handleOutputPolicyChange = useCallback( + async (_name: string, newPolicy: string) => { + if (!accessToken) return; + setOutputPolicySaving(true); + try { + await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy }); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to update output policy: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOutputPolicySaving(false); + } + }, + [accessToken, toolName, invalidateDetail] + ); + + const handleAddOverride = useCallback(async () => { + if (!accessToken || !toolName) return; + const isTeam = blockScope === "team"; + if (isTeam && !blockTeamId) return; + if (!isTeam && !blockKey?.token) return; + setOverrideSaving(true); + try { + await updateToolPolicy(accessToken, toolName, { input_policy: "blocked" }, { + team_id: isTeam ? blockTeamId : undefined, + key_hash: !isTeam ? blockKey!.token : undefined, + key_alias: !isTeam ? blockKey!.key_alias : undefined, + }); + invalidateDetail(); + setBlockTeamId(null); + setBlockKey(null); + } catch (e: unknown) { + alert(`Failed to add override: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOverrideSaving(false); + } + }, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail]); + + const handleRemoveOverride = useCallback( + async (override: ToolPolicyOverrideRow) => { + if (!accessToken || !toolName) return; + setOverrideSaving(true); + try { + await deleteToolPolicyOverride(accessToken, toolName, { + team_id: override.team_id ?? undefined, + key_hash: override.key_hash ?? undefined, + }); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to remove override: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOverrideSaving(false); + } + }, + [accessToken, toolName, invalidateDetail] + ); + + if (detailLoading && !detail) { + return ( +
+ +
+ ); + } + + if (detailError && !detail) { + return ( +
+ +

Failed to load tool details.

+
+ ); + } + + if (!detail) { + return null; + } + + const { tool, overrides } = detail; + + const inputDesc = policyOptions?.input_policies?.find( + (p) => p.value === tool.input_policy + )?.description; + const outputDesc = policyOptions?.output_policies?.find( + (p) => p.value === tool.output_policy + )?.description; + + return ( +
+
+ + +
+
+
+ +

{tool.tool_name}

+ + {tool.origin ?? "—"} + + + {(tool.call_count ?? 0).toLocaleString()} calls + +
+
+ {tool.user_agent && ( +
+
User Agent:
+
{tool.user_agent}
+
+ )} + {tool.created_at && ( +
+
First Discovered:
+
{new Date(tool.created_at).toLocaleString()}
+
+ )} + {tool.last_used_at && ( +
+
Last Used:
+
{new Date(tool.last_used_at).toLocaleString()}
+
+ )} +
+
+
+
+ +
+ {/* Two-panel policy layout */} +
+
+

Input Policy

+

+ {inputDesc ?? "Controls what data this tool is allowed to accept."} +

+ +
+ +
+

Output Policy

+

+ {outputDesc ?? "Controls how this tool's output is trusted by downstream tools."} +

+ +
+
+ + {overrides.length > 0 && ( +
+

Blocked for team or key

+
    + {overrides.map((ov) => ( +
  • + + {ov.team_id ? `Team: ${ov.team_id}` : ""} + {ov.team_id && ov.key_hash ? " · " : ""} + {ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""} + {!ov.team_id && !ov.key_hash ? "—" : ""} + + +
  • + ))} +
+
+ )} + +
+

Block for team or key

+
+
+ Scope +
+ + +
+
+
+ + {blockScope === "team" ? "Team" : "Key"} + + {blockScope === "team" ? ( + setBlockTeamId(id || null)} + /> + ) : ( + onChange(toolName, v)} - onClick={(e) => e.stopPropagation()} - style={{ - minWidth: 110, - fontWeight: 500, - }} - popupMatchSelectWidth={false} - options={POLICY_OPTIONS.map((o) => ({ - value: o.value, - label: ( - - - {o.label} - - ), - }))} - /> - ); -}; - -export const ToolPolicies: React.FC = ({ accessToken }) => { +export const ToolPolicies: React.FC = ({ accessToken, onSelectTool }) => { const [tools, setTools] = useState([]); const [loading, setLoading] = useState(true); const [isFetching, setIsFetching] = useState(false); const [error, setError] = useState(null); - const [saving, setSaving] = useState(null); + const [savingInput, setSavingInput] = useState(null); + const [savingOutput, setSavingOutput] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [sortField, setSortField] = useState("created_at"); @@ -123,16 +96,29 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { return () => clearInterval(id); }, [isLiveTail, load]); - const handlePolicyChange = async (toolName: string, newPolicy: string) => { + const handleInputPolicyChange = async (toolName: string, newPolicy: string) => { if (!accessToken) return; - setSaving(toolName); + setSavingInput(toolName); try { - await updateToolPolicy(accessToken, toolName, newPolicy); - setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, call_policy: newPolicy } : t))); + await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy }); + setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, input_policy: newPolicy } : t))); } catch (e: any) { - alert(`Failed to update policy: ${e.message}`); + alert(`Failed to update input policy: ${e.message}`); } finally { - setSaving(null); + setSavingInput(null); + } + }; + + const handleOutputPolicyChange = async (toolName: string, newPolicy: string) => { + if (!accessToken) return; + setSavingOutput(toolName); + try { + await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy }); + setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, output_policy: newPolicy } : t))); + } catch (e: any) { + alert(`Failed to update output policy: ${e.message}`); + } finally { + setSavingOutput(null); } }; @@ -157,7 +143,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { setCurrentPage(1); }; - // Build unique team/key options from loaded data const teamOptions = Array.from(new Set(tools.map((t) => t.team_id).filter(Boolean))).map((v) => ({ label: v as string, value: v as string, @@ -169,9 +154,14 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { const filterOptions: FilterOption[] = [ { - name: "Policy", - label: "Policy", - options: POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), + name: "Input Policy", + label: "Input Policy", + options: INPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), + }, + { + name: "Output Policy", + label: "Output Policy", + options: OUTPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), }, { name: "Team Name", @@ -185,6 +175,39 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { }, ]; + const { newToday, newYesterday, trendSubtitle, totalTools, blockedCount, activeTeamsCount, needsReviewTools } = + useMemo(() => { + const now = new Date(); + const todayKey = getUTCDateKey(now); + const yesterday = new Date(now); + yesterday.setUTCDate(yesterday.getUTCDate() - 1); + const yesterdayKey = getUTCDateKey(yesterday); + + const newToday = countToolsInUTCDay(tools, todayKey); + const newYesterday = countToolsInUTCDay(tools, yesterdayKey); + const trendSubtitle = getTrendSubtitle(newToday, newYesterday); + + const totalTools = tools.length; + const blockedCount = tools.filter((t) => t.input_policy === "blocked").length; + const activeTeamsCount = new Set(tools.map((t) => t.team_id).filter(Boolean)).size; + + const needsReviewTools = tools.filter( + (t) => + isCreatedInUTCDay(t.created_at, todayKey) && + t.input_policy === "untrusted" + ); + + return { + newToday, + newYesterday, + trendSubtitle, + totalTools, + blockedCount, + activeTeamsCount, + needsReviewTools, + }; + }, [tools]); + const SortHeader = ({ label, field }: { label: string; field: SortField }) => (
{label} @@ -203,10 +226,12 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { (t.team_id ?? "").toLowerCase().includes(q) || (t.key_alias ?? "").toLowerCase().includes(q) || (t.key_hash ?? "").toLowerCase().includes(q) || - t.call_policy.toLowerCase().includes(q); + t.input_policy.toLowerCase().includes(q) || + t.output_policy.toLowerCase().includes(q); if (!matchesSearch) return false; } - if (activeFilters["Policy"] && t.call_policy !== activeFilters["Policy"]) return false; + if (activeFilters["Input Policy"] && t.input_policy !== activeFilters["Input Policy"]) return false; + if (activeFilters["Output Policy"] && t.output_policy !== activeFilters["Output Policy"]) return false; if (activeFilters["Team Name"] && t.team_id !== activeFilters["Team Name"]) return false; if (activeFilters["Key Name"] && t.key_alias !== activeFilters["Key Name"]) return false; return true; @@ -223,11 +248,74 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)); const paginated = sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize); + const scrollToToolRow = (toolId: string) => { + const idx = sorted.findIndex((t) => t.tool_id === toolId); + if (idx >= 0) { + const page = Math.floor(idx / pageSize) + 1; + if (page !== currentPage) setCurrentPage(page); + requestAnimationFrame(() => { + setTimeout(() => { + document.getElementById(`tool-row-${toolId}`)?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 100); + }); + } + }; + return ( -
+

Tool Policies

+ +
+ + + + } + /> + + 0 ? "text-red-600" : undefined} + /> + 0 ? activeTeamsCount : "—"} /> +
+ + {needsReviewTools.length > 0 && ( +
+

Needs Review

+

+ {needsReviewTools.length} new tool{needsReviewTools.length !== 1 ? "s" : ""} discovered that require + policy decisions. +

+
+ {needsReviewTools.map((t) => ( + + + {t.tool_name} + + + + ))} +
+
+ )} +
- {/* Toolbar */}
@@ -311,7 +399,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => {
- {/* Filter row */}
= ({ accessToken }) => {
- {/* Auto-refresh banner */} {isLiveTail && (
Auto-refreshing every 15 seconds @@ -336,7 +422,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => {
{error}
)} - {/* Table */} @@ -347,7 +432,10 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { - + + + + @@ -359,45 +447,61 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { - Origin + User Agent {loading ? ( - + Loading tools… ) : paginated.length === 0 ? ( - + No tools discovered yet. Make a chat completion that returns tool_calls to start auto-discovery. ) : ( paginated.map((tool) => ( - + - - - {tool.tool_name} - - + - - {(tool.call_count ?? 0).toLocaleString()} + + + + +
+ {(tool.call_count ?? 0).toLocaleString()} +
@@ -417,8 +521,8 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { - - {tool.origin ?? "-"} + + {tool.user_agent ?? "-"}
@@ -427,7 +531,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => {
- {/* Bottom pagination (only when > 1 page) */} {totalPages > 1 && (
@@ -453,6 +556,7 @@ export const ToolPolicies: React.FC = ({ accessToken }) => {
)}
+
); }; diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx new file mode 100644 index 00000000000..1317351931e --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx @@ -0,0 +1,92 @@ +"use client"; + +import React from "react"; +import { Select } from "antd"; + +export const INPUT_POLICY_OPTIONS = [ + { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, + { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, + { value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" }, +] as const; + +export const OUTPUT_POLICY_OPTIONS = [ + { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, + { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, +] as const; + +export const POLICY_OPTIONS = INPUT_POLICY_OPTIONS; + +export const policyStyle = (p: string) => + INPUT_POLICY_OPTIONS.find((o) => o.value === p) ?? INPUT_POLICY_OPTIONS[0]; + +export interface PolicySelectProps { + value: string; + toolName: string; + saving: boolean; + onChange: (toolName: string, policy: string) => void; + policyType?: "input" | "output"; + size?: "small" | "middle"; + minWidth?: number; + stopPropagation?: boolean; +} + +export const PolicySelect: React.FC = ({ + value, + toolName, + saving, + onChange, + policyType = "input", + size = "small", + minWidth = 110, + stopPropagation = true, +}) => { + const options = policyType === "output" ? OUTPUT_POLICY_OPTIONS : INPUT_POLICY_OPTIONS; + const style = policyStyle(value); + return ( + = ({ team, teams, data, addKey }) => { className="mt-4" valuePropName="checked" help={ - premiumUser + canEditGuardrails ? "Bypass global guardrails for this key" : "Premium feature - Upgrade to disable global guardrails by key" } > - + ({ useUISettings: vi.fn().mockReturnValue({ data: { values: {} }, isLoading: false }), })); +// Mock useResetKeySpend hook (requires QueryClientProvider which is not available in this test) +vi.mock("@/app/(dashboard)/hooks/keys/useResetKeySpend", () => ({ + useResetKeySpend: vi.fn().mockReturnValue({ + mutate: vi.fn(), + isPending: false, + }), +})); + // KeyEditView mock: triggers onSubmit with our injected form values vi.mock("./key_edit_view", async () => { const React = await import("react"); @@ -334,9 +342,29 @@ beforeEach(() => { }); // ---- Tests ---- -describe("KeyInfoView handleKeyUpdate premium guard", () => { - it("removes guardrails & prompts for non-premium users and prevents metadata.guardrails", async () => { - renderView(false); // premiumUser = false +describe("KeyInfoView handleKeyUpdate guardrails guard", () => { + it("should remove guardrails & prompts for non-premium key owner without write access role", async () => { + const keyDataWithOwner = { ...baseKeyData, user_id: "user_1" }; + mockUseAuthorized.mockReturnValue({ + accessToken: "access_abc", + userId: "user_1", + userRole: "viewer", + premiumUser: false, + token: "token_123", + userEmail: "test@example.com", + disabledPersonalKeyCreation: false, + showSSOBanner: false, + }); + + render( + {}} + keyData={keyDataWithOwner as any} + onKeyDataUpdate={() => {}} + teams={[]} + />, + ); fireEvent.click(screen.getByText("Settings")); fireEvent.click(screen.getByText("Edit Settings")); @@ -344,7 +372,7 @@ describe("KeyInfoView handleKeyUpdate premium guard", () => { token: "tok_123", guardrails: ["gr-1", "gr-2"], prompts: ["fast", "safe"], - metadata: {}, // object form (not JSON string) + metadata: {}, }; fireEvent.click(screen.getByText("Mock Submit")); @@ -360,7 +388,31 @@ describe("KeyInfoView handleKeyUpdate premium guard", () => { expect(sentPayload.key).toBe("tok_123"); }); - it("preserves guardrails & prompts for premium users and includes metadata.guardrails", async () => { + it("should preserve guardrails & prompts for non-premium users with write access role (e.g. Admin)", async () => { + renderView(false); // premiumUser = false, userRole = "Admin" + + fireEvent.click(screen.getByText("Settings")); + fireEvent.click(screen.getByText("Edit Settings")); + (globalThis as any).__TEST_FORM_VALUES = { + token: "tok_123", + guardrails: ["gr-1"], + prompts: ["fast"], + metadata: {}, + }; + + fireEvent.click(screen.getByText("Mock Submit")); + + await waitFor(() => expect(keyUpdateCallMock).toHaveBeenCalled()); + + const [, sentPayload] = keyUpdateCallMock.mock.calls[0]; + + expect(sentPayload.guardrails).toEqual(["gr-1"]); + expect(sentPayload.prompts).toEqual(["fast"]); + expect(sentPayload.metadata?.guardrails).toEqual(["gr-1"]); + expect(sentPayload.key).toBe("tok_123"); + }); + + it("should preserve guardrails & prompts for premium users and includes metadata.guardrails", async () => { renderView(true); // premiumUser = true fireEvent.click(screen.getByText("Settings")); diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx index 49a98b699e8..f090df7c361 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx @@ -374,7 +374,7 @@ describe("KeyEditView", () => { }); }); - it("should disable guardrails selector when user is not premium", async () => { + it("should disable guardrails selector when user is not premium and has no write access role", async () => { renderWithProviders( ([]); const [tagsList, setTagsList] = useState>({}); @@ -443,7 +445,7 @@ export function KeyEditView({ form.setFieldValue("guardrails", v); }} accessToken={accessToken} - disabled={!premiumUser} + disabled={!canEditGuardrails} /> )} @@ -460,7 +462,7 @@ export function KeyEditView({ name="disable_global_guardrails" valuePropName="checked" > - + Date: Wed, 4 Mar 2026 18:13:09 -0500 Subject: [PATCH 147/171] add missing bedrock models (#22810) --- ...odel_prices_and_context_window_backup.json | 27 +++++++++++++++++++ model_prices_and_context_window.json | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index c09cb647b72..1e0a7899422 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -23209,6 +23209,19 @@ "max_input_tokens": 200000, "max_output_tokens": 8192 }, + "mistral.devstral-2-123b": { + "input_cost_per_token": 4e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 256000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, "mistral.magistral-small-2509": { "input_cost_per_token": 5e-07, "litellm_provider": "bedrock_converse", @@ -34637,6 +34650,20 @@ "supports_tool_choice": true, "source": "https://aws.amazon.com/bedrock/pricing/" }, + "zai.glm-4.7-flash": { + "input_cost_per_token": 7e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, "zai/glm-5": { "cache_creation_input_token_cost": 0, "cache_read_input_token_cost": 2e-07, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index c09cb647b72..1e0a7899422 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -23209,6 +23209,19 @@ "max_input_tokens": 200000, "max_output_tokens": 8192 }, + "mistral.devstral-2-123b": { + "input_cost_per_token": 4e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 256000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, "mistral.magistral-small-2509": { "input_cost_per_token": 5e-07, "litellm_provider": "bedrock_converse", @@ -34637,6 +34650,20 @@ "supports_tool_choice": true, "source": "https://aws.amazon.com/bedrock/pricing/" }, + "zai.glm-4.7-flash": { + "input_cost_per_token": 7e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, "zai/glm-5": { "cache_creation_input_token_cost": 0, "cache_read_input_token_cost": 2e-07, From 063a1a437a8afd9c392c386e414accf42500a628 Mon Sep 17 00:00:00 2001 From: Harshit Jain <48647625+Harshit28j@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:43:37 +0530 Subject: [PATCH 148/171] Update litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py index ab3ad82e3ce..a2a38cad149 100644 --- a/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py +++ b/litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py @@ -1,4 +1,3 @@ -from typing import List from fastapi import APIRouter, Depends, HTTPException, Query From b7f43d411a3ed9a7dc0f2550dd072f666075a5df Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 15:19:07 -0800 Subject: [PATCH 149/171] feat(ui): add time to first token (TTFT) to logs (#22819) * feat(ui): add TTFT (s) column to request logs table * feat(ui): add Time to First Token metric to log detail drawer * docs: add TTFT screenshot --- ttft-logs-screenshot.png | Bin 0 -> 222088 bytes .../LogDetailsDrawer/LogDetailContent.tsx | 9 ++++++++ .../src/components/view_logs/columns.tsx | 20 ++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 ttft-logs-screenshot.png diff --git a/ttft-logs-screenshot.png b/ttft-logs-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f07ad3b030862ad957dbf72f2a942385f3969295 GIT binary patch literal 222088 zcmbTeRZtvV)HWL2-4fj0f&_QB;O-FI-66o>9)ddrcXxM}V8PwpoxhX!`!4=Er|MK4 zE~px&r@LqO+Iu~=2~+qgfdr2W|KY<2Bq>Qzr4Jtd^mP-A56^pXw}Et+Z5DN2i3ER*_mowQxsc#N@6a(e+DW zRkKYRRYu7l zU^h3RaI2~}ngg4e9+A@1P>Nn%)jj$6ZxF``(Lt&o@(Zz!Op7usK-{TkB2#`3RJ&m4 zWVeChd0Z}gFLCkv3VvdI^CL^rq;a`@`7G?RBK2Ax{Irx547FCGtzS=vbsv=op`8>dlsLp5-4Tp4Q8O|!V0B7UDA~y`{Ftd}3n2Bbuc@j7=+1nrLOvwpAcXk<25t-VyRUpVTEp zWhCTBsc|5X-872A)6-|ehr zt@c^^x0jpf32C}O8SW%BMNB&!rZj8*dEB%KC}&_GYVvy6#K7S==UQThHKHPoL>5Zb%T2YqnO_xKk|By2J@bZ*2_b@ijG{!{j>f$Pl zpzUi|9Uo}x`uob!#7J7)Jd~)Xp+U_p=2O?-MKGX=AXda3p(@KL;-_h`P}o}G8yXvZ zgPQg*lo7ZCG4@G!kvn`ygNCzN9tG&Mfv2gVLTDI zG7?4I7QYLsgOvhFw}bCn?76;B@F8saNK8!l8oU(RO$>DMulAte3&ea$N4631eKw1b zJ`k(+K8?0{j%NM$Fl{-=VQ=HUC6)djzlL`45fhW6-pr7wl$Xde{Zab3D@H39b?l_Y zr*Tcj#g1gD5dM_MjH?WxtpN2CH$nJbuGAlQmJ5XL>hrBog_Jy(F;7&vr!N&$4>mwN zyWup+2g5#e6f#nv=vq}vFx&cj1j!31FC&cK+(C^ymlr7a1Y99qdA|p-F|j9owo98U znOYKKxqY@2GC#a+8Z$BNynoGzG5v486aNT8z+nk+dr-0ZYMPK(T5sy%)v^mmfh}u7 zHmxC3m#7pKX(3%-BU_&U-GKeOMRRniSz|OmSe0t_5gq|SMp_!Rp`pRm+4<)hcLg+@ zl9CcK0XJmS_SP1Q{e~bN9i8E?m0Rm!m-!UUcExHLO-)T9Auw=kZ0w1N36>M9Sn<5! zk%@szlIoMwQyUu_3UL?-+c>jKuy4aNGfDzTTQV{-4%i?E22KR@Dy=Hp-ua=?>XtbF z7F8G*v}~3n!2S;Jp#b5YEK-uUxm1}@ zV4Rtmn%0Ozg!fELd}S;Y_}qi}#8PvIOyazO<{x}Qqor$YQ}$nnjtcfHE`Pfk6qu8K zABvMdZ(TMQyCLc2$sGWGr9_U%0k=>PuQQ)s(Vg8(;_kj|K zS=Hz?}{`Ifhu_C0zF+U(N4}!cFgIm@JO0Qo12@NRZM!yzdUi9&2cxpwUtNRWGc4sYetEz)I6e{oxBxYvvhvk$W9NjP`L_aeQiB5{DrHNi=iB( zxv{sfMZCNu8~HD|e>Yi#eXAsYdwFFb2Sbg_si2t;?+^#EO`a%A#z03iL|0QC+q5`* zLZ9#YMXfH=1{gIjx|VYMXo$(NM%6~wI?Dlc1qKxCpovNsj06|l<~!Y zHWR2nBQkn=#4KJ)8X6idu1`uxJrG7L)YL=6!&0>6NuL7k2WrhGgF`}ETUxfP#Kob% z>Ndu&b8>MvAi_nrVH(rKTR8{fxv(rx3@oipM6=V_2ig+>bNQ1{@-N%>fJ}_!FE(2C z_Vy6+Z2wTQXnbHK0%fvaDz8{S0YSd(_Lww*$|H-6h5h&PsD%22pdeXB95s#kJsc?+ zX~UQVZLJ|TEteeI0-2NpRAI}+5@+`Iqa~L=fBtygp9Rr_S`c6nK_7sLfm_KU{C2uj zKRP}AnVEU2rw80~yYu}GSVBZ7-N8S)Rs9M^!kf<(i}0KQiHV5~#t;qCpsuxhxp{bS zl`0ckwTgrEqoUM{=b9jG(dRt|WrBqLA&yXj$wPVIB!&35sVxB55A+&Yg_r81jt>lMpD773Q4KX^H)qA~&l^Ap(hpL95$v!ruo%_F4Q zL*#LGa1u)}#pcyeL0VBkMZc_9-vR#l;YD;Fg#&BPV0h{ortF`LZHicqS3z{Y>v4`PHBNt~whM58 zV~Gd4eByIBN^j4wNt@3(`J$+z(q?gN3%RnY6M}fui%U0r$Ys@tSJ82>zsv35?)Hyl zG=@E)gHZj`8cxqXa)w zbr;N==VAQ(^XF08q*+?COlhPZH)?iX`oPT-!=>CSKq$C7B2X397&UivZS5^5gfKun zz}WR@mZnJP+dzN6$;>aQXk=u`DwAV1NnCS0Yb01$1DEi?K!|T<&dzLg2hC3VS65fW z;BlW86Qrc1Tr9Ap$@yAczJ8U8ik1UW{ybDmD+*o`t$2GnX8Ks(re5ByR%FsQnZgRw ze|z5f-m{a=>0o4Qdv>)u+H7cEB_k_4Hj+TMy0X$I=RFq-L+Jk9tBTDGaoXJCquj&x zmI;G4ZIMn+6ROAKShHlll8F{7A(yA+Ea(#~B+MfU!OHo# z$7B=d@lqi6``v=fH9uy2P@|QL{Q|C9p&(Mhul>6NcNrR&^TinrQuHYQ3?4V%*V_f4 z*`c6xu9?1GPYUpGbAQd?PGI-h+TOn0W;Py(9FC`2phOZvb%Va0$w&1ip3Qzp@cN$c z+Ik9sOi`BWAE;%cr{t<8T_K%MO+7X|Av-iIIW#T}EafAUlM|zpU!_LdK4NKWr596= zGct|S&`mJX4XggdVNpvm_D`;=A(}sMq^V*LkjEyWy#DsCu=v*ku6QJP<~m)nDtI?VwXTs%B(!1_NSh9*1xnJl;t z4ndJ-bQH^k@H@$q*i3qe7U2P&J8=?Cw`PKdhKAavIWi}neKvq^siD&y`%ldbBi(mmhzW?CWynZSr0{5($J{pI>%vv-f& z_b@*x0P2hNV%HJOEYa62exHktZmj||McRZmA`Br^IW%06A!l|-xOi#X;-hY6`k`@J zTFKGsp9RaBqgX+S*b16uDptjYEK%ZVY3inS`7>Ha1-}$aFk^|7s7bZUZU4Z-VW3c! z;`En8{6K^zz@RNIoJ{YoZ0J@HE9&0bGE!R*d=nBCa=Ub)%vR@dJ3|Z`Is&Wf|4Jyp z{dC06_L7vSW-0LW@X&=!z-;hA^sqkyXVY+LaWNaMktGUV4gqYqwNqSNJo*zWbQ}o@ z34>N$(S^@^p*#^jd`Hp8z;CXuu2u^bV!B{w)ODm5!nq{XjrH`jiW*#VFEIGPNS8ec z^1s$NL(z99mpZJrx;|a{W!1sLiFykgjWi9{PNcVTy4?jZcUs!n-T$r}^?N-%mr<`6=%@0%Uj4DL@x9aUy`iT!Gp6mPcXITCD_dVa zUpOWC=hg2<1a4RJ=O-S7k&%%YLIHxT*q4hy<#pdzE|1%U@~jy3<0@MVMX zqFs*X`}_JfTth!+`Mo^?;PJA;4A7>~ZgbT)o?Q;zN2>(ggDKytm*ID^b$ZHvHNKok z=c6wO+C}{;%O>U^SK_$ulPqX1PP`4tf zcW9Ig435|R-sKqbL`96ujEm>Y5o9bErSN!+-0(rFSqY^a9WP5EwS-Ogvzy~6e4P1J zMWKUC`qg*c&*zVi%TNgX@BB}e_(s`VXIWn^Z{iq6lipBj>^F0X&^*p}svmIID&{a^ z#sv*F2kq2c7u(tbX6tUS{JdK&Mk_jeTHMaDFZb75_|v6^i#r*0KfB$HRSNi>FRZKM zbGe4k{md5hGoSrP(n!q6Z!uA8cj4E1H#-GOdW6lk3{u&tC%bxI6!%!1{@t0W_?+#GabiGM-FK}?|8nLF-+*K z#QcQEVT(WGchL=FVPO%&|3ux>Oe|-D ziG@|J(>&AL3*ok(VwQP0od?6HMt{8&=mo|;1e!~}-NuvmLkehOEn*5mO;y0hPfpDo zrb;f39jFqAI(m*)yW$O!USFwobFt-ckF6md6fTG@8W}}^zhbQo`ozTEqZHUu=a_=t zDUF+;l{Y_5xP7wQL(b7IegKQtOv4A8go0b3)oJBEcUZ<{h)?Dl;N;|lcDTRKVZF%Q zv8W4hMql5K`1tq={o?xiCNkWO>OB93&kx~&csDG&87fFu->s4N&5H9X_6l0)0*!A8)ghZ=;!FCvJ z;JXYPe)?>qRS!ZcA_}zrW~!?W#ff$`+&eK59cd2Pr5ebLfQSgHZqPJ%eYz#!b{+!W zh>i}v^7q?4K5msU07|vHyW6hw^$x%$JT6DGmHGnawHQ=i%XC|@k^JD`;1;2JBG04O zZQ3-mkAK9+8?NN&y6g$V;-b@%Lo*pk0s- zjg5J4g;AldR~t)Ej-l!qR$Pc9!@^3+$@L$jGbAS_CN%U5YUI{mZpR4B-#$%4Se`!a zXGu^xC{7gtZLsILwC6MOcYPXqc1bn!1z;XQQG9PCzxogrf+$k3r5eiCg#CG$lYcF8 zetw?%0-!BFb9_@rngB`_7}%RF6zJY+4}gCK+aX?CNWBqG9>!1k;nxBTu+iCA`p1R9i(}SgxY>PYywR3>`yHf8Emd^(I8`9?uznu2=qSJJd`M&j^d~I;kQzd%v<2)HlXfj|4%hK>B*zp0b~eNECef^>a-lkgyWI4J`s5 zAq0_NsYR8JjZIi}s}V1xN1kB~)x1A4GO{m*$(Q@KaD<+k`e8rI4{>7mDs=2nqI zD{2qW8y_~maI)hfBazenq|(ECn3l)}xX?g&^`^yo2j3cYlQ^VoCAq&f>*ntGEvqH8 z`)1A|8WV}>7k8m8aeJX5`N91_icn|~#`u=jMXV&+EBFyiS|=g_nPx=r8q+mmsy$2k zpJ@)>gLex&VUM0(-76v+Cyx=vRO9gQH^;L%x>{Pn<>hiNC{`q$Z^1QnRb8D|c;l*5 zDZEn+a>;NZ7t&+(wfng@#@1!!J;EY4#xpal?Yxz4yiRUT9%sc8AE(W`m>BfpP}SX! zh0tG~&pYiW6B%_|>^Frov$Y#-&8K@~9(m5&0v~{GD3`^bQQECqszyTSf4}ZWbhVx7 zb+*~s#hAwH!2*I`Va3M|h~){lS#Ahg>}~x(xY6PBjOZ9Z~on{*iF%L;NHoeAoHuH7NbzFCx>oZ-LzQwQ__F^Ez~67?1)isTzN6!r9ymL$Q>40yuk z7zH;;!5q6e%`YV{JM|Qm9(AEEMi$MKyqK84w4{Vu3D*@YD>DuRJRZZCOmex}B3lFMQEF*S*;&EQ;m|2Ui@$$kbneL}KZzCkRVHoQ zK-g5cYj05aEaN!iehda>p^oRG4gvn+!=%$@jc>PN_fpH`hc95dc&|Sx z;eMxly@3&)_Kl_ZNDWPU(~#~sf)Foh2;ve>%4T^by$<6{Uut-K4SsUWW6^!RS3*8~ z^Psd^OswB9r@PFtqM!1m-7zC0#IR9LCf*h? ze0RwmCy@~my-Z!L=N+S>NCj|M9h!?8Auh)TRe2K{nBRUx1;*>UbA;UtbVDYg z-DP(M*8fBp(XL1L#lNJ}G<-$DB7V8p#DL457p+AQ2ylndX>~=4TO>e->TED)4b<_k z54uP4rMsNyie3b*&-=bUU0%*pPW0P0X}{=TQGO@xW50S4ihwH+ND zua@eqQmSA{>ONOiqV^F?f-cFjZw8_WznBYFBE%)-{M2fIEn&`YER+ye!?tc$E}XPm zsiDg?=5bI?njmO6Uy2do;$C!`GxOHK@K|))I4EYtF}Sx`Z$3|Bva7(FuJzjIn(XkG zEAQ1x<`?!Bc2skn@FMd0Ly_x~rMD{kOIB9;@g{nc8THCg6cqM-4b# z(D&ENer3bo#JFbj0S9{U7u6D(%wo$lVR7^-1uX=(mRu!>RE z7x-Iq^rjGY+SXgT_5gpC|IDXCgmR(Iz#-$20g}HApV#xPZbHbDQ0I$h7D8pu)`Q$E zc9N4qb-ai_G_gw2(NQU>*dgPsJo{fQ!0oqz;f?kJUR(1=zW0)ZChbEViHrpIQry0C z7OhVJUQx-B+>Yqn-gn;{OOgD(0QYV&K;;W_SsOv5+!kd3y;Z18`{8t<-b%eM2@^+K zOLH^iV_H*5ADRKl(O4-wR1PygYTb`Jd9iUNFkVwLT8xF{hZAtDc9=hIVD z3Jw^qqT06r2FdQtNKZE>5YByMex8Ogn3Bu9`-lO(YNL4%)2g>RdoG&&?EP(iHiXb` zny!lV2NK>23b?GS{rjxcx$lFR{qb53L!dnVipHj4e1bH#C0Z0$Ie!5sN9Hm;(G!X8qOiV#f>++j=Qm_hRpEF#;wjX z+}B|d7`3LI?C#k-5NsC7$wqfzER4JArJ}d{dKjGR*~)Oaz*$4R?P{u(zzb*R{hF8T z#XO^ahxL-$mJH9!-Ll;xVBa8U^=sKZ@P!}<^DD$M$%Imh@q^vEt^1h8E^?}G73AW@ zGxGr@0PS_D*}Vzr`~G@wMBYcM|A^W`$IXpfL7+DX=(MpBMQ{&KPg}1OR3ja_8>fDN zBsVLv!@=#IN1}Rz_{au>UnHc0(v%)7RI(l>MUM^f5G%|d#xc{Sh0kt@*=`S)%U^^e z^Z>XJWy2(fi7fLY!<4j@IywH&h0o`Skb5`#E!83Qq8LTBF!=+W+TL~F!yW}5V=@+9 zhg7F-!&D&`%Uv*E;Fl3T;%`3bB_>i1R9^qYVw^8T*~z~|P$Jst{_`h=DSYS%23Bzm zDu3W3TU=^KF1*|45B*Ap`pRf*i0h1_KZ%dvtmsO`vf7`AVXj$NYTMiS7@&wY2H$;1 zDK(m3UE|%~00c=!Q*xH??voeb2palQmA;3E&;Bo{_#ts+G&+3!=08&G(>3D#tMy)sp#AzrR*#o23-H8Xl zM@dOZ6PfgV4`(RM8@jr>MkacaR38AKvbDAK_Ij_s4QbBI=lx)>q4CjAT{!;cU>ZUz zNJe>^3~xUiPA*(F*%tHYCX6a`qtej_8u4S-{`(HSX3e+hT(K{tk^8J zf)PLh0$#*x=_{akgwJq^J{?x`5V;OP61>f+>r+rsUBBf6`UeAB@CCQGr=povaZD6j zj!G{Z`%6Z@;x{GiBLhP-no8^8Fw57_oQ((%I0{ zTXe3;B!HAZ`vMLzhRZ?sl|#2NL`>R_b;%i*WC|3?jMAo@{aX*<@#Wf$!rMZUV`&_A z09r8uf3ODBkbX+)aCJQosGdN$nX`TzeI14m0!QVIYb>WZLYhKOgy_kvX2>^@?yheh zr%UWF$s%S~?rmXzB}q8vy|GV2ys%;iM6V0_LCwN{hyHCZ%`W?9g9sNvn402sKy&R| zSb-j6NT$c$r=dbX_<^BC!?^`m7aj^b8%#As;Tth5M4uex%)!1BAY)TU0X!)PF2i9U zhKPw#H*{21N{Y*V<9m5I!!P>tX>P!4Y^quJE!F;B2VjJza;%CJ@7-(8`0e!iI?KO?+_Mw1>0nZN0*j?~F9uC6!mQ3t{a@Bdj^7iHN`jZ14-j~yh z3;Xjwd;w^U34J#&ADF1-iHlT9dU}Lko3jm*x|yB;78rs;l$n%dwxS8qy0f<@9GucD z%g6@Trxe5|p;n;-rx?yE{K#yGCLGEw+5M;1Y&+)W^?v;wAd+id=j|!1W@;&PZ*IEV z_u#IR!@~|PE;b+n`HelA1nJk!Bvd=VYfJrNgZo8D3~%RCn1fYO_fKM6I2G-7I3jJ* zq#9xgD>ynCM#a(+c87vQO&QIq_Oa0k=^otm%3A{e@(%zbY-X_?V-{y)&CZrBLsT(b z@HyGqRE$!MmXk$v-Rgs*l!fT+6o${_akHH(QGI`2e{ZUXil15pl`*TMiq%_C^qpbd zRahfxsPXgX_qhK?5JiKW?usR56K!1KWy^`LkIyG95L7IQGY3f-$(~cpv6wZtrP1fK5C&o?%*JLhd*ryK3o0T%`aN;gXmhj9xqv%c7_d?X-1QvF?`TACvg za+Mi{lu|k%AA!vvi?~V-o2w|KD~^Qr6mryJv-!@Fz+EevqFX?##+tx9q_8*^f6yUG zQcN+XIIv=~d-f&KF~xR~Sbs184bs63_#m9{FOt^CzZtb#crH+I=qS4I^`9XVuxJ(m zFS#7yINe-YB?~NOL?}7x1WOz8v2NibODiyS1evwoB8<=^*{aT^Vax={4XK*HzZd_ z3{aqdkpw(b34QSDtZvrq9jUUAc%xo=~X?)r8)Y+jd8KTL2NihY%&J z6_?|`s~sdr0jEnIrFwrJ{WF}YS`RDHi;-zQR z@cW*ARyPZY->*+<;o`#9Op{t>DwfMRn@Y=2!b#ZC`e6`yArx^;kzyFdL$w*uv;QN7^1(8xy zSXg<8EH5sf2YoKj(i4SzHMt&3IZpk~roFJ+F5R82XV%Ef;`T0vhssu7Q1m^TeA0k; z3vZCH>Yr&weL0;5%T}%OVtTNh{PHNQztKn7+|+*|L*k<#!`GGuI3(w)TBMt6UiTMp zqc^Qqr|B)f&FxbGS7d!4E_M@YzP-7}+vdmdJ0x6AGALN3c=lh=FqAu}<-d9mmfRSB z%i4uE`Cd7=qKWYckk7^vD+|r$prA8$@_6-WZmuGE(fiQ(DzH!g(ta*L1^xSW=JgAO z@zd}s2|A>~Pq*e-$v=u!N_5ll0iggsLI(8@7cU}Zj#56i6Uo4he_1*Gs!qBF`_;hT z?-0NY;|Tk=u$&$bCvUyy6YYF;$r7afbS?r?c%D3)Q4Z!Vqo5*kpf|C<;?7M@F1Nci zx-Q>mxF1DTmeVuXv#Igux!*76{!oJ4YilTy8|}i$O78i0<|6pvo*;z3x2lL$pi8w~ zZWL**eJEkchWjds)&U1ruf^y0=Iv^hqf~qiAD)hXO24y^M&^(YJQw@-OpoVdGoof5 z`o1hQIsgBby1y1Do6isl5C$d2g>BZ^JM)YPkCh}Rw4L~GD3?KP*;UDCz?i5iHX`Q; z{i+rjV(8oJJ%Jf`_H6!}BFaSzTu?SN1TM)I2hHFEDDi;)HP!}^n3R;3(lkK>fHNAJ z&zHV-!V5}xJJHCle@|Q#9(>L+l8QmFRA&I3Ta*lXiH4nhZeT#1kz;NUwX&+Nj?K%< z%k91-?J>K_HYpVFK4@uadv=LY67nr*7nEd*MydZjG2qh$-`N6iot=QZE$gVPw7mS} z=%|yc?XN&^czAdNl&8nXl=!ANeVNn$nT?{`yMO5f;4Aa>_V#LS?*aaW|7KS&aOb}Z zXu!e2zp;EG!$k@T3S#~}rV`lI-w(xIG96C6 z7^tVGXG&?iv!LV!KYzghoubaGZ$nryLQh~2(8PPw5=5${bKZ0d%LV{1 z=cS~e=y$e_`>t=CK!cRbYzQ7L;B{yHc@O9{;*r$1wY9Z>jYDq&AcF%~M zePI~9b~ubW8#DqB8xY>7tF0V8e@*fA7C@Hw<^yofJGaHmFWTBVr~R*)oFpVd0=@uh z3iSrez8f;d@4T0Qji7lAk!06WSOk`sm`Ixt&aM{Au`@N+7xkiLiSjpS41Db~jgIHD zR=|09TWNMGd-J&aW47+T9Yx6JS*7(s5%T@iABOM&Xr~^LhhSp~VE4vnE6uQiY9`;l z5my=Y!!vpzI!AYFTDo#O9jvwUV9aKG>~--4EXHJJZ`{lypmX$<$JPkE-Y5rolrqP| zpb+-$$B{`S|50%jJR`Vt#2B8O+y`?Cjz>px~3w^MYZ?o~@vc6SS`KjkJS+Xi#ws9KU4)28(nOGkY}Md7<% zG>+-sGG^b6lORmtcA-V_1B}sJ@J;WB%@F+q!UX&|2PO3yP}P(xR*cBaAaPEBcKBBt4++ zt+rk^`@Q?JUK@i0POo=CLBWwkcw_JIaItAfIhc#@g(aNn@3G|1+knouzXBS{QbxQ)+1EEni_WHL<-@?vF2t6ZT-`+zM7cW zae92pee}sfo`}hF#j2;lHu5T7i+tmSrn0yU`N>?2udwuGWX!rig%_w<)kZk0riLXn z5daN+0|Uanh=jZ!8P)9VtCBy^f@@hl-<<+>IWT1`_9rq@n3GSBD?7yrFugYEoF_z3 z_}DzIz9Aq)05G`yYJ_gjhv=Px4+P!h^-h^WDw@ z25}n|&IkT&goboR97Lw?TLy$3Rq{+c|Z>4-R*1sH=Mc z9)vwMtm%pn9v+^WRa3U-*Y9%#fZE?*d)fK^TB+aZTgHDlSBjH)ss|38)>Kn6?&9I; z=_nK-z&5Y2VfHQc1Go|S`}15(vE6_Ds6;4Xe6Ee_eQ21P9Kt0`;_8==SEmzy}oPZ=GnyUcl)NCX*<#R(k?aR4fRaO z>G`H5e1vm8_Gy;N=y0Tbz;q`lK^G&Vnc!IY?1xuDxaNuR(;(Ck`c-z25*H@eq zH}M?)LGeON1?et*0RN_X0@-9XNjN*YN$HRR>J?@qRnXE3|3;@))-|m#9l{udE6R(E z;L+6x9?DoG)F$bXHmS5oI9H4G&A7}c#c{owdbY*eHdD^EPi=oXO$Wmr82X=u*C z29ku{%?@n7%6^mT34~M${pnv{$(^uRr`(wuM)>R?MR+!$WKpii_8grl^L4b%YwSA} zQToLO9~n8il##Tjh2~?`+v*I9(@)yDuv&H6<$G_HlTiQF1X>qIEmucNH7N_tpl3n> zuV(cm*E`n+^$N<*-BD%s>&iG4O|9HPK@a*JsiBZS6#fp#Il5Lg_IKtL=C!z5o7k9~ zVM2lUi)@^Bzqp=gxa$CvFu3h=RgZ$uU;(fQ#K8H0E6PcH7y+Y^&*Tl`W}`h=dD3q8 zWakIEwwKEHbL5x^zug1|Hf;k5NMbF8Q-0r<2Y?RUs6t}wQEq}+Z-rAZGJX{hU>@I~ z2)EE!HMFrAhmulK`r*rqxe$tX(e3i&{kNa`z4Ij*=5EA|?xI@rB9?>m#HE1Qf~i~C zzu3D)u6~>Vl=8t4vI)TR|3dR_%;BKCh*azf5PMeK6fE?2{-7JD1h!z51>{8!;aD3W8+YN}lrz}6IVq|1oL~G4H z1j|J@BNZh}<#qRPoP&=W12qR!VdeK_(NP-)a(&$(-P!=WEN?*1*{H11>ZQQ=DQ=Ql z0xgKNRf&E_Oo%m4Y(H!^HU0x(wWXd94_y&s4V!)FKRl<8uuUPu3&ib|PSCY!i0Flj1}p%E68($AJ=15!!bK9$O*ylKgv4tl5ZqdcN#*z9#z`S0Bz!V{2Zw-&%Un2d zz!ne?P&sI3-jL1YZ3dbM5Wjib3?V{A8I4YXY{k1F!F;^=EgwbpxHM1cO%nJ>sK^Ou zo=o6CPYVK{zE>BsKx#KEs6b_cVYmKh1S~u5LBZ%*InGnrLV)a>`q5&mfZ0$l<*zj3 zhEGYJr6`VR=lxLRu4^QiqrlAiy#%x*Y78cty7=d#*(N`gV?PlqfV3$!{9e#l90WN_ zo2<4%MT&$gHQ3y_WST9%GwA9S@=L-vs z=s-^iSwcR+{F!mb%lj7HRkEczIHVdL)C&wH{b=#Y26dnsLiw zv$Kg^^OnYKops+X2Z^H8-KLUk5=icZ%f_B(vMQnGbSm<065tllFZv9`dU#4^`U9F( z{(PevP{XQ-KBf*-ttGjR97J^_7Iy#excT~Vy2$@T$i#u^Cmi<54+@!w5)|fTO$vi} zcP>=k2kYq`75xJsgs;=?S#L6$_)>b9)`NxCq1WM^l4k1tfdr#XD3sA$@=aA|8tu~5 zmF**+C!1@C0*_(fHuz+0kV4N8$$8Ps%k^aM_%`X(^78VHA;~E*Y^84N z01l5(Km-oc6{&pG(!TojV_@ zkel)}F0oXql~xgk=3l=+lgoz4Q;It7UD<$}CNR~69O!U3lfQQ|YE}hVRE&c6B~q2h z!I_9?H(%hC{QLn%sz9w&%rNYv9Z1rY{TTa*D8vhdU{LH`knG|ks23gD z0!fcj6=;b3UM>M1rMR&;@+x1gZt3XAg3`4fs6W!nOlR=(a#$eB17J%H)_=g~Zh*{E zg+?_vj`pscf5Xnc*noQ^=WoPw3IrD^GG7q)dS;6&W!#3?uIsy4qcm*}$>4PiyFf$c7{8?;9#-}KUz z>umRj{LLbGh8UipMLG-3byFJW0{+|PIrm{LNZjc)>m$uTsPe?(l14q^UHO&yyjr3y z<5x;_op7n4p_!;<2N%bx*io60kpPG`w2xDz#ugrr6A*>m^{r`Jl07y^k=bm#2^c8*RCcFff`J5b$=@b6D%?f_kArsnEGjRfm#Sy7VL$bcjLpeJ9$ zVdQtI0}b=X=lheP&{R@SGUlWvA?Jq+L;i#axIQd2&-*h1T-@+Vps%ui9gqm<&Y#ww z0vC=Ldd1qKF$-s-qJ0wnDJAk{-?|8V0;2Uy-nqJGVyNJ62%T6YIE5h zj38x{&T`*^`#gryGm8DWiH(1On&@65Ur@Kxm+xj0$lyg2@`Znp{t$%*djV%4r|_pP zSBG?d*q{dpOm8bxwp~dxEYK8V$I41T9*H9P0-+7}$Q&7-2;a&q~+bM(SlwkjV z(qS&{OOti`N8jhi6j_ZN!@JUjq+ClpY;!B+B&5=yehOe$CReAze&Wp`Hh+~ml?pXc z=9j1aUAzSwKG@qomRWY5KZ#z~I-;@p!Vvd8=hfP?veM0U|9gkiT3fCUA z@}^}!2@FklH}v=fzP7{MAM(Ci>8_S4yN@!SRxYf$;&c8fFv*S&Bp|bdPokUppbuvc zbC_n=@wq)Q5HPaNeHuM)ErW!?kSus%C^7bM{{UjMCm8-?6vx{4Fc3VSehip620p91 z^CC|46p%_y@z`ja;@wi`@1_i4s+|Bqku*VztgZ_BszB@81VAj0kF`f$cj1(NFYeC} z*YKFqt#SY;AUo^?5QG0LngTncn6Y9AcnSm(@|#Ta;1nX8lz4T4 zg&wr5siqdpd^V~q&zGMyX0S+$S)=ig!R=C0RVCQqfumq+p2k_W*o&>NI1wAYqP6ow z{$?thyq{`Cu4~8Tpx~mr{X0PjX;64za-VVJ2ly2OGz1@e47sre@KZ&q091idBHsrR zaz0ERKM-y(cil8OdRS5SMOBzj)o~iUSzNI_L%8R^-QJLfjO#wu8l|uow;z`}Bf@lj zbuPe5p;me7yF2+pCYo9}ovpoA5l`leUH0z`r@|+qIi78TgCAaB{>VH%Ke@N~+`j$x zfC`eNj80}*TNoMH0ygwwVkRywYG|z_W9IbuoTsrlUnRd;){CDM!kY>(XQI{j4h)zA zMvsEIHwldS$JzeJowtN;JNToTsVHXcjfEV3K48zt_Tk&Q&%N%Vj?Ux8P*KyhyCfw( zBs4B*v-vcR`@p_YV{~g8fHZ-);K1&@S(Z7rD0^Okk{SBE<+uU}RRR(Ha2St+ zoRGdHUduau@5}z~K$slCfyht23AE=(R&Rp!_6s=yw1KL7eNF`4^Dv3wz74*S>w`KEjFiZSs46`x~nVq#va2fzgn| z=C}kbnyp9c+>_wIsAVvrAp(`(A^q2pX`xpkD?v)al+DCDJ_=$~sM6@P8OLVkUoU6d z(yA}3qzK8-3`~_xVy2cBtyWa`y2`O|60Mn!>{mA8<@v+*1?cET|NPO!FJ1YdLb-4( zU78jq`X2KAuMCU$Ta_D7kr7c2{A%zbZo}e8y)nQo>v87@#9?)s6JfWf*9Qa9q5}U@ zi-G7pKVWp`eSyXh_tV1p4Fqz9riBFO4Wck!nsbB!NBIcY9r=~v0tl>JbSGEg^3dK? z)xNyG9@{smBYjus-W7O0sV!0)7#wVEYC5RS1&e@D#1g0AZAfQG=& zinJp3+N>d`ke2o%D{B%-ds~yvp-^G3Nx4aXPkG7f|yv=reZXh7=; z>E8ll^}~_erETqD*^E{{&MC*J&}r^TI$(<*@MfqF`EQY{{#_#Wfh-j)mS|6<8Ls> z;Z&O7_Ozo4862<-T;%>YO8G@=v=b4%Oah z*avLpCR~vIJzx)}3Bb@h?1SN8BA0WGw%7we4zE^4e64R+q~zB>xjN+EzgU3np$Fo= zk2lF%yy=Y@e`SX|F{C!3-(y(;(i15V2ibC!4hyMlVZRU;QlM(=z41dtye=_~wmhxb0uLr9kw6z#Py7midm& zpoNKzO`9N%F`Qiq4E31K2uk>|YgCVmn_I|CUB|6jh_zuWkKb>#lq`O19yBnnIUHqT-IiJq? zbjDx|20GAv-}l;UU+cQ&{LNwpdU{pb_@hXP4>5BK3&1D@rXTpk|6LBafHYmkm5i*0 z1IvA?ShfgD=_ZyZhkYl=eWhb8*C6_YrZi0yt*Y5K$HpJ* zG*ws#W{8|`oxE~;1xVR2rZR!_DJ_lAiYOP@Pq(*$7wPp9G2@@Zu(8Z*McPG1zgkn2 zOepGR$4e*II+G3lyo*bu8^lJ0^YD;%t};+ow#*Vuk51ay*)cM+Qqdap>-?ajCu+Ru zb^cyOzifD9sJgZ`Mk@FE+igjXn10R|_tc)p7Cih`A^{d^ z=lib$mT15zDunryggm5IBG1omsLp{o+|n+}1_u)It@e9Hx=H<}l9M@M#+F(X$GwWb zJz5K;RFfdQW96&6gXWtH7;*>T>q*1WGEDmuc?@kb!o!}*T4OtAXQma_3xVH z$feS8EQ@HF=kCt6GQVqj?sjNm))kMXcefS z`OL9`Q|4f8s?iMw>fvfUiqCzcq&n7kSH8)`Q%mb^v?UnWCxemqdhnSeO7Si)5N`;1 zzC;iA%v&hFq=y%pA=XgrvQ-MOB#u@^_Z}gkUm^Z$EA2*n;meH_mQ5=g6)bE0t9-CY zR@A##U`RcQwJ?y9D^d~9onVQmDF+-`E}jZXP-iS^MXKQ>Are7F2H65F43AGH+uI92 zln%Ni4t(Xx4{Wftau@KDEBX=1uQ<41thz(4vE;3Xlhd?x5BNuVH-e%b;{$~x_$b|7Ag3JD6 zSoVkG(AdFY`iQ*1TrUr|A)F@nZ#L;WdrGFlSvUoUf3;3Nf1Ixq^}U_88se4I3J zW8S}Y&O^Cd|AvnU9%ba^Hbdo9l$G5th69gM`K+w0=E52LoXOTm-$!Qf1hWpBRwXd_ z8yRi%MNK4&BZSi6GU`t_Q`XcU&X>mK`14y)Z#g6<7CiWTm)EH~m6Q5p@eU)TpKhX3 z!x%G~*o8B(ERA<=$@9KQG1Q|oU zSmhwDq{RG*3m&Jjes^GUH=Y)|YO2MGioCrjDuX8q`nFmm`QeU(!nj$$2VZvT3lj}e z{2h}Y(;I8(EQbteXZ1MBtWvfL1dGmpRbuaxTXF)C$PPlF;84IQre~(^FJ&96*(i|o zpb&kv(O3abyScf!GQB@60K-J`dbR(C;KDS4aFpO?(eK1mLzioEJrr_=9&B3Wuh zHGi(Dl?;hk`dLVXzA>l=O66+&t_gczO)XkM6W1hF`|EfmN5~W?wU@Av{4F*8GLz%(b2KR z!p#}wU;97H>~)39vR7GpGCGTn*b~0bz*;-q+|0vtrJ>p9!@$;!zi_`nh>$48>U491 zp^{I#ygVluuZP3H#ZA;zC{g`A{VOEWS_$f#vpvyxfn+>Y;6;E+0NKsqGWG)SOcZXZ zE*B}yj*X0*PPc#|@G+x7W4`j?s7{%p2u~0+9P+!l#oh({UKA_45_$$81tx7mi~OO` z>dH;oY>mfapsf9EYtG^+Zk#GOD(&L>0&lLoAU3DVw~I z2UMb9OOgG%2e*QJQ`J<|Qu$@!ugE8>KTs*fq)&M5kOdgF3^aeNvF(o9Sx}nSpmrk@krW$|5CC02UXuNH^=AEN?)41Sfj_aL)oU5L(!HM)*Ka-OO4&D;*(a;Q#V1VOaLql^jpE^L((k9E9dU3l@-PxI^K?->6o38t@>?4= zMqN9fkVBP|WqgI`K#4FT^ZaaFT7VBgRFKmRT^mMM9l!l$*XQAuw$uEAVe*kiYq>ru zue|((E1H8!fi4R<7Q;ExRE%nwz3KXAnYN?K-MiFcNS`!6I3A{UTxWAzg(xfgP_*E9 z3i(gc_4U<T3T*wc+j_vJVq*0=j+!J{q$pRZd4?aBP5QaR6FxC>6HEOg+p4S zT#tEp84Wo*VN^|Z4q2;6e^%;Z?c3v(+8l}CvSTRIqzr@pNzqA%Rn!sUeyG+Dp3JnA zZ590*c+@}O)KTi|tj!lWvPPPzEPU(4NR@taN+m(D86qEqEu>0tr7IE#=07GT-ALTZ zwh3;oy4nw`Gv{FNbg(I5E*d0R`snp9f?@l>z(5q=Mac6OTWDx#v3hBz8<-T1Swc@u0J%p>Muy;*;iNk2MfC(^ z4D+5a>~C*Cugp*Vbg7XQbBZvIY0|l*dZEg62%L%U-n@_Y1PU8eL`0D{g>n0n1rPW4 zEW$I-VA9BBvAncoC*Bo|dI~5i9gUgTl|a;iih&h$Tw^hb#v$eSczf>ka7{{-EL<1k zM(Nb+TiD=v=kWRSXCOVOVgnL<|NS4CPwM6R5naFqegvd0^z>iBF9F?7e{NDf0XBZ% z2AKFyt)7o(iO!bkIojE+>9jOApS0ZQr+yF+Ai@=J|F;Jq7W>4I$4Kya1l@QIGt`Jv z{EFforD6jiqRznOYG$m`@^0Tr4UymND`kV^0+FiV|MmhJw8$hQovS1)*iH<-+ zC=!p^kDVkRI;*r4`Zv9t`SaeS&hG5?j^&TAn!}mZRNMSQyfzhWx}lV+kpU%TP0fA& z{-H^0?KY)Tc=4vBbbk6sLJgZaRU2c5{W&EfEX@d;)JG@A(OK5DqYu9O8tVCh8T)&# zPn=k7c^n+Gm{U8t>^lxl^a8+$Z)Y*$xXi^Z{@j=G{| zN=|98t%-}bS2yA#<4kQW7mxmF-=3?ernc68(~7u$cq@V9$ov4hdhU;EOe?z!q%ga0 zq>3TlOCLARQXMZYkTQumtQWl;4@)Pt)suv186oKfB;@|? zzBLXh6_rZjFm&DGo=oElkAQL!j1@oI_Yza8h;{yF@fS(i_*6MJi1`MZ5`;U)fzxVZ!>O!W@wgIOQ8xRw-y zA7#7y2fE9u!AZUnuMDe8wM;!av9b_@8Vb|oPdUg{R{Z5p3S4RS$X1$)UEds)5-Z|?)yOaQLfXuG573w$OW`aR4MsuZh^3Sh8LE6f0TaI` zU~kA01zK-Y=8J>r;#d8Km^cuwt-2pFczeHS7<6HyXsV`R$zX6u+Yzc_w zNhCU}jhgD7e+iCBMiIpiZI5&3#|krm5urjV7y4fqs=)BjkPsm}$dCju56u)oWv&C> zN6>jE-d76mHv_JEdw;tE`E))6Vfe;3oXCiFa}|bjxIIYHu-pY1-b+B-3{+2b7L%|K z8~L9N-~~f)u(%@Uf2+}H{!;WubAT7UWzcKFGNR|=;-a8{v5dC*B2{JJ{e(?Kc2U&! z(T0C&4d{GKtgR#dy^bU!A>O@!anBDopZ2a3SPGvZvC+GTpsV2EV?e9$hkd)D6JzEj z4DW%N0ST!_Ka1djMnmlNPbr|0IOOZpmy;ksv#LCRV)zvuZMV3~-$^LBDbn`+N!ID0 z%uyCW#MSEgd)3`AD#6YJ#G?^!p3g{thZOxHp$l`|ECj7Whn$mYy zcJmQF{w?zH%O3T$!j7$fQk;&=i+^GqZ_^#*^u_6?Pn#oGS5Fca7itbA_E9tb-B?ps zKjS2qrkT|fHYu-B)tPTR@IXhh31>1)5YL|+j-1diHl(XXL5lfAPGLIxGS}V4~y7cvX1aUAzfg#+5_4k%W$J%9X9q?05qJKL4=Hytgz@i`trjV)o^S>t74B8 z*nh_zlB8h-yl#x`V+FQ2AM9eibM@ca2drX5B(+(CnC*kV z81ZGbCq~Z`f=0OE|GYB&9Z&N#1n;*xa}2&M-cQ^GU&L#T)j!deaalTdx|9dj^8G*pEz$wd_NaJ-^}GA*NNKYmH)nb*qk|vonFx< zT8HoG;2=y}!1ASVp$>(1ULHk&JrG6SvXufl6g6Ds zHS(h6%kxvkj$*a#L^cc~+5x^L;w(&j;H_5mmyYnif+S_|erOmd+N1x93`2nE>f#r~ z*1OR_k#<7gbYF=Ld{_yJns*L?m)YcvLY<6KsD{1JDd@?*l|fRq+HZ?s+0B`@={H_` zB72+_WI#|P{)Ug}9fWV{~20Du3 zo3M14GcuH&j`zbcCl)?|D)~+)LKyyR$S^Vi7g}QI13Y$EdBZ7MQ=v@G7HLn`@cJ*K za>18J%rctU7xOitH%o6qbUb@0ecTZ%R{&Pfj;(5lr10IY@=mV%%Ds zS9`>NCFe=YPi*a4jit7f)h#bk+!}M+D;Fu6SDrS#3>q6xe~SNhwNOQ;G3ELtlRMd_ zIYEAcSr1o*VbnC5A{GAO!h77Ai-c`uK;@HWUpoB;r32;_5lv+n-RMz~->tl0;{qXILN_1tmTkn;aN>jsp)-QUqE`Yd?X6hn68h& zx?ez8CCk}EBuC8TE@MJmoQT2+zunPo(WNJ57>ds1Usgw_W!++%OyQ#EdJD;a)2>To z->cb1oRPD_iY99s^{W=wL@?#>`B+^Y&K@fyh9%Q9ggdR}pEpOURdxv8LpYXK#XYdj zcq~GXz`L<`nzri@dJ`YsL@TSLYhv&hOc&_s!pWhyUd(eCaA`H@4GTuPiUrLD3r^ox zV>l~)+Gl6{Xh;RSQ2U&<``uh{6K`g%z1HIbR?&(Nqt>&(6i@*Gn`j5vAPYyxDm>~( zNtSD^aG_z1NbVF3r#1X#_oCp&OaOzY@I%|H_!NT@1FMI1j1LL{k}zb!TU#6d_r*9e z8tl@v7EqrNh?BR68W}rZ2~N(VAR=~B{qBSfE4C3nCg6eF2=*2qRT}D(zSA%VC?ByT z*H|ir9jKM0voJ9hUEC5Gs6*^pdU!o*&9Wb|P=qMh|VR@-mINpma>$CtvpMEKVF8zd_zV(@rk5#FHQpo=6T z{E|~GfY9ix<;f+nA+8g#SnFu7-7I37!@a?h$67^Kcmt&0-(dm$%@29)c9`6}0IkJb zmrHf>?s9hZ2raBZIo*14tz(nlBAH1H69dWS036CguVF&y{JR?rqIDV`(1z`J?R}{; z5HKm%5Q6di_l!i|L!Q86p73gmDzZd-D{29#!Do>`NJ+Hfw_Id{?-XDjH7Of@j7Q9c z@vV{E=5$p<4&jaoV$vJ&sg4i0Hbuixi#|0AuL3x*o}QyXKGf zomx{s49!im))M<7$kn{<>Tq^b|8jH9kOMDEdo{@cz6Y*aMbb^gi2JeT?At||9)0GC zeCm`M^4vo4S`=r|xOCQ93#gX&7O8NHVkT$t}O14S%7+CX7 z2u7iseJ-#nKD&s6ftHHN^lIoyb{u5gb>gkFrK-nlZe1gpPj3lJV6gj}a@TE~k(w>v zYWO7l4TacpzP!9=Jw}j*M4>87O;H2Q65spLZ>$xzHzRZ)x z?@e={93CHUnxf8dhbQ8~iThC@%F9n2>U^?V2igkltiHu@jFj>pA2P}Xs_pwh@8yMn z+JM$&?>#OI62avrr*Q-S?vWAhY|+Kbm_&t)AKkMR2^}b%Ch69T0ohAGmH$d0?`s(P zt?>UL5VXf(fi*=jEK$Wr1oU)FFd332E~?$a(HL&`Xlrq@GFUNPF+h6C3W76fNFMmJT+@0(-mZaZ3Oyy%2)RnmXvr-=;2;;B*+_!O>-+a7l z^4mfEH+_92(=1gVt3MUoSX5%}`RK~N;r4^&ZIpICe@x3IIPnX#+;(s1OVY_Et706R zP5K4vF|-8T$^M}Gd!=TzKAtZ5&1Le+jyJvnE@M1lBNCxU!@V&Ie~;b5?P%A#_dAZ! z>d9T5K`^%%UiVs3iJ>pV!@X6Pq)bVbXwzA;lwx4u=?#!6c44?}MsVJ$E8&M`GQP*h zAM7%E;TJG~?~YGG<{1>i>WB?6M`!534M9XMR4OQsT84iesK_iIB>_pTL+u@+iD$qxPSV?5iZ;x&3cQS{=LkdiKw%%9 zFARA>n#hN|_AUDu3Uv!qKz0$gmK8{kh6<48d`mVTA^X3=tNQN}pL;$p0#GHRe2`Df z|Mmj3s-O{Q>EG6Tx=eWEnLQe5M#ORjOeDh3=uoF9+X(zo!H5e)8C>kAo4-*_7@qO$ z|74M`pM1EKy%qI8=+gV_XWj=jP)p4xlp1p5DZ!UZ|5cXFnn`b!F&H*oBVSpVYdbyV zW~^7@!*}a>ks$B9JZ-DPl<6Y;C$ha*TCr+&%sn-fNGD8C=i1@8G4LU+9k46{fjW3 znjI87HIWK2IT61R$&!f7P)eW{AQ!7Rg^t(aiA#vyCnhrTXzix`LrtM8R^Xe(T#g;hlbnT8|U?_k2 zkV6Cgg>o$2^^r)=7Nz`j_TkHH4k?t!B}A@;`418SoOD#F&u_oIT!?!-qqViNx{pFF_f`jOi**Jnizg?CGMG4+y@!LN)4QCX-vWo~ zlIO;RcqdZ!Uuqf2L;6;m(;KzxQOU_f4mU;%(-`8|D#tAqUpf|Zc5bf zzz`8=7-IKnpjHuXIV!Z`TOi?wZHynQ_o*ygv&LDcf=d_2D%^j(!@nOs(3Q7LJZpf$ zgW9)KIFJaIM1W3}mq0Z}seZ?B>dFnm2m#*$oG0Rs<6~kVdCx(6&~Fn`dZ5pA-HU3J zK}&hS)fezuDw?lPmVa3JHgsyJwu%dfxIY2}#rc2-QuZVXj|W->-&*D=wlwL2OTJUe}_r=Z6**k#~l(G9a3vjZ5fW{q9I0|3@`+&J2L zoGS}bpPY60Xlt%Fn8=Lj{P5UH#h1uJ=%EH4NJ4DSzyj4k%_xP;_!j0C7@kDh_E8^= zMLvF#%0x7S-FQlGun(RTmg@S7Rlkwt2hr66P94g?2$XOhSJ5kE5Q?q6GzdIDmp{Ap zf;|0p*l+@}J!`Sn3<^DVKW4@u7I{LyVvoySZ%b3_*QW)p*+) zXrS4wdYyT0#cvjjN*Y8U!JoAxze_Gris%ukliQUv5D)pY7bVUiZ%6iX8scd{^{gxG zO@PM?Exp+4H)GXsN@2(_%V__b*a*2=9rNgw{Eh`@d^+)xDS72CD!H6#fj{?~7^cZN zR}5FV*%ugv9llx1*(;jfJJZvx^63Ug+JpFc*o5Vtpo*6-^R3 z(`W1f!_W#S>BKXdmGQh^W%Vy?J72JVRq=`M>};(PGU%FOB)3^>#?YS1p0s+JUgvME zx^57ikgZ)EOB?8A4&SjkT>JI+m73PssILf$wWa4s2_|~1<;8A}?wp{y93V*FJ=8`G zP4zsm%Q&Y88>fi@JC(tE&n_#^LHI}K2_MbD6 z99bd+GD(mNy_sZ_Uf3j^%Raj`SVjm{h=Jriu#6rrHCDIw`*>Z`gA)M%N~LS zgcH8y3OEBlC_`*lgLYWNd(K~TF!#_4$S{oVrQ@)6m39TU8Y}Y(Q(u6A%|Dg{w5gS1 z9Nb7+n_b(52HGKk8KlA0QTYY9#oh{y28W%Va>bgRl2T%doQuYW?P53} z_|sA%LJR-8Y$reK-gM}ru?uwu;z#290^`0&6&MoJt7#Ir`b~gov_n@C_;O7ku^)PA zo{87fi$p1v!$XYGnUPRcb@(=N8(VI=W4V(m-F3sl7uUcuW(*?$4)x>-Q2RLns(DHM zQEvY<5Gi=h-!bH0_U`bq;CCEq$Cax%1O<58(KQl!0ahv9172IP}Q)4^(JGTal;%}acYr(ZOo{|^Vr#xNCAA119 zrYR$VR`2gnTGcx_@aS&h>?uFkY#8M}x4MCjoq#LIdggm0AxM!pcua5I1QR66W*PoV zPjJ4q;i0qWSTNnEJ-279x$ap;thM(15J6C|2a|j|kF=vs1}=G2R;GD(c6Y&(4P}3~ z?9nGh59!+@%WX=qz}yA@Mk0Zpf0BaB^I+87`E^DQ<~ynfn->Eqoa>G~UxTqF`#UBh zJE87J=ZgeO-R~Cbm~<;$6BV1H-mNiOCR}sur6fq_a|?$*_A`#Bw1hm2<=432%Jf{K z1y--#bF8mn?--DCebbC?cjwDF??2pJ`0IX(YI#UBtK95AB@X5Xcw0Vh*YBFfJ+qW1pT?^XF}#Dm zdK}zF4`0M3E_wyurk{{Ke$`&M(xzoEH!yi6(!;lqhzt@=jP-IO-6x+!h#I#(U97fvIe~yxdW73be;x3BR8{ zO8S&v%mO@mgXQ@8_?+jUqP`JmT?dxE=lF^j2Z6D>bMQT|8RpyRiXLqRu;Qi83h`>` z=!i;hyvfp7zaWLs^Of4G3<-fhp+BcFLm?IjXY052^yK@9X!0Fcs6Ams;AI4QeRDZ%xQnPs4_xxit z+@;zP10^N)r?*#(Y2c@14Q)lue2+W+){W)Izu8fJf~fJgS1YEFqu!(MpUvf^iCRxu z+&A(_QLhjWpet`Kv*Mq$o!nsXC)PFszC^aqIvFB5y&L3Zusv)pYADK(JF{@dwkA6p$ZsBCe zyM}sunJ({IM%<@(tVF9;-6U_xpfO2dZXF;6eyMPN3gics*Q&f5(w`^`&Wb~`n&VQg7aZ)JP+x_-QIy}d1~vjr z*#r99ngOQcq#Z-lO=JDwQS#l^l78m=o3CBrz<+AtiXVZCT*8!>6PQ&CV{I`|BHPD? zO{>!`$M~7RcHu{%oif_w(>$^S!4$*2%<%r0I}5N25c%xC_9TE>W`DO?AwysoS&R@x zIM9f>PUwzWz&P_fn%P{5%XdTUBN|>jLq7^DPSrbQ0^WspBZU3FYn8SJ1)?NEs-}Ig{waR|{U@(v2eGKjDnhAmGg7KMhJy0*!Kgjy-@mPe zqjA0r{_T4D^RE{c&q(2g*Zcl&qC!hEhD34$JGUTNXZc&)*vOf0_bIf{k*l*FFIXPh z2{h$V^U>0#x;wsB^U`{ciw<;ur)NUP(2P^*Ye_EW)h@>cKK+o{Z^CwYt+t-`Pn`|w?Ll=z^h2prR;ytz^hmQ6JCQ%A4-Liz93vhX6dl2JXuFY$;sAGigr_bXpW ze3IX-4OC`YLf8izjcz4S`iN3rud)(9S+;N|O)`w9A~9=9iRQ8t&CmIG`gM5%2$sE{ zqIoGIFZnEK=Mo}zT&eH(GR3eIecg#QW}n+&irYyA!H!9JIk~`sx0mB$?r~5AJNH(~ zMQHArKq1a%_?28ePdkv?@2pXnb(CD(tM#ycCcUEq?kTM$<4CZyGfr}IO!&KyT;m+i zhxzammzMk0T{J2GPMhn`9m1bUcM`F=wH6N*d@M*~I~M#}C}X7@ZJpMJsfV9U9yXM$ zA6obiq)|O)Pml^ai`fKheX3_`j}9*<)_u6-1Lq%%Zfq6(y#;P95`F4k(pLSOD?h>8 zUoJ8|Jf!}7TBC(DNn+ zS(6YjV$@JrN~SHnxvTKkDI{l6P!73khs?jgx;lI%uV00%PT;{RzBmd`ZRu4AZE7FN z-WgKL%+^}}isQm`rbb&s{hrirx0@(R3<@Fw8m8YPqKX%ZT9zvT_!nScV9ek6^qCv| zQi+h5tTt2Fj5j`bk8c37-6jI;jp_omt0}_*Qf{8&>RSkxiC-S{%u8)gy;LZRZn4)a zPV26hMyX!hPzTWlw~AYu-0J^8FR_GeCi|iaRlj|9p8<^TBw&*pp;yb8PAGQp%riV5-7~j$Wq2E!y-><=9Gx z_Skp7eZkq5J>}_5eSF|PRPCt3d|ziE(EM*sd!j|vifx-#{JYkA)8!uyFeM1u=i_(j z-`CTKq@5aEw|3(HwNv(ZraQQ;W>!~wXHJ0FoMQ{Jm;fmq*E)Qu{;2zYHLBE@avyza zeO*5m-G`HFgpVEmY-{b#us#K+Zh7<5Xhpt6!Us}?%1s1=k%7kgAwe{i3?Xb;r&1KmHqhct5zoC8NKA}t!RC;$FGrNZ$ zkAmtRkDsH`A9taM1dIE+BVs(fd7E%vP7auE9VAl4K3q9pq)t12Fj{K_**GSfp_S5% zE%S6c^S-QQrW9pdwJxBJZ%*AOED4bKJW2YZan?* zc0)1TTCaY;N!9aLllkxxtcsTo!JT!hB+2p4(2tjLmmPBj89Gk`QxqSwk$E5L`O~m^CI$J|o^R?rgJtG8%`i$VsC85LSLq@-7C7%reLXyET0XKb_;e>c3-@m~5l4HF!J6J? zKeAA@_*0yz{EXW0Jp80=p1K-0H9AL{e|(y(_7ydfYiOa{qT2U%qpY@KJvFlumKV!X z%6jtE<&y+s(RVP(|VHleqa#g0u< zqFNFC{WOSYzhz#?|6i;&>dvh;>;_G1D6roCp4ajcXkwfGuhy-Q{D_Qh2E8;LV!v^r z#l6Xa`m10a@4c64xTXlq`Y_D)J*c8hlV0R!P?|gzv%KTu{6ciCt`;X7 zuN+J4{C1*%rY~8);6yW?|SF z+x7ZEeF1XG>@f6rw3SF`RuGKigM*PV`MbCdNB#O|&|%EBiM3#}v#kUhdsvO>`S0OT z^F{;#ZU`uO(L0L%mF1Ry=YZBeP}Gzm5FmT3HJtSd4U67U*C6#yb;UE z6Vr*@g?`n}{1;Y*7GSNtnvB^DaD;4hd=#Vj`7I@~2r(W!GAX@A-C|I&1vHUrrU6nd z=QaPO2ct#190^$u09S?D*$7R;fS1S%H+A)Aurn8bUrSs!o9B<1&S=Qa+jK}i1XrRn)wOvN_E#7p zo=|K)__n0fpG>cMaM-u>m)%W+eP27jHeORpdP~_7@~gagQoe4nAATHDD4gQ~y_H2* zp(q;gdnrNw=dU^uQhY#7aEDU2PjmA>ByP%U_c}~)ksnR96xm&ONs!Y&@ zgn~sbWRg=-LT|hNA4>yx1O8i>y(5e{0087-XQxPw1;X^eTr32rqw4{c)r8c_k2-#E zDDfj}$C=50*Zq3+0qC4C>w6P9Kqu>phq4ZIyr6H1_IuirS^}iMxwyC_%YN|`D^*A0 z50wMB6TkxGMw(euNoiW!)>@tA}R zrUC8C6bv}fMBqg``5(`^fV_~svsAm*{qejH-DkAf{fhPt5-uaw%{>?~09_N>EjU(W zktS!-x|}SzU2F{u56h-gQd92%)*X0MV&8m{lDY-_t8VB_&?4UKivro8Zbw68o12@j zf-hz$cHnJw0J2S^K5b&+48TLaI$qQe90B%y4%-#_FR)rY*lGM8IIrP70k|=t$gf;Q zGF5z<|M|QN6@Gs!yv6HmZf*v$GRU&TXUaX%TieM3gETTHF{M)s=MemI+y}V)jc!U6_|*ip|P(6 z5+R^syF9T0X}2God4)!oNz&_TdHw2?rNs$&F4K% zO{d=7)6)?G8sIvbnwk<4y+9#-cXHOZ)=xo6dGWnqnu3-#thggOIvS|m0BHN&^LzQ= zBmgS90euURH2~t}VE%`*%QEV`3F&hYy_tI0dXJU_id(l0~0<$8ka@^FOyA z*a=pb;-p{sNn07pMFG*9UIOzG`B46kNfK|8g<}KLTS+oeWM99)&5@KQ%JmOdZ z_qfv_os}pgD#}d6K=Bz{otzkA0J;O2pI-iJ7qrDP#OqD=Kg)nABgGG`RsHklS4LD1 zp8Z`b0M#)oV}1F+&CPvpK|n~@BgbCdCRROhtLt-PdUbR2Do+AocrhRCbYJ6s$Rxsj zA_fjZz}-zWZt$mbVPRo#5OIfUwmn;9rp=DhHT2%E!jMJwe?Klm0kG%zqYA*wxt_M% zdY54emi{r|xdA+JM*U{^^B`jpBhoQvcJi^lKFMZkcs^#p#VF0l zAjS+}i<(XJmQ|1qV-tGKU$q>*UJgFv_Ov7R)DQXVyllJ55za0bNhp|{VZh|6*>ku@ zNlAIU?6KDRkRP(l^ZKS`&l}w`b}!n$f(M%oyI9kLu>Q9f09&g)Vna=wIbrT+12GQ^ zxKH5<4>bAQUiUSwZ1$Ac=t}CsxaUHE3P_wuje&vD2?c zOcjCE3huyG05sol57q4qB^fKpZ)e$t^eV^TNrv?h>8l{b_OVR0k3UMDz{okP0+d6_#37gC z1!oW&mP<_P1MPVOJRQ7TTxVJmJ#LcXB$c;_zE(3O9p{9Egy3Vbj)r{zS&FE>0LJPD z?sl|P#cY({O3{oGu zt+y<7jfSEjh(0@a|C)YTuXoz&H;ktM;(?EC7hgVSdW(o)e18w#jCnKSUtCKqH}b!j z3`>9r_jPjyMIQ1f5i!ziy{&;j9#t-SIF~@uEB+uP_|gAUz@eJ9HfU2$gQ$zs|vE^fXrpst0Z7ZV}h;A5(trtA6cYa1k&q9c#FV5 zg1`(sVf#GpK}aVTVHIQ#Jn3MRBS@?X3<_c~R3xtdU@t-L!?rE@4m_UM+SW<>fxisv zNV1<>fS)GgL6umStN!8d-@lA2h{!Fkd&hgg4edUW)r>~d)^NJaR0R)=p)P8I?nL;^ z?#zF7>(OqW4Vxf8X6=u9vLPtI9XkV_*Hg~sUDgz56jS*K_YaQZQZ#^z*M9g6l4C|) z7*e2DPa=v?@b2;Qa>eV^_|fm_;k0RQuNdS67@X}VTFu(b{W)D@9c%VHTU)PC&(haV zw{5tM-y7t*WYAkMO=>KHC8;2 zuWqhS=1=$c(Fq95o5{la`iQ)>^HfJ%&(23u>Yb0~p08$Cy>{j`0F5qQA*?`yULU?-(QI{h`Fsb6j?P#tiMz5NDK2&j6_J!GTv!HV!cY258*CRO}h z!P%$=?=sqOTF9*i+2Gv}27vU;uwhOBAzLARrND1CN?_6eW@!yxM)qS(##ZLuikxorRljd-m3l!&|U?94d*7_bPRaauYVQM1VjMuUrU`gmccHF^MMMyDKCh^kc9GEE9P`4* z6b&iM+qi27C1IR{04imD{pVm}udx1~8%C}_|2sp#_im{`hja2>^pn9dyP7Ga{Vt~4 z+TIq{ZER@3L;BtN1~|6npgUC7Z~5WHrO5J>2W(`ex^Oc&rDh}{1^_O@usl>xNSvgF zL^1~AuClU{8SOKKJek-6*nvQPcXYd^i!=6THFR1ZD`96i?sYc~h~HcvKD>FiV-Qme zvUey81zh95d;wKXHt9rBhk-^7GJ^R#g7)2?kmCW1F2tSr#g(B90dhw7tOaAj6%6+d zkg|1vwx+1&>$RU^(w|F7~ zm^2yrwc4+bd2>9HYYBKByOd0bMt=DEXj`w1^`d*r=zaIuLrR6~^9ecfpp2^V1WsQ2 zfWX?Vwu7uJhO^}8BzWP%w$r5oeXN3I9w$3VOUwD6s|io{LoVCfhyoK%hG07JXK~uf z9v%(`_DrS55*r`Wk)59*&E;V#ZF%wVY+a8JnLR|Iro3^Lz1EIWEYfB;J}@-#Z-FxmY8RmPdLd3Wg!SM=)x4axSS%U;3Qig&0BLExJy z7o@l5G}P@tKPY+x*wX3zE?L0R?5X0Z`z9_)?1MD6Sk6tv$$2&(ug7(TD?y`do%@J4 zJl|tK0kgy0*||4^0mfQ+N(B!qD+qnZe4r#JMtYlM&9}M0+|&|HOw0)s=ga=Auc2bM zmBv&@M@QXqhr&1|p#$KPbK^`omu9RJ(Jd z0^-a0v5SCi^X($IVQJN9kDfc=kakABJckmF=jXXWnflo;$V7}a(}pUA0zgSF3#0bO z4>Zs|<^4q!xb=aXOGA?F1W$mg{Yfefh@h;3f`Vi(BT(Mh;Ibq;_163$7lG(qL#=Fr z0m2aK%&LFt>gsLJ7lgwtb91*YD{f#{w`Ke{TdHG*7qt3v_QQs0eKrGNMwktqgEy(< z<$ie@m~7ee8X{<+Yd{k1#7emwp^XN=f4m>6zts0|`Ij`nHT8IR8b)biy4UvZyo~ug zzgt3Guv|I&IwbZm4D$8NyyVl?+^wOx+&V-KioMq(sF+$(Sr$Hs@c2KRy=7EYZPYca zf^>&;DU#A9-60JUf`EjCbPFP#N|&^h0!oW?NrQ9=2+}PQ(jmU^$@3Z%{*R|GMbIp0cT4PF|>7vw3a7QP&93glA>+{2XJ){qY4-#jCq?6;*&Rfng zL8ghzbv+9+R^#b6zLj{j*);yc*SbdDP`Xp-gUZj)?Ck73+gr^-oB5pu^;Il+MthRr z>!(+ZREwhUGJQoZtjrS85b($%%7r|E8`A87UUhT)n0W%^!9yle7|sTU1`Um(9jAsN zxHQ~8GgiNli4|*750B{iCZFTIrRDKz6@+bM6-C!iJ&+|4qdBGF*xc}m)E`rTzQw4g zX#Mo!&9r=c{nOLo$CxWz-DM|; zy+4G5M>~?@A^0+f!^bH1umRmjM&FV|le+NulBnjvICv<9hy^lcC;{qkH8s>U;3Ta5 zGgYiw>r#Eg$(Ip{TY#07j8_c8vm=b0Hoj+<(3oRls>08@=&;Rz{Cv$r~b+GENZ^WGIrM93uf^L<%^s7A_zXoBX$wrQj z-BY$VYUz43Yv5h(P|8l&g=NBb^ER8=x4dr?iv(+;#|3GHJkfeT*S_W~5(qy`3#Cb3 z{{BO6A&SWp%^|(+!>!Ed21;)>pD*6=5AI8(vqrFSrw_7|3YR_<&d8^whbK0MOHE7!n`>pI&V1$fFNf*& za_N|@9{Id-p%s0D5#h||UUV!LvtExRUej58F9{4hbMt*VYaqjr|31n(sW@aSICgChRP?G$8!K`fefmngkm2A|WB0u+TFh&Of1;eNp$A?Fb z&L6wPS(Uva@w(!i(GiStH52%(uVI4NiM-Eb+P#2P(;t>K2b%=_8|-rT%Ph8>OV$ zQq2e_Fe6eV#<+16i2an!t&KPC=%>f)`hBVmAO0pB8ciX{)SR`uHmCd6@^SVuO`2zP z#cV|Bv^$luvZ~jKe@~=m?9)H${d^&RmAiB#7$>KvbW)0HYL=74b-X`VIc2`i_+e`& zGGhRf$oP*+)E6I_IHBuuI1K(?Z3`A}bzb%=HflCoJ$RAXu-{td->5<7GU~P;Zym2yEYdei4x$n)V{Ftu^41D|UXn zKIBoPiiqzX#v2Ps6Rt7ln*uu`L-}5exVY6hIi^d75u;<;SmI)1%pY}*!*;(KBypkN zdv|iMe^?dMdtY<3e};AQ=lE+w7z~|0+?-|JRNOl3W-Tv6-D!TB9vR8^XD5#T3v=4% z=oH=uo$X+s7nH0@mW?7h@uPOkGtyL1wdd zivyQptiG(QyXCTO@YU;-3?H+{8HhWx&=uW4WM*S#XrimVU)q~&cpeo+!{@jac9pOb zYpU{hL5deMks_wa6w33Td+BuqWfdFa#Y^Hg)iJ*j`-~R)A93+T20cN~M?TfAD__|z zZ0x}iuxu5XC`s|{hssLHnxdj2@9Cj~K1$L+POLzrUtmL0;qvn{@#l{bTd(N+TNR0K z|MGg8@oOmIB0x%`fYj_3{Ek!qvj8^qmW}OIO)Z0K{TrK$W4TJ9aU&mGVs-U{QT85( z;~2NT{tpXi`n?ps3 zzZ$KVn%WpwxTnT?a#^R#?Fw%^bf@(A@k`d*%bi-l**J4M<5ItJ;}G|Tr)Q07y%&}m z5{WKLqS*7_^kK`p{MpY-$PqhR0p9=e#_^gX`_J-AdglOP`PF26%<9l@8F?j9(NN1^ zYk8ekRV_!y1|$6Cfm!mrQld<)6>WALQY)n`FVbH8Do8ez8SA=?fWC6PQWTm8LJ$VtK21Z~vKY zIC%o}onSe@>(IYM(+ILT1A{vx1uHS_4A^6aPo6M;d}uJZhp}44s)C3tzqM!*w zV9zNqq5xC*?~f%lGrcJg8KUfl$bN1ydX-5%=GHUcA#NYN!|fLQ!ub!LX@-JJ4Fwu> zDumr_ot*Xnfj&Rj2rLEXB2(`5PV&BZ-HJxI$f>+z+NGK-Ublw|bL_K!B3zRCy1snj zzk4@1H2Vr2T4Q5lM=}fudjrc2@bJ$A1KZ<;6Tp5Id)$rJ#LUm{4nDfz+2Q2s`g}Q7 z9s?6o0C-B?X9#xnZPX{uM5guo}f zN9;yA8M3gj09HzHvvbJQ?LzUa@lfW0ZfJZH``sVUI9l>#lWV~J075U*ojc3hk3dmd z3_ozNi`omputkfY5vL|Vh_pPH2XmF@QnyV@!QI3gX5@K)@xh6QK09p;w4wwGaG@CR z8Y~CWH-Ma9S;>c!Gi)U#B}J++v7W>;8w>7C7lwaK*;JAso_p@E{+KLp^8hV10x`r5 zBn9#!*9Ay+8v(d4By&)7`{mKqo- z0o-O&MlGkOHwF3t_A#U!g1kvDR~o#}-3y+wqoq9)Bt#sOtl?)C3gDT@jGUaDcn-5L z-X8A|z~ynYH3fGrz^f)yx;Bk>}ad!kj^MfJp$2EqUjc(TY={{;>xTaE~YD3PXdE7G9MA+p!Rvw zYh|9Qt`&Ipvke)1xJpV&&baXSLEvK|f{X`R3!chGM$`_o)1Uza^U<$Cpoqcs;;!?u zg01bHtveU(E+kHX7RLy|a?%CSS1?R4@ohN|IA~>fU@0AfRsQkg<7#3ww&fUfL!Fzq zZp8|)qb>Aia}oGF0WZ-$AtMGoJ-u%6lCjxYYKJ^1&(_V$q&}jh5)%QojQmhD6@~equAX5$Hz@X)g-80V$Vr;#2U!%Ox;2Sd0EXrt-nUQK#2b2A=oDKhvtzPVZaxrhPzR}Nx~>wQ87 zxmqk4+~yeM+}SnIDAv(41?#B-QwG;jz6y%>f-43KwaHmp)ppaXOG`W^t;osgk+tcF z7-Z4W%h4R6YXZv}=4*X&> zph;tlwTKNnN;@LPUggWwk~=^@Wg+5I4}_-}JiZr9CKbyLBSs-f|MXhw%7;)?^QQ z$1+mwt&S8T9-OUZLVK!G_inbSM}P((7J-B!1x4Ctg4_r52{%!Wver*FkorTaG^sm> z0l2eF@2p8@6((sqS|1J!m;1u>ccn9cv76@a!tn53umtcwzM~&nn;v+yl!ilj*YAtA zt}epSjl4Jov^k(5DlM`@^pfE#NK_wzQ?u0DRxt2wc7cnRie8y&J|=OgJweg#P;$2` z>2zOM8c*tu-E2dOuZogVAlgCzUVw}NcN6q)0DTV0xQ>nVZGC%tz+S+Ecp?7W7H=@d zh&+-(@5)s!y=Tvaeqydg?(whnj(5|6G`nH8&KLmkK z=uc9-P+#Ex3X9odApJ$BLJo^bl zFhm_N`Z|ZsJ?us?3-GX514{sc7`Vrno0~VVS_HpH;j|7wH{&k9RmhC8VlY*E@TBkm zOU@FBn{KIC5`cGlqHG`=Ig+s%_&W<=3`vEWrQf>+8qjF}2Elqfa zPeE247Pv1cKrtMsP;r?e+wg*s+}YU~h|>@jCqj1v8Us^Rc4<=Ce%DdOV@h+ltjCyY z!oUnhss`2==+v9G+OiWfR6%Er<0+Uk6m-E>q?>Zp<#X)^myOZxKMM^u23oYyKh(9< zYn-_s^^27m2+iixBnu?slP3!s8Yt_N#myqoKH{nBE(`?#!-J^+qpJw)-3vc{9P8}t zlz2}!0yWn>Ijd41>SYRh3=0?2+UEgt32(^uc==hjGVDzYN!yUlGPgQuib$DT?JvmQ zk8eJmJ@>gTy)q$yn7UngV_NGoVuSnh@=lGQMI?cLUz<1nl#$+`I|Nc~ zVxqQ>Pg7tZ(s-|+I&)dEGDkRwWEmP5L`Zxqk((^y%>Q%1Rs^XA8uyns88*?iFbxe2 zp#T6v9su1vJsO}BY;|+&XB5bv)B>?VizS$0*+_*giO+TfN(}{~8ees0rufeBGlz6ALNM|*0UrcJEg}H-~{^f@3?5WOVAQK4x9M%@!Sp63s zt~lUX4-F5;;vb@aq$DzgWKrJ%ww(UCatJqBS)T?6qw!TWv z8gp#O+l+ea0E<{3yTJZWLWb=0@KbMHT_WY1eBI}xZ&9A6*C5ph#$suY+FT(f|L;zy zkZe`}ZyEZ^8Pd??gwiZ%$^bHE*dHUT5)i&3JUsECurq<}2)+I1!9hmdSGRBfhXn{k z7ZCm#-^qH@#DwCO6v}R+w}AiRKit7MG^Sm6Z-c-&FMk%htL0R z*@p(9WQGUqLy#*=KiAaMtlZ-0?grA$Cn?W)e^j7^c+e}q2iugkc>l_`S}ZA9b_m@( z1^SZRySo(NH5y@SJKLZafjB&Hp`w=VJZhr6T0#to+Zh;y(mBJ1)y+ou>dX3E^KZE5 zC_HvR(SU+VdKv6X(1Ty>52W&8K8mIh_W@2sU_3n34M$&X?h@fdxo}BF-R!HrxpwzX z!yTUsARyr|n@p95TpPg=xPGIA^!|c&@)Z(3s3{+xC+Zr3AFai+q=DgCX93h5%s&+6xYfHjbKMa_e}@`@lVGtu>~UT$`79&MICccjc;W$8C^> z4(20bdKh?Hnx*08XFtr)@Ya=FTPF=X!>}OLz?X8dQ4|6=ZMF~U>hNQa%$1Xt?1XyzOs%C_7lG{0bkDhJZwo?5S zZ&c@sUhOmT=3evfOJRZr*s#O;1_sz!SQgP~_o2V6w-SBhLu7dCJ{}$(r_*9PW9D;$ zpQMaGkB&m3_?ncvClFo`J?*3kDBKeF=?FTz|AIs^gs5&d&-L~6_DTVW8Nx*aw^*e2 zeHV#)kR;QNP*PHQ5ND#d&CVb0nb^iqIy4^M*kTSsmJDJj#jy_jNd()+H~0Wj2LVEqkQNoG`JXGj=;-KpUYhTSwpDT#!wA#LXqq=H~$hENRgq@>_Fi=bIldyNgl>17NGSJyt>{&Z?O|F$>?7mQ1 z>Lf40VZn*3556M9_p%WJmGaJJR$GUZpRlEgi9ZE7HFZ>=1fkSZ$uv3wY*Z%41o61W zv(3VoFGW8e;x0X1AIe69auKDP_;cnQ(XxLgTCB&lLmu2Uy!hL<3kV&{Cuc&zhW4>52addTEKBY$+{hz?Gw6YScW+hnii?+>!%8(&m^1gO$ z{oOaaP+nio2jUuE9(}MX2yXu1N}aLy^6U{4=WkCKInn8^n)04hE3L{m*r2`$0=;Z(xui?rXFq zW{owI4eR&e1x5=V1x4@mWxuOPv`DnLFw-4$#@ib91bCXdPl<0ndvOzdn+XR7ZX;@y zgN1p z?>;TyAV1N?7=M!!65e`01Ac=C*`d@@8;ziniYGf$8Yn?(gYkj^^6TcR2Ipa|4+IG8w%qt;6i~fdj|)_otL{oHQU4PWaO=q z;~JL5`z;8nQc|rT0UoG-m#7tVAwysCy}vvW0^IL`9I#XiTzXgP-1mV4EGa5Q|0(Yw zSgJxLpt6R14JFPD!tXJV*JNB>^7UkPB&-ixf>wmqDS+K_}$e~cSx zX;}bHwhv&0($dn|(lv*m3vdiKW52wMIxyxz({sVisl1A-u}D|jh_4a4l1ES#;3WGO z>Xstd^(^ZJ_4~!Ds;Y<#25#tdwSzcpEXxKn8Z=_~-GGic2?FwjhAtNQUJPg(%s8`9 z^1eZ2542K_M*u7~!l3nCF$3m3^~fNM(#5w)Mw?R=j0u9ut>@SC)H0(y#5f5UsNV>e zTxX}b31{q5z&vvRw&siEr!#2`e%m9*{h-HvC8hD{j?v5qI1QvhKQKL8W}4GqDJH=r zZ1n=f*TVx?gI7gE=1Xn85VQtgH<#!3fXe@c~Y99!f2`|^ICvcK$;#V-a89{8v2 zD?E}9rG~^H*JDDRSqBqp8=zO7F@9_G5}*#e7tgTgZIZ&S_d#Y$SqZOG;pAEpB(FhF zluLun8?+-9uKrSEP(IG5VQv+m^Q%4=-tqo~mZK3Dd!OiE-y?fe{ddWiC_vq~hDj38 z#~)d-z(fES3Zs;4^M`hz=Az_o>mYTzqMrxw)0nC zGL%5U^yckbNh`9MdW_QRNmv3!t+m>Hj{yY$2x<3<(=o}N2YAH9*S(o6M*1E`H_%bk zq<^Lsq{OhNxBuw#MzH!4(gE-fqs%XBin}>uGfrb*i)XohK2A zEgW4kzXl(i=%JN$hQ28D%frUP;*BcZ)wj-a!}hB`8P|REYqt$onbU0nm#0xV)$@Bt zg!WPQf;x(IOv=;mU>euiC9p~JS)wY%W1wFvP^RKP2PK)tz>o{T*lzRiRAgmc(ad*Z z;YBi*)}HW`csBZrnVb>g40IjfDo9>>dwWw%FQ>!vSY26ZeyE1Meb)(9;;(g67y}a4 z4Yc?AsczNyxL3c(z%+)N^LwK?D>d|g$2zgQ1Sr8 zBPKYaKw#Oqwy@BqKY}^z2uu+QFq=O9f(Jz-gK&BcVzC3f?&VeE7Fa zK72WoB_w4N4fUCpLLj;nH`bpT+={bAILPYdE5*0mDG>;Z8Dfps|^$&l0 zTfDN%4Z3j#sujKeN^Y3v;do;e%3$`N2^MrYZZiHUGJ#Xu2)|^LKmK>BHIeJ*K?U1c#0FkS?fYtgDX*gc4o;Z4 zA|97RRMgh7d=KwWGs;PLxO6Y8q)h=aab; zzdyjU=KcEdpsy7iwa(OL-enzrZJ8aI?nR0cEfljl(kbM#zVKJyf4X#~xsaQRcZFMW z#zz6cr{WcHV@6wAjK9SpTYpzrNKj~(odzEIY|VFz1aYdF^`x^5ckXtV>YuYDzW?AH zJoJmgI&M=m@>-Znu+;4&rQ)JH@3<8a3q-~xcK~@|WqmRs;Xq7O7`}`vpk}nEzgOIa z|Gh!B71a&XRrVE(+u#Wh$(N!L8S~TG{N58XLBVBm;}-lUWc>d&fLl4b^Ws%vM%&*5!GFW16{UrP&T%vF#EwI+Sbfp{7?$psO~x6=4Wj=m|=_Y1tg%+ z2dn93++R02JQ=ECM;w!}6&pPN92zvQ7}P9u(HVpG(maiKV<~!q*Mnt`-1#RvK7WoI zXWU4ca!%>%AD@y*4G74|Su}k_!=dA?sM!1AL&^DD=i0uGj;YSNJ@&P3m+x!eZ^!pI z@Bhp-TWn|)!P{9k-dSFz&6Yjdi~`jYrv=L*^m6pfU3B>fQj;@UD9mBL0|>80v)%pu zk>TN^)|GU0bWpx^q95(d?X0g)LWX7`cLY!V2U`J{kE_v%w)XC?wdo_**9no?F4oH8*x5ejnjna~Ke`v~(AtQ$ zyT3tzulk|v$d+*a*2y&5feLOO<6i}aje&V>K0;VJ9 z01XruxvG!?Lds*&$BE}N`4j*#G%Y5y$O=@?Q}E6dJsi6a`(2SnT;`+Q32~cHoNe(k zUx!~OV`B%Hu}M+^NH(|7N;^6o=4wfVb}bTK{ksqX*lTz?t5@sl>m>-edHl#@A3ngM zn3?$c6+`9F3x`tJ=jTPua9~;;8X2GMmB);IgUbmA_boXLqFyvcF0m{qzj>2)?%_#_ zDTM?cZkl@q*AI@P_7CFl4P-p@fec=Yf40 z&!;m0@0i@W4AQly-`q%=q)vW8N)AJrp&`#kyz%lXS7mLZp9Q8HC$}f766-&^idSf(YJc{cE&|qO0lchI{KQg zQZhyhy)0=QqJbz_II-x20ruXzRVMyd1)hC)LML-8KJNaCvtHp)3@Gjrul`U}o=IG{ zv5rL1eD9L)h>3YRAR20GXXkYsHjke5VZH%-Yocwcr@Zyn{E4K0lWs|;#pA~=lB6V= zM1*PT4<029&v5)R`s8if|i5xgjRC0$}Rj? zsH#@gc;D5atCvlCFM6t~K&$?;Kv;5kfo9GXG?1lC6~7PN(E0KzsnlTFYkqwlA4x-A z#C4g7*QlP~1pL5bHHJ}fGi5|Za%W$qBra_(&Xjvo0F$zPd2H13R->qB)^QdKU8uTj zh@uS8<&>@7AN|np=wy_9W^>7E`sj^-;SXl1HYUoFhn zUe`Om*y#?Meck;jQhKB5eK40oA{9okr{Jo7?+r=zfvBVs17XMY<=t%WTS6aYO%0rt zl8MLZ?hfGwa54%+Bbyk~MbVFSr4I)(K=wkB=m zwmn0J8v6MRcs{gkz;f_}svX+%%=hz1BV@tDYi#ESVAtrPw{UI+(F=o?7S#7V+}z5K zQ3lywYwi{LkTU*)DiwTj8P#5=zj{>#jZ)j-ORaZW&@!_7yW0vOac~oLIlrLh!8^^CMOGGeLGL3GlAw)^6!d#Njb847upHec z_CU2?D5M(0&hw!C-P~^j&;#cwrr1k138-P)oj~NC8f1sVd>Q2=_KgMd$+;`| z^f{1((J*KU(7DBsT97xjqQf2}%rV+sKzj1B(QC64qt!&1#B-{3@!6n2t@3QOt}gxl z;oi&#??>+L_Q5WF2kWyvz0v}M$OkMRZ(E151^l8zBaG9`E^-{{E86;E!p73Tj>Zoi zn_#KfptiVbQ%fHt9tiAmn|1v-BW0V-j4EH-9@W?JRppr4Ml?u>^W0rR@v^nnOF{r z3ZRAkCA93|cwbhG4z2LLjZ@h7@up98M92A-=5n$j`*-|B&vEx1(K>YGFCXepO9vp) zEwuM~k^~vWy^Mar!d7SidQ{7%%C7BGED|KU(E{n3&LSZ(bY!%f*T{JisClUm2xN?% zoVeQQTC+2ZDVla$G1b*!)EY|I#c97;LAT8@fP%%Qui{RD6U@Vh(-N}|>7ZmsgZ9~< z6^$l(M^{U0zMudFgc=aYD%vceYZaG?Lle;c0Q5LWV7{+T>2&A&sfG0ydNe5o$*{)U zF5@cHB??0=TbitTj&nzO`J_+CF3~Ady~0`9Kashp@zVVYIh;Nra^lh5M+zeDYvOUX z3VaK@fCVRHW|jV(zCE4n0y$&N z>nJDLU-%uSN9{O+f>ESp=-zz1(3(TO-X+1868b_E)nKA=__5|W4$Z}@1iYkpFUo<3 zda}fn3Qm57m`OzsBBWZDMV=qr^}YEyKdtMRJA157?n3CB5r>LpC$W*yL7hhW>-QS0 zI~L<_uT;84YV*0pl=dd|nFYSh9TjuNt?UcazG6;> zE)}qJ#KnIDS5XhB9KaWx2Nea;%DlWh--{(0yqF0iFE-pK(AjSG5v7!!3^_qt8+y!5 zb=MIFY7(0d*Ob-O@n#hE%@-lS(|z~CowQOG_z!twQ;8(;@}tG*}kr?h=> z9~7&ZmojM*HTC8u(;dII1WtwVo=S{f9hn_&nZvbpEn*F;D3|q#qU;UDu-jQr=W>h7 zcex!kjV_OW#=qlXVxUYfqWqXyM1&}0VdFq}xc6f|<@(&@SYQ5#Nz_}~CKdUc{yaSd zrc`@gkBWZACA3)arm^Y8kL>9h=sRtBn0R^Rs|qB)5O5^7_MiVx$o!l?!kyUGVVZ z=!M=Lno7;%o;Z_T%9w>GqYOJUmA|_+&R=q;khynI;M-65@K`=()8oZGZ8^)J6Ks%pab);PGo|5NRmq6in) z7kk6kcV0xG6&Bn0arUN)>=hv|G;Q6wpZ|D%n^Mf#AXn%7bjpP_sW)c6XOVm6{rWoE z4l^wdd)VH8eMM7KsXITH3PwG2Y zk&%W*3lbg9(&C~Iu%N<+Aq}oIX+v3yxylVn*EQh)mTy2SgPfdP?Dxg~b<7)C72PiN zPD^qJwDtA%bo(=}qi7T#Br)AVHc?Z40O(eg=TI#B5Jy5j4j-RLeIf!;9$@?y! zV9x|sF;`RCy%&3PTQlJ_du2oO>9@e|HoVEJKk;K3GS>ipqvYje8luQU^5f`X7ZVUa zrXH}`Xp&ai?f5R*fA?h^`$G|jNA9Ys+fLMrJ8P`$WVolH(lPgYbFh8#i?n!wP zwZgPnhHDeM6L#;V*Df2K14Pl+j#!q#Ulb#ve^B%vwry}eaIBe`wLMpQ<~Vsnh#`BS zD~`a_#`?Qi&dtY0m+yo#tRBlS)kQO6gfr52swk%onepeUMv)Nh^{KfkHxlAgb2Hy! zYtBOJ$-$@Z>`Kr+NENBDP@bwimoW0lmSUI3rWAC~sdlA~O9gl1%` zM^&RSB|1wUxs0#b2-r_m#g_^+JD(D$S(hDmT#>_Dx@M2NXfuG=`mP4b_jxRfYB?Av zz4keK;y&>FY-OUbl=2LnbLJY~j2)*nANiW@M!U4!{b;#j=MwTXm34m7kK}I6YBYO; zao%|+4P`w>5R=kUJ_pT5PLb;*Y{4B`^k-gA`_Y+&2L{l*toep+g^}hcQT+yTH)cqeivf zHPCt6#ew!UIx{n~H#)4tbol!rKs??QJqnSlo12>>)|K8IZ0zeJI~(1#oqHo&5}YYp zrOd57Prb&@KZbfozoc6%dT|m7pFBTVSKq&gWL`@l?l>iO3OkOY@!oQh!^@2ec~vsG z>+(!qg7VJGQlv&9z;>Ogc9YPD2L+$%-97RN80I_J+kH)e^WeFoAU&P`$iChd+Ko2l}j0=fqI#Nsk zL;SsT*8_tr)!(N%#f?icCND}$?Y~6q*Nl-q+r`V_6+I5_5>ym}d&=d1R~|v4a?+Xa zx=)&O`{6yj*BnyP0VNH#g2IDAp}__|J=}k0o6NACl6m2iTxcPix(WYOxNadKCS$@R zrT^iLW)y$$=)jPF?<3%BC)IzjI-(LL&By9*HHEooD{ig;Qp>EVdc)>yjw81O^96ef;2+gyW%)sH!FrjEA7nz;GR}7xLEA}Yb!2sc z^ny95=gtk*TXF(A;VKOAFd8dI1C(T?yDAnIt8CRXKVcrDES5n);21m#*xA^Acy$Z5 z?XUKqcX0TWLviUt_;JcsVRuwb{E)i%%^P|~MueT_q&V=L9{oO>fIbOa04}Bwi*74T zsV)Du+F^|aYki|vqynF)@363E3VKU?S7u54X&-oxJ?UPLKY4MovPG={OFVYJ8BiYw zP!Zd-n~+n@BOO6qggNo^#l;0!Ss}W; zxFWlsypO-F-Ihdam8bTk=jJ9r$x5<N)Z&i!<=s z?_N=%!lv#QqrYoRySE_BocMCvK6l*OtbHBHddRf0KnscdiZWii!d0_f@Hq{oLopAI zhWcu80XQ^!Dgxh-*LLN_&xWDO^30+|<7hk>d9Ph)Jk5lL3UZi3jR!s@sAnEr&)Vb>Kd)oU5f|m9c z?Fw+RzuJXB=u&-r2-_JW(H$`}8d z%~Xgpa()_|8`Ncao4?f_j7~jQ_0+Yh%I$GpK7G49?uAUE=2fq+}&tts4F+ zWqNG<9)bFi`cju{k`eeP>-Lv71cF8pl1TMf));a#VB-yi#3;xP{QCar_p{A+`FJM4 zf5LOPkRhc0o!k#_LQ6{{(k1M%d*w~Dm#diWS%QbpepOBwrKP2vgFA*4#^r9Pco6Xm zq8J?4uRs>~9iMse7!X#_oreMzdOS4;LFj}HmdUIie_yPLJ7SKGj(%Geym=E;pU6BV zE}%~+F5V2`43RA4t=~>Ea0WSx>e*-Ef(ML`>2mmZd20+-1e#C#ff$jk8W<2T>#-CG z-23I_WvpGprvbE`Y|?_gOhR2wkr{^1TvN#;4Z^xteg@Sf*H%cQa-H1I{xrp% z9HjGH(&}Z(@YUs7&D^Zm9AqvAKfbl4C4?h1wD<{WNPu(l15ngK$`732fcxd417fAn zrPHDzWPf-<@dZEt?!>ta<6MP?Q3|^W7*`~Qt{TADkGrij&n1P|%HKYmd3lfz1m{K= zo`|^`DHrJ2?g`3MCSN>h7J5>kx~sZ-oN80zl(uI1+7o;;dFg^+^K9y-d`+h;n~sJ( z1=C?r7(&VKQQHpKO}RICe_p$Gt)yo0WnG<+f;=KFd02)eW%&KTCnoDe3)sCP!ek{S zn46*md)q*2DD^Xi8Cd>ZYyD{xQ&Z^CI5Z&oX0l4!?$4H99u*Y@vRO4XcMBwgB-C5x z0A0#DJ~-Z*3j6^}`3#;Sv;5jxvWLl`m^bd6K-ewhguaX#em+KBw%+8c(d-CrUr zwL8pvdwc4x2;IAxfg|_xhk{_RWW-($!g$eQXJ~c!s;j{G;%L5bhS?}s#|y;zeGJAeypSRp`O^Z02S2k&)(koO#Qb{>+vJ`0{BZB>_VEu$`~ij<$}?-rxC z-w%|}S7-9jtif?yH=&vP^Zw<2=5Ls}1Isb=rnfGof^30${|(FsCEhLE zK=9EAhirKMG+QHX+c%wBgo)NzUL*>NVPjRnRNX3YUXcW%)i%&?#R~Q{Lg0N2hS52; z`!G)y5z#I$;{iwiz4QB0O*uvF#xn@ojX)^{6~Sc)l!#!2#uN6571o|^^Vv8M{^@3u zr6)|%Vq^V(Ad~?)2JCcv_rf2|ICW8hSV}vNSQ9)jGwb^amS|%j(?f3Py zYJI^$Aifd!G0?akp1IS)kli`j-PE)+lOh%Hlvcc_yat0Ffp0_rDn%ISGO8$hg8p5M zyW;{pjN%B2pX2l;e3vBNA3`Jk75yOYy)TDCnr(VJ85t%XvV-&nQ{rxo=^` z^HvjG_6&w9_O)*}ER%iu9psC!_E`gMA+DRaV3mCe;ULZ zQ=_AZR9H?vv%_{0ibgeh1Um)MzCLR^mCg01B$hsj+JBYd<9iQ_Xd$%BtWy8~PQ+4+ zg9My&hu4>u+Cr|;Pfkvf5)mOAE>qfbr`)-LxlE%q1K1(I{Y>(!SDi}&e0_S*92T1C)? zVUjEo?#3ev|7sx!<`` z`Q|M)#G;PPc0y~W;(pdiXBnz)>Jp@MkBDF?02Ij(nP|Ew&_%GX|9nkdVZa(6!}FI{ zQMRBdo@Se_jLl&Q_ zIVq_A#h^H*$Lw0GVJjxZXQfmYj*J>B<<5l9jgd}#hX${|VNP4k0Gq!g{M4i`UUr~LJsi`HG; zXHVAG^KN(xzcH?Hz!Mgzy741)`DA-69FDrFgt^)KD!)&IRm;|P#ANG?)6&dL7xQSe zl^}szR{No-{acsxaXnV~{Kd`k^aOi==7R=W4%>ebe`ObmW_{bH) zw)nG1TnDxh@q-48*1VE0dVBF%UnV`5-uLvf_ddYdkBQVXnmd1Cz>r3n_^fD^9`^~4 ztMPduv3OjR`E>lhUldkh9@Cmv-}PWz-%D>b4vDzS`Mqjbz^>NAZ(gBa=D2)S{5nZ! z_P;BBp_<#cdZ2H%k@u*XL%=AsvL9jWZtqXq^@r0?@*t8 zRx7p*(6;&~_>jx98J6wvuJ$No&;a`+mq}#Zz)tXv9)rXd@Tz1 z*3$P`o&A|xaN|u19wlK8W`WhaTJPCP_La>vn!^6G=x;k5uQ^?GX6UIc@3*`rJbWu| z_?+jB{+#A!WOp$1QhdUHdIn!{L3b-w`w3w5H zx1Wg2SZ%++jWBp6#5l%?w@@HAQ~Hi;!>c!H94C1D?67qrcVFswoRAZC;F(o%D4-R=^LtN^M^` zRm30OZ?QzvufroIrDVYEL?@<5ugS#ps1gTb^GBe~p=e0>$Z995?DieOn%w1>d>gm4 z$Qbq;X>PJ-7wU6aOKu$$YFk+Q=yi`5Zx(AFY7BIq4XnP&B7GClEh*%?g>3yi`$ksA z&+kgF=m-14(v7S19XmdM5qh2;&$%LFxIt3;FlDK-ZO~Z&)%v0&wLye|_PO^Z&YRBC zha(TBOxE>Z2~lu6O!xfK{&CnxCso&vQvN2UKK^DrUHPU^s(8=0KD8j^UwKTzBOJ)& zgI4@hMSarv1{f>+;zK-=ZtMhT3kS? zn@->r-2^4`0Xn;HRQwj_4o8-Qc3%6a@UB{(?4G$;qnrpDkV5(D9n?tQw9AW!SXT|u^@MXhLNmIdA9vX)AnlRS?1H5+ce!{AA=#HHlE(>5s}Fc-!P$nkh3Tf_HccSH2p^E7FkmP;>r(NC9u_w} z3-2x0@y`HVt}^U6GHmwjj?Tzo1}UT4;f7nOjbZ^ZUZnWa$_++l zdS<9By6Sqtw;*}fZ@P@b0oGCx*$H#D^3i2%b-W%9I~nxhD9Gc>Ud3WVPU|;A-M1Sm zx%AfERtjdXYP1!Ns{)hwK0n7g!;Z#|3~hlMpHmzP9*v7XRkX%Eg|DusRqhanbYWxQs}u7sN<^pTW&Qs z3bSe6!#PEV8)!#yt-7t?x1Z}d-`J5?zAa++9;_eHquWWWV3mQdNFr0d0Gmrch@b}gLc#kd8_iD%{U}0t+DC)=*-PYA#V9p~4IOGTpYI;m z#f)DfY3@C~40G*3)h+27i)tfu6*&}Rjln)onlF_bk1`i4ou{c>)NY6fvj0^tURTDB zp7+Dg#?W6wNPG9=w$QkA%vr(;Mc8A_fmepT`>iRF$Ar6aw3OIdgRJfJwSS&3=HWqe z;_zdO{@S>Uk81_8DnFZXHoHhl@7&<1HYF?}vsI-#_(INwiDu)k7yxx)MuSB)5rPTaDrnj0mhnWhIN}9eP~eSzWf)tydOLY=?cZ~$l^r=v z^DxTls?spRMN&-SwKdKNC4=7ve`)tC&V%>9*_N&`j3;VL`7=0jSCD9vcCx5{&KO{F z$zu%FUV7PZ5azTLUO5!~6xRh$8u3O(1_CrYKXu%bC-l~GHgDe+gS_L=kVSqL0F@w8 zZM`&3rxzDnOdzH}fQ_97(uFkDr`#WLLl|3*dT9VY@7eR`mgb)TdAF~x4-*r!IrL&) zrjPb^NyE&7DwZ*Hsa<4CJ8)#DBTnaYfI>>EwCm>;8T3@8k$iH?_<-u+bYkjzXybGw zY(d&BoKP!N0g_Drz~J3n%2~v$ZLf}0%z;g^t;O0>54ebOR#djacS9EEqpJ@xg6L#9 zsoE!t1)ez37VqA8g?#>jyp9Rxu&F0^k8miREFE7_!TV+ea3r)#+|r$wO!(|Uq65U)fG^uieA4NL*6q|!F)C@}8+vX#T7R-! z*}cbvpTooa#;vv++2LZI*J&c^Yp-K->#F%xt%HylTNBu_hN1|txIBi?JFW3yy_(?s zBvTRq{b69pt!tZ{j-Bh0V)!LTt!KJ3w7*@3K7sP$HTGPyI~oB&&U>J8?sU^|>I;^>uz@%MUo5#vll zo0ra)k;sh7Dig04Q&HC=Uk3$Kz9S>85naJfu4mA4Ynxg4XU_V5OcaT}@zwb{YCN#X zP&73EvVlTWbj~#KMf5XRm;1N~*qB)Sp;ryd$D4nO9^k7pGqBq7<*${HC8;Q?e zf9RAHm|IjNqR5v0bz57%%)2PJv`76fDjP`xwyebU*D& zxfB)I01%29V<_?@quXm`dxP)(SI7A(3DLc3E-qC@71{je7?OTD!S^7^a>j*~bO$c_ z!fu;Gpkm?s2~;ijnwXL@H6^8R%67K$-C#$bev^Cglx1&E;@fXtEm$*^d(n_T`l z9Na)@F12F$pSOvAq|_UE1Ie;26V!V~p$?i`#2uF3zV_E}cYF?uo^15*?FV}j1QQ~z zQgYlw0(D2;9$s^&)X>VGII)Y79b6RP3UoEM9?bgHwF4t5c#k~nEXZxOy8o+7{!QC@ zVnaz?cpRJO$cz3rOWDF7Ns03lxt3%aKB@CG!Mb_2({4Tm9=skN;10XlbDKv~-VpD7 z?Fw_@d8C?wQB5=r_6GJMUL}tK%Q@st6>i-gbY-yMO-5C>i5|XGO!^w$bHFXhoTSR8 zjJG}eUb%Ql&lOT4Jj8Y>*TbJbi*>X~Y;AZKk8|_*D;LvqQ$yx%-7nBDR67iH`Z$qC(Z?~>O!iT{B@Hj<`4IlO$^^r?ic?0`$#%A0)>ib!01?n z=>hrgZA|f9-LKqyZ=M-b>}|lrj)E{-4`01y^7VVPId`sXTpK@$xQL?1z9|c5l=8o< ztY(z*+)H(%y)*~+_h9yr+xMxd4Ip-UDGmpOpon+|*?3YDt;p)-;PP^w43O?!-P%e? zO5%4&zwtU?LQS*+KAw}<9y5fPC+aXa3HV(=>;f`4A5gMkT)O2gYw?nG;GckV2V{Wn zw^d}{RGEdPmIA*4J`7MRnn1`5CdqUNgkNrfiScnnEg{euPENoY{Mg&tddv9kn4$ov zqk`ZCf>bsNC%W$?&`AP7%fVG`A|uM$+FvKn7NBJyhM|BORNi*y`{1>f*2#3+9oasK zz#))h?|?9&q9nh*0H&{?mjfZ>01*?ts^0r(wHd^M!)Y83wV!3+y6u4$dA%RL9&MHf zxCN5{BNQ)&kY|sMK+Z|gwhic=G}P3PNLqzWaSEK;3n&A81>se3UP0RDSaoZRp;gc@ zbRBrO2ZuI8NJ+cT@I1!?LFD`0d@xNgb(W+TPYpjB{U54f>~fA+aIE1oMe(|4tR`ZU z2Bn+y-gA|&wuaJ9IxrbbVt!g%&&m*;Ai_~DE@HSkeD=!pRH7g4ba^f1)xu#>I-wys zba&vjZRJg>aot|7CAjc9Ylebx0xDeJnWyX{r9;NqqkNST+#I2oiX<7ROov0{Zc29C zj~9jYD7-FGTZiU7bB-_bumyX>PDuXN&Cg@DJ#r`y*_4-U{a(BO7u8sSUhJ!y-k*)U zkwv%?iB-G*{HymT*GmbxF*y$?9zMSa#BposJ-^k{g&o1b9qrcozL2k@{Bs@7AGx5Pte~ zB{kpD_t~c0(?wBCcG5ZxC{J}%)B563c0e;{028MvIEEiE+?9OIC#uH~72(Pbh(ge?-=Fmzw?Ma04S`L9HYZF0X0rd(Si07J)0 zUWRXq9x5`L=TtxlbWecNNOrxv&90b}R~iRrYyo4GPx_7>1f4FYK-3Kcl?HBjuazW} z^ND`p+QMKF44XTQ)K5d{uXuy5)JS3AVGu@n&Z-5%B@)Y0goyb3Y}Lu;XD5XAn>AjXlRJx{<3fE0YDTHMk5U9Afev%MWaZ%0J|ghQk5jR zeImG5u5MNpDLZl2*drGz-Kp;pm*s=(6URn0m_`mrF+`6MszwS2-9UMUVk4?0HvZP$UG zJIOQpWb?DO0Zop}T>%e8UGEDFCokX9#A<a-n^}Q)8vQOYRlllEt3)>Q@KZ;V!`A`|HdbRl8H-# zF36vuIJa`OZ~ELn$D>{*YaZ@Q&NkY;3_kqzuBo>_ByaS6#=Z;*3YrO8WyI$s4rpQ@ zF61;f4DI5^PB4tNjo65!Ip`Qq)rYg~A3qo5sh_hqlwRb!eH<2h_}SXXG}{c5tn}BD zo6!7~kwJ{61zs5yF3beNeZymEA+lP9u#i#N#XMqYK7)ZLO{e9{Vr7`jJP-!E!yen% zuuBX#Ha2dH3Pv!5aN~8s{cj>4xQj9=<+xoo2l1IA6ypvA(fH@UK(OQE3ToK497_gU zWUJ(LEuZp=2cJqk8Nq``>8&Z@KNYOstojTaorc3IrNKlEQB0xY%~E-j^CE?}Uj0`% z_w~d~aJ~mfv?^*beb7h+voSD^d~d+hcKS;VwwTKn7cZQb8*w|Sr4Dm-qiuKZ)LXt; zx>#9s;SP=$V{TYS_9Z(fI0)%VZo1n1d?izYTPWGr0#+r1Ug!EJS4AZLTUcipC>-ij zjELcBio~x=24|2HVJ%r*XaLc3zW4;&L};6-L*`$14%&XrSJ?`RL|54-s+vg^$6ln9 zDcn!8s0#4e9ZPJ+>v=Qy*1jFDMFpxk)SRyCQ0#NbQWxNEH>-wwF%C)>%M+;z)bleA zELh=P4{J5RA5kP-Olo5+h9a&*RsEC2T5XrBxZC*dLF3}#R7KjHS4`Vz_C4iwZP4)W z@X$~+_lchAKPC=6eSKbD^K68Dnw!p3mrOE9$v3h*psv7olyaT6Zks&jDAy2H<*eCl zC>iL%juw~;&=`>NMV17qPQu*6czRiiP>rVs{^1c3ihj;+geB&HntnL8@+h}@Nt6)s zpa|_F=^R=3D8h{0rKt<1_@%|gMCP4(#3$Gej4y~;z5~4Ew$Kra5bC4H7L+JAVB}Bl zJ*$+Dox^yDR7FBXH9$tN%q)0CLp=eny5TO%ge;=?5KTdMyjAzk1w6_%-~%`-~M;n>K;5NXQS0gbW}Y& zXZE69n>jsS_Qw?cx{nY+!$@4bOSbL!Y%v!UaS>}FyK|*yf5KmtFd)y zmVS6@(%jJj-NM!!_j2C_$2Svw7&~vsN#xU%@#K-K$AOHG4c~{Tu6?D&++_OmCyFiW z-TEb%=MbS-_S05$$%}h)+yJ0X<53T7Q;pLOMhK-bAhsqU{lUeLYW@b`WzHp;pC{_I z^7YFCK2xD+cJW~|p}SO+mXv(rmdyVk(s0g^oONbrJT(3^lPmimhDH3Q?qwD-RzH*d zmu~h@G}ss_#m9savQ)mIiHrwmO41E`A_&DFLczX`*_HC{IM@e;4_m%(gTEqYv9Q^DmECq-=Cif zPe!Xk4nTnkft?KC__rjNuLzB_qPXY&`lBQ^FN)NKnD!T4>OUmF zo~f&YQ|N_esZDfVem_;#`M^Mbf6=#Z_HW;IP()NKBFavW0c(^mt`C;;pzzB*%T${jhtw}Ba0=^@v< zd>^wwR}hxA3+clQq0=5PFTd!aFs21~nUps8LpQ9>X)xbT0Ip$Rcfif{06zGQUd)vAa=l_Z28WfUa7Y=3vB|JHV!x&)CcgW#5i$15 zndNfj8O6wt0D*}2aKH?4CRsEJ5d@b_z>h;14L~UadqM^=Z9l6SozoLSRX{f18?l4Cqb%UdFE z<5GcPxDHz1-N(|*;WQ5(GALl9A|}RI5ox`&yaA0ZPuYHrj|V$SNNgBwCL<0~;WDQV z^^3i<SWw-CMSvuR^Qb=c66NajpNqFYT9 z2%3+_yKUOf7tUW-y(zQFQ|)uZ(36vE2ZXy|ozMylw;_?!A;Hl|mBqSW47b}V&ZG=} zd;B%zk@{E)^fC?L-FxvsUFwVt@erRzTfV^A|1-?ByqwMHNWqG_pwqMVSb>*(#tMQB-cM2HQvXto}C$N&9+-w+i7a-LzR{M~+BN7nkq4$w77uMY^VeYhh; z`#7k@?3`_~7>SODj~`}nW-dc1^19_?fB}EQG<>8g z;CnUyYD=VNJw6@>;llCRucuMtQTDSxHy$j51bsMwWarz^7}eOLvqWk}Esd=mxRv9! z=OuM%xb8+6m;mE2U=VecT~jNVpaQ7zBLRW;L7_0z`o7UoLX?9!yNWQMeqH%@CNWV_ zX(A4D-bFrUD|qT$55vhRk1v%KgR}F~KS-SyvROoHN|1hL#4 zLcFO&&Em4Y%^xX>{~4r$KMfL9lb?q5n2>!d!2XxC$Jk5Zn*5TfR$X?ACFyDE$FE`F zzdMhP5m1pW;8Sh%T_$RNo&uH}gn&IFc%GI+iX(pah`f}Hj?@(ge3Jzx@7n;<2O))* z{(U~U_VI;IW{mgd1W#<-LSOG}{Zp41c`{nBg*vz)ATHT__7Z%bUcxTEV!V5A-(kTI$?53} zd{Ig~-yd1{NRRCt94xoqP1%AbL#j>-E>2ExJ6slBu&Ndo5jp491%<@Rc(IJlYO{xm z{~NXTQh=&kRa;vGuI3ZB{yESe5->rcS>}2>ANWeZWea$NC4mdg#~bje)q}1lz()dz z8=(=P6tv9f2lY6B=y)!AO#hv&#taT&e2{_t^5lI4BJcq;>_aCdD^g}-#K()I8 z60QsOBX;&wDW79NyGuMy2s~wpRHm14T517oLklE|MNpbb!?S{49ynZnn{nR`e;~sA zU-zfkBU6F2!^aGxH#c!nHhl>U)&y)1L`A191C9;}+Y9(Qo9xx%L7=X_eoQ-vl7djT12`bgta6 zfIRK`x&<$`?^gHJTOo zdr<3jlRL+<{Gf_oZN_@uso7M|`h)WXb0>H_oq%Z*_{+kL)%<-D zjB5Zw{%U?pM9*3S6+sAW?LQO;czfPlB%*jpbn9bqazc23U6g8G(KDzEIb-R-fUVY# zp9sr-5{Y?@!enN0#bympKGIhBa~0LT)pM|tT_4@42QKHEVJ*8X3G^M5xiMlk!y)Gp z4l6hyjkBWJjv*hB@EM5@p9k1_LGC?&K052+I`bofQ{F1Vt+5^xmzbZ(ZbK%)W!b2E z_A{08#oEVN58u(a{h8#L2_MVmqddX~M?KJA%!Aw<+XwP(A!nK|Uxtu^=1Q=dup0G% zy_qzmDu3KYY6ETg0_Z!aR;r4MsP94Rp9;WVTr%0LfFJg7rpo*Sn|e6?XK^=Hl-=E3 zuqT^fgT_>;JD@g=(3K=}>E_Mj>tc$B3PO!|_lUe@DwW6CS}#{)d}zI8#Jr}<4OK}N zZ3zZCC~g8h^F3P+Y3BIoe={8 zayfjd^BMC7VSBugn|LcvkT|!NkNmt!i;0iwkOO+~l(}%4fy^yL90=%kieR-;!TG@Y z+?5__=gX`&=3hZZMkL81XMZARriDk_%>8WqmJ0%*s$Xtf=)zMGo^5dL%t}v>POy1! z8A%i2iF?(J0cu4%5Qono;87eo=fLqeT{b!SXAQM<$Jp547A~Mc^zx>aTTboI(dxUx{_>c zn6;raOsruakLZF=mF9a@F6(XPc`QStd`)H9{y03;DJ`p>nECCv$FD!m2KzyLoiB^{ zqM4ID$arNDLaCcW1K70q^8HhcY!VXGoai_Z8SGFJiN0;d&YyDiqkk8oP!T5dAIk2L zS1JvdSf=sif{$=#qPBVRjhjIuvSS(JCPEi?c6TOf1;Kvcp63^K7aCt28XdiGReCHp zO_eo@NHCqQI5q9C26|k6+UC!{h;=U0iJ|9v30;!Ap!@Oq)EMy3dJzCIgzm47xsQ21 zUKmmEIDMP8D=k=CF%u4asmg_EeOFTzfniP8;88|lLYCh??xoIS5_s_NeG^#vb2xLa z$0F{)b`g<}Fp#&={-M2f+DuLQr4|@?@_AWKWX+(+(I6>Kk8#F1rpGfZI*dR*hZ6aM zZWxuCW{K3SvsRHdG~f4V2E`S97=wUvWcBEW=;!KTnmX@v#&Egj)kZmU^ZW~vq%JTd zp!t3esCcVP{Zi-SO9;OtOdhFz><3al{W8M4%kagGpznexk4suQGw54!ad6J6tp-{g zjxV7j4D8HW{%&cC%T3S8rvkU1T~hq{aj^uA?(M?$ry6hXP6;(7JGPb{7#Jve#ZS9R zGU}Dc`jp!Wb(9yJ+D?H^wBRvq*q{N>P`NU4p5Jk)n?H)CJu>wDpi_E!_6os->7?sg zfgC3k(kY~$#xS;`A4A2Je>O_vbfGQw0aj=chdTnsFrJG?NN5d3`SIkEJJDz?yAs!A z&-nyRF4+)7jkfLb3S*2|r4qrqCs&-OPdFb(6ARJaj*lUKY;V5@s>!=a7eZceKYnx| zy_N^AY~r!3{{DY!r=7)R)LDG1oM$*`a}sb92t-ILNM1{f8A5u8j?5^pkA`v;Wp4Ha zPm?VLsE#W^Vc=^aI58v|oKqfn@-(g!7pYLnkA!m;&`mOCa9u_Zna;t%C9xG4#Tf~X zh<-m@CX#>XJunJ3?k-SsjVTGqo(X0qHvCql1f31R!NH+^!4>yu0I(H{M=0)s9p#=(iPdeA9*w3o89p1`{}gj3 z*lKfpfwg%-ei@EH+g-j~k;lGU?Do=uQ;@4OCt|*i0bx#?Pa{qQYr9A%aB%(8>5QYn9Wb~tzoi8ZX5}3T?dpNYBwL^j;~=PyV90rkp$HH`DK?aL4ne!K@48j> ztL4SDgTkc2;*)>QwB0tLvIkrAy2fKLC~K$y5MpUhN0fq`AURzTfl_(myxER)cDMfB7~vq z)e>vabGycHFISOX%Db5=%Ajwv0$Nb2C!j}ip!@mUhrZ#c6Q64n2yw^ZBUOWADvp$h z2qJt9JPM<=1nF|CmuLeN_1;lB4JAdj(QG;TL-wBKa z50LO#n3#~ieh||jCyTf0nsz;$w4cd&r@s5h&-o%r|DX`Q10EqDR4x1Nmg!(Z65+3& z^iyK#THt-9qJMJO?=27_5@q#{uIQ1JgZxb>T4xG6tDph|Q{CdS9`z5wG2y6is@7Bo z6DV${FujH;@SQdgtS4yaWPnkHY^Sv{jiS8N!(EiDyA_B)tN|<+qYa%ItK;EHhwH7-4>jd)0|1vdK=FfS4OVg`JvGn4o}ej{5vYWg`s{FySw(5{0=Sg@o#ckjrR zCy;Va?Z3RbvF!q<0ZE-Sr)Qc;)c;V%%qBsN%9x7#&Pmk-osIkC*np(Z@dJtNZITcY1DWu9U4$lSg_C3FCBB5PsYJ4Dn7ycVK;v~IQ_HH}AajH$x+ zD^4;#`5V*z+LS8zO)^r*5}?WMdU@HjJ<;w9))~JaSY!uRM1(GHCB6nfQ(y@Veb)>9 zzENH2?0qYiLPQoLw}f{sLbC}W-djkI!^(o@bdJ_-bWw)d>;LdQW5)C+B4Ss3?Z!7? zN-T4^0$r8U@Qwo4hi-~PptT8lYG4Zs3-iey4WVF&sUzP=9V3Fp!tC+)76d_*%VTBw z^?=XJ#m()o)*B}U=9Ty2LvG$9GiKT17%9F_f1?X|tRI4?Yj0z}l)Y!w(Hb^-mW^D* z+Rfl{z(xgf7n5<78M-4f-ZOh9nIx2b7FTpNIHKLYno*XUhDvbqNb{b%n1Ii=DxX{ac;i&p$Aqwm})x%hVqH-9`Np; zf{&wM1P6{TK5G|{s|IMH1LM$$d8A-K2%i1*t5b4eK~I6rZzV-B{U0T=NN=zCS$h{c zp8>TD8_pguq>D_7Pjdzsf0ej^$C5cOyQH{4P4+vs6@A9(FPR+u@}3;>e}YVny#R?5 zIa!)=A}LWLrXt0R&c4VjziQaw`1=|0dH!FHx{{$^qAJJ)7>I~qaJ~>48=k{2>o?5h z$6I)`<})i}zu5DO$e4(TP-L|-zN44SYkd!BT#e>pG#)?C=sA2&puw$5<Ynev+TPWM`E+BonhuRS%~{LmRU_TxN}KOgLPmOmYb`n_v%$G$dwlfw;Fi@5Cy2k5{ezP4~3!%5VA7) z;}vNv1#GQwBEm8fN85V)%Wc_&36E(hRe(&X6huU92kw5`#;!d*v@yOxz+P}DcWI07 z7)+FsnTgMaR`O2WqrVYs5_ce`L1<_pMyZN+yNA&1F8SmFuP3rQ7XZLo0p1fs;TNPo z6b7~%G){uFdvI5B)^s6ICMZYu0SCwn#K3&}6+#r0OSuLS!5$HQexkyhk<8&Lx6x5b z6ygA|Isy{ef_{W)usx<1}0|y&aAr?;Pe>sAqFB< z2~an|ih;wz(_-r8xb?K3rdKw5A4?eN_=>3*Xmbg%QOinVCWh$Skq%F71Snx?ra4PpM}1eaFeeg)wks(SOIu+dto z12doz?>58_skGath20JBp^`DYtbWOiz@dG5Qm1qZOx=Phy|*&GQq9S!WGtC6KT!)I z4KeUid2i=NATDoiaYrEMMy+h>!c!1_)WOA9kdg!hn5qV zub&-OXW#B2?k=lQY>e=KlIbzyCs>tsOMy2I1)~(+kEyZ)&h_Y#O9)ZUN=nN^sEM=` z;lb+>eFt7-p_=gt34|>&GWvLu)f)z$vzCdwkoh5g3!>T%GU^O|DHXYsb{SIPbbJfJ zf8NdLQ*c?fDx~<@C+(`&WLPnsKiq(a!KzG?Z|&ea0Dfctl#Aa{p6e->CCHqM0jcbX zqMELK19*SESt1&!yMJy1>F?HX8$#iMyc1S_#fY)k_>h;Ekbo&xz@?nP+r9s9G#OVS z@(q`0fKl|??LjLQL?z9)5X=*HE&`<9m+b$m1-ylLpd+_WXS#iDZq|pr%f#0p0Kv#k z@cd)%es`j4Dj22J+7rV(zF1u_a(BLJY?s@}iN4kenFT_%`U2XME_4 zEUR(JO}$VW%PeH{pWky;XhTj5uA|>P6v@{KFxo0HO)Cw&Ob}KYFXTou+1b1-*L>?|S}#Fq-!BQf zO(7BXZ#s7ReuYnq$98HA4X@MZn%*G=r9InJum30X5Sg_04IQ>Gjp@0+v_&jts?a$+ zZG5xQR6(+87NG*MN}01_dop?s;s1IxdG2_#bY`DnDx%-|y7@@ek^Y_%R9#J+mEQUX zaH+pa`SfZiBWq(V|HOS?RPrJm4b#f1pclj6=_7+{`%J<=O`N&r?dY{;gS9t6IR^Jg@TPZ1b=8 zb>PI=v(&)$eR^8bOHYJ#W)X z6%;~Q2P7E zKdY9PuMJ+tZ`NBJNu_CrZ`ON{F9h5Wo}H!=A=kxf+XlUKj(2bp(lFGI_}h8qmUy*} zjm^{{AJXm`zwR(ogAcpE+zQeyGpIAU+cWoJt2>y1^0F{LGL&LSK3rHfRtgR2G}E5r zrEl1+=KYX<9vwYSolrY_jmC?-zcW% zyT^@Vs`p7Vc4^xj%0loIz*l%uuKTa1 zQ(x$KBpq}-)Ou-ytk7ySLr2Hxs-D8A_D$Kuch;#XQTv4!IwC-W@vk+-{RsQjgA0L` zdCAngF0*#yAg5ywK7_)dSnEuA)ZI+Vu`~*bij%fe9bGElbZBJgho$2^Q`4n#`|NuO z_PztogrXwU>tHzid3Si;-#83barh6>yPN%Z!?{W!to_uQ8oHyI`5}Ab?B|CmHIv?StuAw8`;KZPHT92bT|6Sg24DcZ zTjS4~kD8{oi}SP5=)AkU+Mr9;Z}lu498d}VE_y-cH`FLh=VxbYt(Sx6=6q$SomZTn z_XjBWKM)pkOcOT$b6ruN7sS9o5%55ia72CV?Zm-*vO%li3|kTXyW=5T=k`zBdVCKY zoVtUT8eq-3U7d3^8=s%+?=9_kOdFBW)8m*!SeY{T#a=|7?_vf4>_Rhs?_AUMpFeZq z;k{`S)iU?4s4p&7AE!N&o+%Oj6B#)+-x>rp;9=;Lf8PZyyms-sXmr|pY4l-0CLrlW zX=Y;T;fY^Ro{SqG{0NR>Uva-b#fQYE{?q%eXF8rvwnLvNR{qXydhU0;v24wc;7NL( zk}2EtbbW-&47hU+UFmOpT5!Ldj9+(cE~$G)WO=)h+SA(l8JoLScIKs z4&f;7I2u@gLc)ZVutjnF0v@}g2kL;3?8jLW@CbP;q%5mv!%P0gJsP7)quRNI?xc9Z zyEn@umu?QLZvuTI+-PU?jLYX!jh$cO5p}zKFbYl$JwjKnJ+!EL`lVYYRES2ap)A|) z;3{dpS(d}iZ29;_rT}&Vr3vvW$l@P-e?{R!lAxu#9O>Yl`orT$z7VdNa%=7k3DTpM zd4QJNBLQfMh{(HxYGlHH6Nz1eSW=fNP8{rGOJUbLhO!p%S4dx8M!hVyfTA!T7IKkQ zVm`r-*Y^(ySivGAB|l63`&%b`!mo^*yUe9(T&L6!6RhO0j&4ZZ52E$V3)v*Tfw<|4 zaB98pHcL^R%#`o^$G)ka2h$dHaPc-%hFn=_4ya_8|NTr5o#3FWTc40X9-l_pYO?e# z?8LfD7jD(JR5tJRD_r-Hc*A9$X=F|9 zSFikpF2pz=2r$DR%a5yM2c%IlVA8~?h<)-}g(kGU8{}MEMaQ8hNQ%#kw1m>hx}7f*@Qhhvu-hEZ{ekSg4M3^GZgiysh>JWvK%DO z*~yNz`pp~y{0*Lc%8vpua>P^2wq*!;I$QtpxoeY3t^1KbbUZcS@Ni`DMw@dZeM&cW zzP2ZdN#5P*tJQN?g6xlJ=RHkvLyk0G%Uf*tn>FT^N@7yF4kSFKvJlH#Af+bR7m8=ErnMfZ4fuy)ZYGdL0phA0yxyp3>UdR@ouK`nVZdl zP5gpE%zyh(Q=6!q3`XfA8GfiO>v+&Qf6Q#@Lyzai%ATW*PqhL1`uyC$QAbK_`lvsV(F_4BQh~h^2%U@ro_{WQbchaY50?B)x~BDmT>Hw1 zV5T~y$&#|n6w32Z?=df$8tU3=9n<(Lg}(&3EC67k!AYt?&BLa^uK+(xATNEz^P%xU zJZFgKFIyqS2LElowc@~qrGWYkrRMLixv6X4JR92}#(-+(A9-uz!6ZlY1C_Sqhyt-B zZR-D@$7E6JI2oC>>!?GJ&8YtVJp>A!4O3N98w8bqfd0@uLgNU|UN6sg5&%Nt5DXWA zbN}>uq^e%zJ#IsOin48@n3u_<9De#U>BENWK!%)HgGom4tE0!+&7Wv0qO;0=tipt$ z0Bb0c56mmVkT(|Yr+rm0VcS_)SXMQ1JFa*Q{l4^Z?a<*%|E) z+)CZB_jYy~fvXRIu4b-aM|})!VD4#%0u1LL)6xbC9VGQNqO-y5a1V~e(Qqx?eC~FK zdeS%Zs}YU=IJSWCK_nBC7`yY`f}U*4S?^G+5I>&?tL@dVu+~J!T@J-bfz>g?A?Rw|Lhg=v6DRU%|^7~ zUDn)CFFC-Dx|be;zG?vanzcbE!~(^{_!y{?YM{~W$e>FNEv6?#>=dPkPq6nB1xsYB z#q)tZ8cGV9I9bH2FnA46@bdKlPh$+^ShE#?X8|>ESFQh?HjOJykQpMbz{C=kKUk!6 zDh2>0T1VzKHa4G7MTkK=$}SKnrLQ}H^JN9l_e_PT5qEhxLLpI6(3nU}^v^B%3W!HR zfqSX9J!`=JPIipvvfsExPl}ec1Wd|H;HLx0dIWyMk-oYjOzQ^B@f;Rxg5uzH^Nu$g zyg>sM9$;7kUh8XkW@cOZBRaQG?m}HGZfo# zG^7L(DX4-l{;lnlhCGW1l*=1FMt$ABAX{>BM6ur5}EtJJuR=bBZ;#EF1tZM z#vW@21-1YQ0xKP3cFumd1i?g`AMu`IzHb8*2X z#`Sc29N`@UbLwf^TBG;ZKr$w@L`T-q)~*3QQ)A=hf&wB+#|AL4M7(U!=CZv4jNTjl zzhv)@5>cMgw z15UK@>LIKv@na>&SX9InIgzO`3#o(~=#9(Lu&?7jf8pR*E{}nB!$i+LYF2!6D*?gDi{R0tN1-nOtw!`d6277UbW(O+ z0i-94y)CeCnw2bWgN8#efmiyQD~8p*SX^3aPQlzj)UxqdNhwpxhd6<4VaZae<=ssL z8vz6cqKtMpiZmWEeN;AlBS(Wje=_E3zJ$I# zYGt=+f}ojd?*D8N+Pb=4o}Qgaa$Cfn!G$&p!-~S-V4j%{P1Tx?xdndKcP( zSGR2q)~A=uNjYjix6lx7!9+&iJPpUlcde3cr*(i0O!(?u|F#>#oH25s;xJdU1LUCn z>7VZ=I%)rq>VJ&Slr~s7;`Y)w0IkfVi0B{iAVP~s%=4z>%SE)yPj>h`f!Qt$Oz`Hv z^p05~44QfQ`8_te1}^9ee3S`ef>NOjtLY?D=$S~|!@cETTUt5C~5}Lh?geG|Nxy)Loz@iUV8L2^s zb+lx>^EUq`<}ij2gKfhP}0U z3JB~TyQn{Jw_LyOV-hYLV`fsEz*z&67s_!K4zaMD(bT=w`{L_ii zJ%eP6nQ0QIjQYIauioE1Pmpi7p&i-?ldc6*I=uzs7H|F(3o+bJhN`Mq=i_7ufKNmg zm&A6WE#GT1x_0ZeY0!e9T%X*Q6X_^Q{Fp-TlsmUMu`969?wk~P$vf{16m-go#=PzJea(zuL73dL|!gwjlc)@{kui7~j zUIO@4YxRf}z-%wex3wMA2#vI(gzb&n){g8$LN~yR@!Es0V9Ks^X^Dd^p65Gqun5?s`u zo<;fZBd%Bkd?`n&02Bx|ffe)cB*sU21sKv%2@pe7H0nUm9hrFHh}t8BIU^$-tN`?O zHMhXe6gk6;Th79Uwq1InuG1iDDvNI#U%_1g;U{Q~@|Ng|G!6bQy2TvnHP3d?Xofh+ zqCu_s6Us~N?X}~4i0r7;Nh~GR@z-TC0t8>xeXOFV5rHG>qx(dCJ-c1Fzx0Df+I8uP zlGr+f?>QgpxZ-nF)pxXE1;K@yyK+K7srklSI12R=rtc6gm5OP^a<)MxdDM?_y$rEZ zr7~08`>7}fHqf>ZRcPXh)=s-vy0D!Ns97%vIg!fNU`cFtzUpoYkw?sn5z}+eG?i1Z z9rH@DTQ5eM<&`ILdFLX4lWyyPKSjJrwogEsKUgEgBUh_%!}P%soeA%WwLI4SA7GsZ ziL)#;*q`aWdE;`*eamfQ|05+A2GM>Wjq?6rR7}P$4c!?PJ8}1I2pZD`YEL*3HFfk& z|HU|#=F9Or1wl9RPy_j@iB9a_%=i6URGoq-GcFA{>kkE>xVc1}q&W4#X6akMWXA33 zeYcq+U(fW6e4?wz1hyHg@zgp^jqBTFR!{avZLfil);dOcA;JO?K?L1{zMIYhO!eNEfDLs5;&k`m#1mXjvSlY< zeo0#FPfU&E0uoVokWBl66JYTDZTT_tgbWbEKBS~vfIVMQ#m33$zEAcJIdg~Bdt8YE zF?oY40PWp{o*HKW(WJRaS@M%?5sb&!UpV|+e%bYkaeVPw6DnAogUWfyMRTf_2g&g%oc%2 z;n%O>nLC&Yw0UK#N70lam)?&$|1Y2PcW}7>YxdRt-jb3Ma1{9V0z+i~P%GTK_ z>`DE&iak8}L9XQL`u!q|>T0MQQLs7f%~!8E>J0ZL>CwU{c$XO;PcTjb&BV2%9-&F; zGY~inlMPP=^wv?Nm%yIq4G*UC&-jD%hP0gV<=R?=A1o%Trr=W>Lu67Anxrg}wa%VWyKaUh+7%LF?=Sm#w z2?A_V?Sz_{tQ;KJ7+Begmd@HbI(b0`1_mtwH=f&acM>Em6sphNXH7LCq1=t;b-+lx z4^Mr@DkDIWITvtq4l~&1S5MJ?MurL!0igkj#7%JaU1}UgJe?#)6&i1R{j1mce9e$Z@|Xx}V7OE;R~Ce3 zvu1^`;o7{h5j;CP19i1WHJs8lD#iU=gkGW3`WiK75BH>Qcys>Edrl-M$q?RnWL|*sf5tRn9l@ z)u(XlhdYs%Sf+$o~CalAe~5qmp#Y zX)}fvFNC-CGUPyR5t#8~3Q-9Wn8GbZ_Ns(C9v(EZ?&+qPwjf!z*on1?eZ-Qj(+hHn zzP`RWeJfNF2|3XFiMcsWUzsh`OC?}gb8b5+_6=6gUJHpa-KIM|9^G)LakH5D{ne}Y z20tvXD`1PY8jD`Eeaz9$t{Ifx=Az18t&^aQqzS5INMJp60(BK2$}R(tdlodL)1ZCi znw22i6TC8rHeC3{Bpk5gDnDdp#TQZ<&>nw{tP6I(#)y7`UmkG0yYJGjc<9lt+iO(~ zmLE4r;7A-Yp#dY23p+5P{UVJQZB=nj&At7^WBBuh>^98l&FVZT!6oeQg9B$&h&2`1 zeef8xI9Ztqij|0yGwr3{td7N*d zJm+0q-`2A~qUiR9OUXU543!mzOT_Lp{o>1iKioHO!UH&Xc@yH}JxA(#%v{Pb{X@N? zF}ciouUQo$5U)h1-A?aKVbteUD8)L%cUVaW1J{3E=gxyXQ0wAfenD0VyK zbNJ6nEjSyyReILD1X!vV`KsgOgGBzIW1BNIB_)Mn3ad zB6;gX>xMAffynEUf0hkMD9Hn?s4A7hB;lPfxw6N?zy4B0YnLU~P=z)TFfDkcei|U6 z{TUtiJkc%rv5V2}fSI`!mn4I?Van35CVzpKiWtjS6;Hr{ioM_c3x>|tGx*1UR7tGP zG*c>b6^J5ci+qv1m@7*;h7yX<;g7xehcx6vA(kK8gtpPwX|JsR^D4Tx-=Q(_f2zdx zTts)$;QwjGcBHFj@~oC-me0h}d|sS-_X_dJ!wP9gq%0UPt9qTTKVNHGTTu}offX_~ zK07rvRW|V!1%A7b_2ocB^&xSb^54i&$fAW1;P&-zSfB1Yi0E4v8hz0zdBvGR`s^$H z%I+@oHjCcPPQO{36CazvoxU_;LVsSuWEKtMItrG>d}8nJ>G^+nd&{7_zCLU7k9%-} zySux)I|O%k4?%;wySuwX2<`-T3+@u!Z4P<*>7IG#?V6sNs;SDSK*A}`x%b)ox30BT z+5B;?pzpcy8{`)&^R?s^8}^n)R}H%4+9`v!pE*4KGfj=n8RN-Qv4jU64H}i>Rkp2y ziU!|>x9E@qdO}yaqm|1zy&1sKgsNjL=dAQ(19}S8Y0$%!?yJAZw7hooOz$9Y@0297 zy1?>Fhn?~Ym|=x-Sukg)!lL6O!DqBB8zNnNM*n{PDyU$2iZq&G(-42UQF&)4l-Z(A zZRp2+K!BybY*S&0{?ZsP#ne&8;Q}3uPt^YN2IboGn?iE^pM%Sh4-W7!TsULR+?KIQunWX<@U!oXuw} z4X9mH$tze2MSAUq8T}y2(GgMEOl-3%u%&&jI4wkYh20HI&QEA)aNJm~*3BC3SShrb zc$k~#RB>xR{nLLycGk>Wkee3^XW6ZH;uN)R&+JKVE?p)7r2tq*RF+g>Hmrt05azX$ z#IyEe!H}LHeZ9R)jKnF-1nAZ*ustitCwr^l+IL|j5)s!PV;IBFSY;~3#U?954bd#L z>USeTG9^NaqhuHru9u?AfwdrEy%-&eiX>yw_2)H{O~+9k$LP^FbP=82{P8M=<2B7i zqLKq)wu}23*ekZoX|DraZB+GPN_Sx54HpB3G^)g%FZ@;`dn!fLZG}JPo4F=oxn69S z|2+!`qJn0(DIt@bC9y1o{JUb13>tuMZ50fGfxUj7%+dW;zJD?Ovp-N{$?mdlS_;OI zx(HN#egV%@0WEnfi*5dq)`GW976S)0%-e$TFn<8ftS?df{gy-0VR^fa7aP9iqqag` z*V!2-dLr9W0prg?W}0Rk^46g$9%mw4TmXDG6OX12?7Yh(nM_<1LdMt4e5|99Y~qyU zEGru;%}#gxK&)-;j)^VCR%M=@vbm+F=bkZ>l5%TAeE#I*%)^}m*9>64Ig**0o`^(} zFrp|!DM>48=q{-yhu42*`k6s2&+U}of%HC5Bgf7!Eex8`MhQnE2A$SbG`QH-&Jy7h zY-M@1aY1}F{xd8pSei;>XSL8l!F4_qjy9crwROi{c=yzI`!3MQj^C{o>2_v=ElKZN z`^}@=*S%#wxDq_T5I6%}eC3Jr^gzg#6i$>VCWd;8S-8fcD} z>9#F{@cV?tIof18<_|)$x$k&!{TXidI`LD^h#)+zTWZR8242!PY$%54@?p*e3!@o< zV-`m@{dy6dRodVA+%=PVc?IKjjmGJli-To|KE@yiqd~!>+%hM61;nbYDpLq-t14<-2 zKh=RlMP?YFgt3xygC>88S&oz;**iRR2ohG(Pz^39DYqhm<`bi*s|c(H0&1Bu#yJN} zcaOP}pG+Jd!+MQ0;uuN4g_bBs7|T7%o2TfZ1?u}Wp=VF-rO@tt3-~0PhBS#dFd!;~ zy|Cn7f<)eleb zq@VB64SF@7zq)W|wUmP4{7hA`K!PB;>*x>E2k)?|k6g-K2wpHFsG?_KLZ~(nRn#zX zNX?n}BmDlgFN9@=TtQlP=q>nFo6TxGzVh3WfY;ShSa^aiq_>yo+DzH?oOIHvV8i^+ zYYR6^+aOu$fIUvSZz%cmO{}M5zx5Z_Ft-U>DXr~fEl9O~ukS#?QAbp%%Sev}ifKBd z)yI`Vzr=5fTO{5sY}Xhjjc=?UiI^^|NoR~B`|*UZe%?Aq+%>2Q>$B2pZcRkkIsB?5jv zF-5=8dD8DGxcDZK^q^sMRKYMK6{$~P8|{!gW8Ce+!A{4fkxw%~cPTOXORrT%?C#*y z;o+f`m0i$}6HjPiIDEoIGeu1wett#Hfg<#2C1f3jZ{d;kE^fyH`Yqqif^Biw&rkob z!yA@WVZgycw=2V6s#;}N^hrdYFJ)EDTlDt%52$ReET1OKwj`WqBbQFb!rtYi1%{sudiA1 zs2oj3UtG0=wX%_*woySf;h0F!kDq^b*;0+DDqn_Nv>PDRtayt&U)f?l;qj!k_cETS z`Qx48;8PACog+?^hD|Xr5iaXF+KJuXW8s{+@_T2AyVF&9(r5iyHFRdo zf{lXl#7hkyl&nz0_v) zB|p*qB@;VR=+L2@fEpKdoX&k7hm(FrJ__R3auzQbuaV>P(BCoZD%`4>vU*c>m?f8g{#wQYkVsYj^d|fC%>;80Hn7cKTfYE zhxj!TV=!zf>FQ)YANC1#DPUq#wrGq?$YbXVqf01Y6SDLM?CL^hlsq`3(UP=~irQ$f zqOEkk^qt(>_6=3;Mh2;v$&mfAuDVcV0^dCiyAzst@giXWh$} zo1NlcSE25o-ILTfJH+!0?g@h)a_fGyLjPS9j6)m#)Ct`gWOyH1xR|(0I&5B|ig^_h zJIii^a)r>3cb;aR21qb;xWga{sHwrxCC1~PXG%KDXxn=4jPAk7i9e&dCMmT3J1=Dha4_*sc~1v8vQena!vw z@um?Z!djs+#8i9_p@m8>}+E>yb0$aP{Ho7Ha6CSZk`i*FP9E&I)3L9z*@0M z@Mhm9;lsW0Ey1jCkd#b-&lp z!pXI*0vl%uIp>OBCH5_fbP3D`enn^1a|uk8+I}V^G?$AF;-Nh@D*7MK12`4(}P5>m5vtqGYM!j;He?`!0oG_e&Jbtnx@%HBs}^ z|FGxmCw*J87+_)|xo%{ZOatZPF@E%d4>)9eNU_7@S_8o4A zJkvH_P5gFXb-qVyrkBV+ZK2nkWFsS^Vp==fI`$L|{|vsa8u#V1O5cjHV9=%VKC6n1 z7lZN4!QT3hG%QIfu9Aa4NOeMznL}-SZ&p0CMBx%zSV2c zPVtJ7p?7xj>wNBUWX6QIEHnS9jEQ3`0KuQMY(q~=kB9){-^AwpwEK%*bMzg}>6?=> zul{!sl+yfqZsTn%dqg+4hNjZcU!1DJwmGV0 zZ`Qh4Tv*ob!}GvF?8(8Dt4JmnrH;vD6%V2EYJBxF-8{Z|&fj?yu|Ew)p(&-cv#{n? zbU+KIXQss6GW5%PM5mCkrf~|t(&AqEw%cF&U|a-2#)dwQM|W~(x9)DxzAD(H3{i2> z9Y%8Y6umHU*V6g6YvI_rKKD99@y?~;pjt8WaB?c&Dpz|p!t*frbdSI{XwW5CBpi*z zE-VY3z}^T~`v&YTo;@wNdid{Z`U4#-jYAX_jT;yYXXsSfR-G|%Xf2PCtX9Br{Rf6r zzQ@v3Heq>yi>_*$AV$L6xt(kg+-aP~wd`rh^gA{9dhX%WwZw7ZvRReQq|2xn2D! za@azEKjfqI*l%c~(GoRgV}Aa2g6ABdJAmq{oKe6IfQOTl&{bI2*!Wkn{T(4GEoBd*oAcQ~8))QP%D)BbApEV!L19KacK*ulYxYdK__$*-iM zx5`4Iy90e$-0UfC7p_$6aD{u5kPB0XD8)`~}A&-Bx&71RsPRzwf}}^6JL#8?3t>;{4g2ggmg%rsM11SaF1rKWlN@X3H04 zWhUy)EA|x(1d7YbWc7~%9 zVwc1|wbu6SUvGe)l76UUG^=>c63oW-iP|UqWk8zE6uLR4UfVvkU|SEzqaS6Wrf!du zg-cRp+u-dp?2lRiMuu8&mR4#Vax!_m8ra0;c{wNd?^(dzjUkL4vnKxWF95QKZfa@* zX2R8_rJDp4vZ=L31F(R%pS^I&SCYdP76t}u6B8A>4KOhS#&i3(mq%AuS0qWG75CJp zw7U8YP@vk(V><3paB*J%$;s{Zc;1He;kcqkKAX1s*Z)NnRyR1}V%SWLKrZO|YQ3ftDNO;W36C zwem|Xc7Fvyo5H-f%{yZP)onwT7!ajrQT>-4R(=52(=5mJ&+rzV!GklF ziS}&R%^IP#icZeH^Ff0tmc(;1u+G||pgDy#w8Jegn4aQIju3#lW2r@hW&LI^$}B_M z(2E|DNa)Vyd%1b`>o{7aK>G7a8G7>xtLc)cK;iB3Kl|MreBC}>h}*GZL(v}q3fH>t zySD{h=_dgJfz-g-1}oqw*7fRk&LEvhhvW`OrdL1#A0|56lx2&jhX)$$^Yi@U}8cnc&SH&b?;fYU%L{xMyzkMoZM8cTS7x-#zqkSv;sNS;`3fLn?CG*+t;&zG-x9X*u`N#v>)t0Iuro>%5Y!?pR4sD@A1 z7iX)E0$(y?z}q>0;3_z4xC4t6>H^u23+uyi@szBJjXdQQ0e9^A2aJxN{|%tQ_7m)f z`uhVzLVO?hQcC2r{M9}cR+d;#O*ckBUg%sWBBecsbSS_0JrZul)!`EuD@4O5FtI<#1md` zgD`!{$wbes(H(sjIJX3K(FSs_e5m}4C3aDkwReoJ@I$7=NkvNCn`tfs3jj<06HSaJ z1IV1$%Cd}mAL${b{AM`}xw8fjZhTvVz^`m5y+Sd7LO%0*t!AqO(H9|JVQ1%S0Q4j} z_y|UGcLB^=Sb?)wd%uYK7HaeviUXi7mY0`fr5G;>mk0@4>^HLG;_96Cq(d_Sku0|T zwW_LWe^y3{3#<-GGlnat9n_iUOfA((OmZ8Ldkqfq?o~)ZYI55%7JC+KV_% z2#1UyEPn&Z!_6IQYl6RmLXEwB4M31$Y99q{NXZ{*AApbK#zeq6UluuD$ppy2;(ZfV1@)L?uUFJ8P)G}nC~5sW}wf` z?qKRfa}d%?%F4pd`+R{Ss2dmJxnDrZykR=ikPTD2+ZhUCk9;6Q2;LzLB{!z#T0ubS za=mE;CLJs52X(QF|Ct^fAf=6bM{whaatLXWduHe7t!@F`&)w_%qvF@(O6Dh!p5m>q z-{17nO7&-eh&)sI97aBaT@JT9Eiusr`1pvNU&bs6qdHk41R&>Me(ie3yUPpt%%FFg zIm~nZv+o4_7SR#91bOnw@fL`_z~^!@0h*q@Ya-GEm4l2j5B`D0;0Q$qX9@>AT#-!v zuJJ!U<^867sE~*t55%@P!|3%Q>to4^6edoAM)8XpF$_f^7kUHy1^t?!c)hsc(Y`?N z3oj68?6&D5m&H9FK^PbuSe25H0QNp*`li6Yf-0!}^z`Jk*LTX2oWo=bg7R5{S6HLB zbPd4Vs((eb`cUxTZGy7^#bt^*k{{l7KljyEM`KypNO!cf5Tl0L=jQJnfx<1hzbDJ4*I_7{nr{Z_x{P^ zeQhy42=n~?3nIo|?&*Q6a`H8-6F)q+9uZEI@0H(+)Z_yRv~VDgSw0OXkR;cRYk_wm z*uo+pWC}oMFrbj}Wkw2m1?LNIX_@0VJhS=yfNH7c|Xx_jaH@U~8wRThx_n2wA3ozVcdnI{S{O>r16Z z9S}B1K#=1@_6JyIqLl@C``r7B@&}B zDgPuUCku~!m@jMRlvRE+{0c^* zZ$0b-0MHHhM+n2^b{V!E13bFjjDUR3K;Uth2LxzCjj^-}mCEWM@9)+b4>@4)Dgf9# zW$>UKvN5_Z+qIJPN~CauUaa)#C-~+AZ+w=X&XphY3+?L$x#HU_!arn5l&Dk3x;@7H zT@w)rT;WlZEK`-~^ID)_{4seIU4V7Z9f&9dDE8fx6S$EN6n9DAOF6B!wibxL6BUHk zpNip17U^y-ECiPz`h0)Ban8P%nu^Kk6RQ#X4Ke^EI=_Us`xe+B_!|Y59g?L(zi>l0 z6c!c&aR{E_EI@m~?imop_wbQZawG73+Tic+4L$ z(r%!f5ZELF|Kg*0af}6c!)kSUJ^uc>@d~^_x&Wl7prG$;w*pwl@INGG%(O%ZYqB0K zv-&0UbTNYiUmxGFF(UpvNK+EqO+o!fW`yw-DWrZ*B1XmwfP>cT@_7Xk`1^sdY5E_B z#QeR^3zeFHN(*r4=yzhb5XNV~-3oD{0ch6XOrQ*dRe2-Pa8%cIUM&<-tWcGDYf;GR zN`!tAD^E-SFnFu#|8D_>qdBK!1Hj?*3~aN}{}YwAwX5I~s+PrB{E z!z&-w0lNPy|9)@}CzAT1*T?T2tXl|G5m4IqGLdj#OEfprzx31}Gb z1>j`BZxm?+*~Q2QEPAcN7f(O)&Xt%%9kk**bApb7STKrxZMNG10PGM*Vc?-x&CKs33Y_xFM?2J**129`>#*M9~PDvLlG zm{sd_Jbh%`2v|9FQ>eHW1EylOv69_OP#w|C_45wFJH=zaIZ|g?4p`*%^CIAxWhME0 zWx$NN|DwQc^A0J;YXHI}39{6XmyD;?VtXD6Q)Q=nc6w@R3W&x}VT9b-*)d!oLIhGd zJKRDd1VZJ+Q~d;Z@pBYk0kZ+)koyD>;B~sz z){vLC1?=GTe!ADC+#&*sGi3D70u`WY4U@~@M!QN~+}+@2?jy_373P%3L^YG=!|cz} z0|M0dVJh$XbeeCy0SoSKEZBb5w2DCA`zNB0Vn1kq&dI4*C4WpqNd%9Of_5?)S{I5Z=m_m z<7*%b=wm)g?@{-9*KA0wLP8|U4oEs1Fmp1Rf}dBQ?3fpcWZFa!M(B;hab%-J$&Ks2 z@i!g`hNv*W5g`%N6`Y9s+a>x>2i7^<58&P+*%F z%u#9759WH0!V~_MV5=VgiVTeB-!1B)r~Q<_<(R|Xo(X>|7`HKoQ}K*RtE$Y?ohsA( z-h97^LB;9{`?!#gd;mgXjzqM6uDL378*4|s%B76tQ= zBG@y0tYLb2xk$Yt0Q9vZ;W_ErC4JZ6E%$nR4)&K8VfE61rpZ%O+2Tpv{E>?dW}FO`$DOS>kp|CIO@Hbbd6GX6L!xyfEEL5^6LzZ+*z#37 zxA&r>PNPc7aK;pCwSQ)bigFL7aK9$0 zGMgwRyXcLfvJS6mw+3;g1r#;8@Rw1*Ase+3ufK1J2{YE4B>*^LM+JWvZNb05c^q2uw|8yvrdCZv{zYO)j)*5;Nr=GKbRCumn<1nG2I^w9bW=pQuX8nWB|X5*t_ zp=XDMHQ4|X+;0(-CCshWy7P2~fgZ3gm0HpM?Q*0XHMzyre&^O~ z=_6@niX|B>=i*e=tu9>3JZ>*0-%QoSJ*2gyiE-pyZkN8KVh(5KBLxDf1BIk9kM=fA zKijHrlW9Ojz&fjn4%Y2-SmiiaL}-G6VP94K#7~EEyNSYeAj>7#7OI_CWS64~_B5kX zoyu}S1gSo*mwc&S0hdcFbJ4h2aD7rTi5*FEJA9L_XT&%$QtBXH(j<5Du_Q8n|^UFO+>7mB2K5Ey+pfJY>lZV+k_ zFH`-fb;}npAjgUl=auMFIE;G;{Xsfgr@ zR5fT9s(9|QDQVMqtjn)8r`(;=05uThdCFi)EaLN6FjdRjiGA#5{ebf~R!loG(YfT1r?Pe^jObUkSAq2H4 z(7#_8EjWZ$msKJX>ouu(Mk`C>p4_;^8E@+(T18 ze%JDPwH#PQ1Um^lK*JUl3iW1SGDH!sNv9=YFJ_fceeUs=k`5D z-^c_P$9&1gMy9i=ek6Dm`E+&FEEkMo;3m9f*I{IxxcDplZ(dF7RqvT&%&w93he7zd zuP4MoJ+zI-L3?R#du~5R?e>!9R(oFyK}r@sPnVrB)Hl1{Hxpq7^T=z^WSlW=5$G|= zLB1nNJL%WWTjemThQi0vW>LCYb|VGC5GfCfrK<#Bwru8jw&_$WmvQs(Ot(#2N&E@J z1{Q9O-1hd2nDXwJ14TJ%=Y?1Ggt8LUavO1sJDE}H+903rtTR>~CGjCsSJRexYnhY& zpP_7%)3ST7qf`_lH*>!~4VtMaAOdN;aMD3~O8*-+1r2i($X|JH5yeKu1Nl_}bw&`SITX04A-H@=~d35r58}rfTuwTCPf6u1jkCM zjib|<2>**;HO;-(Z7=bry!KOmRH)mJd9A6G(87O$xc6q^d0h5yMr{5$Y{V`MhCO#N z;UVU2+*U>Ks4j&$_i!patXmo~xswqQ5pfsA#1(eBeKJOl_Kl8iyYs=$worSE>WJdK#hU38YK@f7V+31_eRBV)D$E^R_}vGPCQq|0kZi8dWC+dW z>d!piFL1tXPJn95Wgxs6%GMUvYu0iP29~4QXa>`cDS-ZdaIM~RXtUM3Y7t-_*Y#hg zEG(Wr3FvBpZaZ8^gOWPsS$E!#ZA^*6A*7zisG@TlGq;sg@1 zVxIjb)+f;YW|{=xO2J3$Hy-4Qa)6XoWos)pUd2)@@5EnG`GVFQ2E+71BJWo%((?&_ zA}S)Pnpe%1G1bd^uV^o+c7>dD;1)d)4^LNeVHpTvSDE z2}*{!m`7)p%ig_yo0SsIoo+v}G--`1CaqZ2<$gCS`SQ%0lzssFW@zgjpk4rj2#2BI zXMOO{h4Y+SKKN%RC-jbW90t64Xr*v4D=%9)p*fz=TJ? z9lhqg>g1-+ZQi!pO$YULp_!F_B7JpzzV5`Lxkfw%H|qmv>3^Kamx)gKY|Gucv!J#? zS4}_8mG*UdEJ(V|=(W=pStt(+rp$))J81wDC(+otj!{71@dAsTlxItA%P=>c-f==DT1Qto`H%xhL(&;T9iTM$G|rfDd+QtGv%A4clFog}cll`+L7|C0 z-aNuIvkU!^C<@NtVZdH|)?A^F?!DT73yVZ(H>d*GyeNnOSL$dBp0+=SmOiI)+2kP<0{TqXo3`obA&TU&xLmgQLA z8ab&5`ZvADQ^uMHEk*wRtQ7Ia>0jiFB0LlFk_w<_aaUoGqZcC6@&rt~oVsuIeq;Ha zi~xWym>f@;Txz;-L-x}zX_a3c&4tJldT*uW4N^58Q>58zS#(^xnQ<#r3pY_6*Ih zIu$J~NN+$9C4|go1IPjdlt^S+gA(Pjb z^n?bdX|w8fCm&iib@2C0zG?p9<&B}qeT}0|z!=e!Uw&0IyWa!WuWxAT=%}_FSPEV- z9!@~2pLIUafWgv^68unClKX!~T!0)5VUib5U-4Y{EC|CEsfX=aHg*kezUd6M5;sb| z)(9A)nb3bm{FsHRPUVme2Dua0J)dczWNZQLOA>V}bU$8FFX_ynj7^aOW^RSRTqAR^ zFvGU8avGqfGN3ra9S9#Q3! zHH$)sGgKK0{9$X7EUvR?KV(KF;bL)mxykf7J6G_1=}=E|n4-%OWb`p3>pAW^%yML6 ztCli~Tt!)!_5@LfgN;k5N+|>qDo=?>2tPYMQ}N5j;R%bh{?_-$V2``G2ck!8?9*Vz zL?T?_({7?b{#`pF9;kZdc~7TXo=;!%L~@eympyW4WlV0iOi8#y;8=;?xDlRE_r#vB z5uZ=I8sdK|sHm>aG5TJ~b`s!YZd29PRbj230czn;-<}ghC(o~9y6?UZT?hn^M_vbV z>2cTz;H$Mi>h3DMwUH@Em`v>k;j=tUR+Lk73U3?n7K`7TIV_psatP`nA8R9+pRr0u zpeXKBgC5K7Hewg(iRi8(pTkQiw|wGP*gdzgL)2|fk&Gr7PnYwUD~w^Ws5}~T@OgK* zgf#;YqPQL#Qw}SVY11g3Z+y>SR&4Hyespxr%OZV7nc%~JlTB~|dIAGkCb|-~hw`>7 zA&3a{pqJTQM@9EBN79f8&Tv7Nfk4@6YYW!fRJBidN%|V`NT1Js2K&Xx!+lGR%LW~) z%*%rz!j6(YPBmjWICR{{Bwzg>0xo_Nme%KU7V~<<6=(KTHw)y|fA(^6gc7ljtE;N| zHYUGh3eMY>Wuac)Ui$glTG${%5WR5skayppo^#J^bQ+Hzw z&^D^71MhySd)+=nolY)P6`#v8@{`Jg#j~US)^X%!y3>Elx_sUI@qcGs2LHdXE=V}E zR3k0(jWDoO6&CfEHHbI7KZQ758xl;sZi=-Y&oGrfCpy8XntnwLu%oF&A5%}>cGpG1 zwij+yEihrL>Mg^-$ti;Ty*T{ONEkNJ+$07bX_Z}_(B`8xnN^Uey?=E>hTqw5;o8ZT zqny&Zphp=8*JB{aMB>HV&cc%mI3j+=JlK)A0!mbci9&W6(>aSqRo&gu}OGL!q;5)ZMP`%sh z0szBzo`RDqikUY35~a$@dICM0*x<# zwt^-33E4f=CBa#zni-?Ixw!@N;*wgp&rxgq`VsX4Z4f|s-ONtShGD+-SGNni(w^0t z<_sRLAV^32X-|6x-tP`xN5u+riHmn*l3(GcZlCyYd$3HuuP=FT_A8x&KSHR^sfkRF z%Qn=+pkQF8>pL^P!A72S+5BA%axi= zL92oqLD29Qw=K(ouAUs`a>J4lITzoo=6*R5>)x;1eg*2P#sn2{2vl8z;*RU4-_8m&kq@ujTO9l$!ZXQ6=Wep4Q(P z+i})-7l0dN-^Q7>KSl`sMG^r(VBrM!Mw4S0VUhRLxH)&7_Pk-6Y^p z5b|krJa^1pxt!e8{x$COpti}(L_`$#oNt7dI%c^bC?x~?;o-lCmi~ic;Wh{Mn%Z4Q zc!SOEZ>O7^-{)XpqN3=uJC_4u0Rzt;D39>W;jZ(TDl%uybpZv{fSKqSD~WSIoF?@Po8xd4^`vEJ2nfB zyV`TFr+bKQP(c1{1Z=>$!gy_(EykW#LvQkgDM>TEsYSfcOn;QAQVu9%JK)1PiHj@sKl8W7?@75ySiobsGY+(%1ZZ+CBQt(Te)W3AS zyZg%F9sJrFdU*3-QYGC$X?-_Qu#6PmIt75PF4Skcg*WBF*!XnhtVUYL1{0iq#GPinzNILApQe})^xE-i8==Ju}+Go zDf#6|dndwwyiQPC$Vl}T>mQ8>!AS?5r(~~Nm5hU5+IhQPSR_1>HI_cr5xSd3ky0L#u$IE*g9PzN%JMC=vFY+;U$|EI znB#&w5()9K7)2siHgshz+u@3(BU#lpX+vKp!B4s{!?HWgrhbC>D_?(n_h%Fm4gz8! z_+$rtDUp<^8M=ouva2|s;=J=Z&j7`M&OSB`Rr0v8Ap3W+Ij-yO+ypu(*jEKG@jOFNymG?DWlQlwFe za(NCSR-+-ch0LoKfxgG^I*h*z0$BPOsx!pGfWxY;~_S(XX%UzS!_ zEF3a$3(_-jkZ3YiBA#~*h*eB%s$)>6>fjFPb5s;?M{FfnVeq?~8_Fcxsl*Nj@j4I+l! zhjZ|kS*Z52a8 zGcui4x14A?zbSr)$(xB_Hq|2*`Cav^Zu3{*@L$d4ZrCC)Rt>NF`rI_ zc<;6tpiVQfA_;9QZ8z$8a8g2U!o$K-*^FU9;liciU}bih+Xk$Z zhW8>c#;)#pc7}f2H2;^g_YGv}>!yXSH&_b2%;$;A`*(IHA0bC2sc%+JM*IC}DKh6P zo>P;HP+uzTEmj)dYagWd{%?{?NUK(}MYS-|{eNrhwX?J20pj}FiqJi5=0^ukPiY9M zFZXRde;`2af?!-lS|%73knk17W_(dpS?s=V!XDopQ!SkxU;SEu_C@ChZj#IC!w8U) z8F`b~m?+Aget2A0!^jlNB}N3@{^LeU3jcaz$?BW4?g`x&@L))JXM~S?<|0(nFvFQ` z?$rt5lScP4@n!9^se}n2Z`vK}Ep=xaT^Uec!agP5$#aSz?Z8aqo7_Pq{983c9Q|AW@JtJXSo0f;A!@@w$BxS=ojg~)4p82&Ve=5rW zJNM3rY5fvryP_Xt0KL{v19sWATYcXE@D#f2Uj;o5G*lHX+U5KE?6^QEVK(hr6&Fhb zF-flyI@G5<;ax(oi#1?!BK%FyXGGb=xNje?)k55(zGS$_v;;dtE14l1%5Wbu!W!@D zufPWNiQszLGB#b$-XNg-W#&pnF(9Q{Cttmy9&rzUlHrON8e~jX2RoR~YUi-dzbM*9 za8@`G5(+|HZLv;C>T67HJYs3vVvhRHSH@a-clkUaw7{i%6jXAeoAME6qX4bzUkdn~ zjwZ`Dk3DaGroilK&kQ&Kk%+ZA z3g;dE`C}IS6;8rnxO8N07DkPo1n!y46n#lpKrq3AtwUu*#NzR=QPFO~yeA@n{gM%lk2M5ON8IIw;?|GMsd)>u~92OAm>cFY(+}u3uQwkg1&Os<00c~IhI4d^A?a_42csQ zx;?`ipqczX$|yhH_(&+AakMhL8-I-Tv$pyp3mL}s82WIx!@X`G5KKsB@KQ11J>%Tj z&zG{v)bAUm2FIUaS!R|d_J|gDB+{=C@h9b3%EfxCe*zWbVrAO<~4N5pQH06rnss;?^k@UmXnT(NG^|U#j&*fPvg$R;%>&uuk@pnmp zPo&Lw3wf~QDE3(m^3~T*F1GqD z(2(*nyBNq-)EdQ~mRmYuh7DDCQVjdi2L7P2MasoA_V>uJG;N5*Hq3D+XAxidT8yP5 zQ;-<=wA8odLqOZGeXf{>;2U!J#m;&*!Cq2l6MhTr8}Q=ZiOqXL=Q}yL*Vz(m-rGl~ z{kP{gkZaw<+S=aU(l%s!=BN|z%`XxVMvFJc77|W;?)cWdKFy)4# zf#yw)*{XAx7KEn|9Pf$D^6H?S7K=C8h4$74!UU-;k0NpkVw`z4*L4yOcDBh3KENw* z`a4HON1cm|#kN!$4-2_iks9>%H47E6W$Y(WCCtQ0T~3#k*mOUtR;Z2BRb}bwDu*?0 z9NDh$aC6xHuKJ2Oj%7K*9P$d}JOq@BqE~4&*Iub2;8^4S8t+p-D9GS71<2-4H+h{h zh+&lfBdufW_PKe>Slwx`m#U!%ALEsVR}CJ7%QN3QwxKH~XS{C4dqZ$3Kum2d1)D$C0mgSX7=!g7K!JaBWHK zyh4&OiTi3o9?N|xvWaKZjXuu3*ncQsZW9EFJ;LA(p2z|vNkB=VM!}nQvIL=h`VZn6 zsC1MU2*=X8k5|A$M}UclA?ZT%$MR1MMkd59)glGWdqx43eWdnp_2W4K+7<>Xev*5% zKkQ-X&ZZumiSX|s5I!AQ+YPiHYV}k^qJX4N#2+jshY|&WzDga8D6|iY%s{6uLa{>f2WPTA!LDM*q3r>7!PWn?BZxz{ zJp$z+CdGASdz)LvigG~L!B};~X^raD|6J%W)7whexXFmxo^i7HNyE?~NuSbFz4n#x zp!}D*B;RDG|4A`9MJe0y)Fz>50NTM8)d-C1ZIqYdA4+hDD`qr|_?Qeo#b7Da)7g;^ zS)>1r>l5zy|Hjf;fXIL|&83qIbGc|al|#v}lHYG8PLQo2y_|CS%MikPQj_uVRZ&#j$2ASlY*O7K zW8ej0)f{`jY-Q)02b|64JIu|aXz^8vE8|6nyaG%x>f_eSoxNf#>M?`Z?%ixZGCv~Y zoK0Uv12n;eV#1+4Ll+os;Oh=uUVmhh>|b60_Kd?%`4-&@4RB3C!X2m$_QxsFV{Azr zWWpQvl)Bc`};xJERgCBf4anvY%sDllclQ9xpBWRrhmwpUD&9Y z{@N*_wW{5Hc9ytxY;^2~T5^7P3bsAD{PP=&M#UXi&RBolLlmRpqjzh71cLkI+r!My!0p#%ahT3i{WSb244`Is*SP^GNY^TERpVdp`Z)y2 z<08EZ|37*Lw28IJ#W(T)>Ew)Sh^XWUu^m18DT2AVlSiC(S~=ajC!nf2x3n>a;w~^q z5Xu~CRMfyzjQZN6QsZZ>44z zIc16TqHWNXK$q?t%I)d7>6CqVGsJeW#7aL+n0$iv^6_d3INYCkwwToy9h{A?H|3IYfkBEt z5o{Y`a4V;Buet?n_4W*WtB2EuiTR*}G@@q3`=`N>!kMtvbQ@hfBf0TYh)H3C1#3<* zXmKg}U6=8rHn53}837g^b=uEPiw}I0;|!u=`5`g#3?*tdL9bA)`N>iad9{+ud21o0 zbDl?6ix;fZ%>n{{k>@tgv{(Ii{>za`EXwQEct~s_%RNanf|6pbv;v0G0AAX1!smiOhjh|@9zq!q<)a_I#Y5B^7jn=IXQK_ z9JU-!VpxOjqn9uVs_h=9u3By9vjs`%J%t6<`a3QRRAj(ayzsYUIAGZMUL$YQ|I%%u zUt&?INL&p=Be=F^M>zR(1ONCJ^{A?NL&~*bP)oyc5{sDGAd^a)&rV$Vv*~j6czsv% z&j4$m5Jnj*97pvX3wQfIerdnqHpECDz_Up`Bu7U52S>kvvT?gaEP)4@$C6>ltRk_} z-?aa|RgvA|P@u@ds8puQ`ETdjn^IZinNvXiWq^ktnfbKVEf?~`xSD@&_D*MXZ?p5)- zJzT{sQFNk=@gLu954ib!!?P8*E|URNpWl|sp>NPEGqGK9rfT~!wiuU@LO%YaqmAJ8Q%XwhFvgt}W~PXr3h;5rUh zF?^+92Ppni#~rJo2OoB)tg#E9CNI*b|LMiA5d)sf7NQMJ4b239nT8QrJXemg{wZ5) z+la)I;{}=_%FVV=P))1X0y=DqJzx9ppE2qEOdW0;lJEINz6DQZ7+X2kf}4150dXUh z1IJb~lj4WBz$eS!2mq|b)@5d3;=Bf^5*EI#tY4&0MWr^T{ViWdT$q%yjF3-JZ%@QAHl zMhxe_Jh2^$2SmvgFx&~&3*Io^qDmcB;ufSOcN3y=LQ!_C(lqFFmGcMnl|tLuU<|($ zboW3Wt=dJ7NX_AhH0n;&^3Q*>4+J(PQC&!n3V7-Ow(kXy z_@2kk!SUwaDz}1#+S(jqwBZ8dh>@rL^z#R@!CwM|ag3XWtMMVDmTQ?qx?v9J*kVeAJk=&gzw+$=1F0vW%(7KyIo6Mdm?cBKdnPu<3V}8 z`$9@6h{FZ@k70C9@~=0%#_%pP`=m3O_Hu<9ZD%>q(s zdjadk_MU;Dl{XL-@aDZ#MnfhR^a4=%K-fFbd5m%%y!VfmWKQ$N3*$pI;Q}~H9SZ5P zaSWXBrRojb{pDGaMMI-2NT4VOUi_~q@n@csgFY{SOM>p{>6GPT&rqXIm*I;QV50RK zYL7$_`wQ4_p8)XxE#?;B3tj8-e{5bf0*u(CgLrcs6oa_E{Oxob|2H8ej#tHD`{m86 zm{t^tO!WOA*BAJL7pruC1E6XgG9Y&^&B%w&18i%nU1M;+Y?YUxKz+IfM1j9WiSCbQ zMM?lZ;%9;LR&yX1P!P?p128rw4g>Z`5x_1=yfi(1b=i+XA>8K((0cp&KyL#!cX)00 zsBmU=VUKe7-^0E*^eQub{&d5E1bp=bVfZ&hHNA!G(Hi5JQoUn?q~K@}HQ&XbQ^D$| zE&uSSH?U`!Hf%Rilk5a6=4-k+m;7T=Rud_d*iCRZ@+E-QuhyxD5CDurK%bO1YMJH@3>`ZE5itcZDx)vXM!<0z`ep9UsF$O8#ehD=vdTRJGV;`=5s{!+8 z8_EMip1kfs)`5`AF>#<;Gb1gZKL$K;FF;CY#3BwJ-biPA7GNokrpetz+weJID(?ln zFVBUs%*V&adJPsM@+_S<0IB&h9LFe#dVmff#F7`{A$Bq#`UCwJ4$HBg34jvIVgz+$ z{s>ruM1@?!0S7)eY+tAEAMz(4T>$|WwiQUo1U@Yh84l1f27#I}x;~~m^PZN|_X#K1HI7r0R}_=f-N1z6;IxlQH@EpL)}kOIpJ!

41F^<&4%Cs2_o;_1RGX%I)nwe zkmfOqdIrwesZWs{m@*c~S8!ak1k=-BRkZk{_5Kyd>ZvwiLQ-V06u1#7fV@;(6iO

4aZCI(QqRn)_ zEw6pR2iIvnl+YiE($8c8)OzN4$8!Hdy1wW%gHx5tLWTYUo24hWHH8eOM!ABXM-fA1w&n!b>!L1n!AZQVIBz(?Gws~V(9Wo?VI*;Ob@vc|NtsE6m) z`6|<4R#!jIBV0#seF-$*aARI;V@TA0X`2bMQobT)tB;-!sCPC=s{cG(#CxC0NL=$C;4BEE-~mGADMcxQkD!K< zkPx(bxg(@vz6WJfyvF?_)Q8{_z3l+#+`TpJn@IE)b{bo?UwYEPJPoEjV}mWOod0dG zKwK)iUgH12wcT@#x%lv3GSABCneG(rVTs&7e}z<>*j*J9w!EutyH@qx z?fez0TLeEpFtYXConJAiJ8!k|W*xdhqHwyL=nAheE3UMs=Uv+qV*8L~qr1nkCb&ee z#6`B(;`lwlS6aF_V*4*M*FhJtlB4}eBCD)Peje^Q9s8t|l~tNvH8T9opBHqUUuwTW z&EGr^-ShzC(gH~?dAs5=6Rn}VZTqqv91@&M1yAMpLN!mmk71u7>wgm&d*YKBEbyB% ztp-h4E9T|G&5Cg}c`+Cmc|-lNGXl&3tEZhiKM{e{uG)T)*}o31XW!)IVzu%mue~gc zg@^Ym0uf9v^^+|Oe$_724=4b z+2t*J8cW&6If@_~!R*FV+b`=H02t*eyXzGP@cLiF{h~Fy zsd42zqdup0L85OAB2+VrKv_Y3hL!JK9322EveG=>p@{*srUFb@4Z2iUOC=;^4-%&iGpZIObcHHdVRn=amME9K|nKjW^5=YT5+)O;~Y( zfZYW)JTV;=9YRdZI@9r(h`}!*eX+99&UNeP+h$1^JA3SO10%gfWpY0&wV8}hzvFP` zN-zTK=00vt+_}w&W(Jedvt>_6IERh87^%>49mCbl`HGYC^ZsuPU*Jai*7e#L1?bsB z4$iD5u*Cq0(P@zWD>y&X!>Ai>dC7N^7CAHH^EQKvqP@A5_T(3&N-}A*U;CbFBDGdT zxcTAxh=_B&u8)5&M?wm>?XwmiI9mnisSYcTI_cFdr8jo`Kv5+8*I40 z7+DWSk(9)u>_z_dc2#jKR5TaYY%3hykIOHHK|;>Aa1yhOBFr5o6>g6-!&h|-5a$3WOgHFdH#>Q_b@g4OxvTd&A~pdeK`=~BS4HB+vs;!xxJkJ30Q zQzD1bPMbQKxY=_z;gGx>mo^Vhm7%mYofOayvo;D>l=c4Tr4yw^p)pSOhF3&KuX9o%NwY~+7XKcJeb-8L^GgRQ`c`PuM0 zLXnkrkPem(oQhtt6@tR=0NDO?{LK;2dsBO2$ArhU3Y7QLfuvFiiHLqONbL<4lB0F1k3w84>&Y zOYXQT$Sc3?7uARb3=dlw%;fp^KNXF z4TVQ=P2=j>J(T2?GaIM@T5M@o-b>vk)eiHf^@=?|wxgV`_*!2CoA>ue$YygvX>PULDGhi0B-3J0LiH%I0@r+g zE<;?LV6M{I|1kmv0~guLDU%NI?&*nd7!b^|uC_3g-{mR>hQeA8OZOKZBKH5bTC+3M zuRtM8pr{0KaW&V$6nvl{@3Z}CD?pgYK*}gnsOEIQ5SFE;W2!RR7G)Ah4s+elulQ5h zi;Rog*;?myqrOvfyxB4qZ^I-?$=ZN56Ny%yGhu|Bv)5z?q_`Z7V3ohd&7!+Fx9oDb z24@^hazIk)=(v*D2^Ld(xat$6Oz06RLy;XwT3s+EDMkRq3QQ&(&2g>+dgdD4Iy>8m zhwYW?(LsFn*`C@g9OvcQQ1sCCZ?nenrswZXh^0_=J(@YUD}ie;Az`8nz$Gg*>C-g;Lv-CKqSS2o_O*Lun)ewC{5K@C zXng+~PVR32G`QOm4^MY311DC}sdGjxDKUQk3-{}1f%fm(wL0`^^2#~F+M26%v<&`* zR*HPuI%f5HI850Wh|w{KX`{tr8+t;96pzZ@(;ZYzy=4Ad;WIaL7O8Tw)Jm^C zn=hd>H~sxf6AH>`0;kgPQq_q+oug}3J8BB*!^gOoF%1XW^@@yIt=}W{Lf;ew$e=_Y zB4)D+>SmXYcIN+TN$S?IH~3OHaOye4WQ!#;w4-3LzocJmV7NkB$SRdmn#QWXIcrR^=Qsi}U}aVU|s4y|oix!_>j z6$~qfTbhv=Vn81IDen2&9%Z6cXlY?m+|*Xh%VqJ>e|o!v$I#i?`wxs}qGPZcFVp{<9ZA2UipA_UER##nWN6 zVFDP+4m=$L+fy3-TJFOFn8|E^t@t)1?{ae&JwMiA=Ln za{yMm0$`Q;FnT{+KRbN?bc4xG=hQr_hK?mCsUuS-Ge9PWpPQKkdy?caniHO{mN2tt zON=Zd42A~bQBPu&CI^ed`bV8HobqPol=A8pi|Db2;Rkwrz23|a6Zxzfi8FbqKsFWT zg@k`o;+|S24-YZ7a+<=f3lN*{1F_?G!B$V`cR_sreG+ zwdD^}j$XTun3A5Ii=2X*tJO(l8w?r^2c5g7Wr7}C^2dteks2+h8uf^;%ydH9Dur5Q zG1>x_bs_i?Z0d4tDnpH{JPLj87E1%GL;|{YecCc#V3`Y1-3t4<%AJ_RjDpPaDXs7bBU~J{mnImw5cA701puw z*ufR}FtEd#e90_UwKY!)n4RI(OeLPYai(j?k(u*3VxTf z;0~cVIs8&f^Te+~vtw42T|-xMyj9f3pW`uqeJZQ0TCOsTv28BcpNz3{N#2Nsn}IFx z`98N$L{KoBS1Zff@giso4-HRA+;5tLRc-5wT&rkx;(5@<#O0Nn_rqlLxV^x9)RI4_BsSL9f4R1F92U(+?(`L#c--2e-m{Y;i#kMl+=JpKu70=W-ma5G%Wv^Br3I4qQu>rVz3MM+`dd_YIv{u69W*4gLa?7okx zy}PWc&dJYisrJqxn{0hb3el+5$|NJ`0ec(6&83p?Kb0;ytQk0fN;|Z-wI!pDG)0aI zkQAvoz^v{_<62p%$b6x5aEMb-hn$`MA5!OU^{OMjiv6P~3fa#k_O&h@t~^lx)xeG| z>G;YF&_x)rS@ge8;h8Sxh5}`}+cKO?{zHW2qGRu7{>x!>2T<=F4qv{!@2w9WmLR{n zn(jNsL1%+-w$2Qx9Y)pD@=K}+~knt03^8S@j4-YWPc)_#~Ue3Gpa|_{vBi8dKt3CBrtE6{%5p? z=gXvVc4|6yKDHjuX~*;K4?n`amRp{%!w?HKIMDEz>M}K<5EpoE95|Mbc`j+_?Yy>j z_#?SFbpHw+Nb9{v2T3X+wYlUT?wIKtmYHzG@P1kqelY*V`m5}K$10vahfEK7!`uOj zvOND!QeSII)U-|AN1A_rag#C5wOE!e8S1EjC8ymb8-5n3-SMX;Z;8Vjon68@9CX4a zYcYPq+?UPI>X%?Guc~U#`r^Wd?+Lt=H zj?B=l*F6o%Rx2zF62zD@%yf8uM3vCS1DxLE=+S?b=<`%h+epuyZahnq4O^}9he0D8&HXk# zu4kAA3OZW%m$TlAcYK_Be-DEys`pWTQ-5&0dq^EI6V`C*XsAG5O|(Er=FtAg6kf16 zy@*7{!&#*qtr*5el`8X%e27dVh@as;jrFphisC)J7P?)zc ztpc{^No_+xt#oOWCd=OI@R8pzk&$=1#C8{0g0D+;!}bi^!2OT^gq|-WhG598KKTb2 z=TuCj=SR3u`4`s-@9=8wLY{c|f~B)QHf`O$tX z87&Mm3f?Pd!rLs$+!9j;Ayw@hG3*}N_WZk|ZBtrG1jAnv6K?&r;(P)bR{)ue&^XhX zQ2IN3#6oJd{Ul0QRBHFz*kYH?0sOQd6chjai7Kc$ZV1 z)&ueh76dPmGi|3fisc|bvwF&1KinC#eCiQ955md5B(X-@r`szrJ39Q3k?PWR5=<&5 zNj|}qNx;DjOEX>!wKY6Qbte5GgVbWoCY=;8V#a0c(Zlxv^Hu8Jw6xI@r`#m6K}wDO zCqBk41TM822?c?h7rkw2mDJo^;U^u8=G*BD9}Q+cdkOrXFTS;PFay%YZ9*6!}}Ep(Zwl4@-fY}sB4v{|OOa6w2iXeb7j0A=i-+A@?a z)+8j6EfOtOCMpI&6)jm)#ab%>5#<2f6HT;@0ag(#>eMW#+H9eV8Y4fP%`kunP>_(I zq}sMQqgVK~v;k=l_4#8fIvb#bDO41dOnMikg4^rE`2=2Nbk< z#FjU=ca+G|ttl&Ra#2}XS!-+hm=-{L05pT~eMg_Vt*tEp(MJM&G~Uegw5qoDL11m$ zPaxA}8SbVG3oTTTG<4 zV99qS(iDE@u6g3EpskPS`+y*`Sg!L6c`qFqNyamhy$m7ZL^YdCDblnr+2Ltiu*1U3{qqumeUUdL~Bz}w4i*MtA@+NR z!O!nzTH3kR`Moohvs8_)VKaFCO>#SLzaS5>{R;@&-)>J9F$OV7r)}ne7StGF`GfNP zZnWb#*mRaN}|D#-jAJ<;3@sNKF~jGEo(Ny6h)I z&i?eO9B`d19*JC#o0F6TQ4$8r`xy}YXY79g*kK@h+8fZom(EX4R_o1SINw@=hOtZM4q=8hVrQ?cI(Bz@U2UJ#v@2E4_pJy4AX>L| zfW)js4IAccYPv-Uxdp`D9~eJv<4mazB@l^^h?$CBwZ1%@Twz)02L!wTY3sm?Mz)$i zb$7NJNq(?~vv-ML{1;*rx2umk6BlT_bUp2{>@cL1ztIzGJFZ6o@bTLyMbY$o5b|n3 zQjP)m?OXc%d(p@j=_`7?fHe#pJUnO%koEs;(a!j7 zSWBUSMBv_Z({%hBly+fY89|U?9=_vp@tZqxyD8G3m#n^d)BNIiHJAYvQ}9oCYOoww zQxmI2Ur+Dlcit=MH2_o%jS-Fy!hF-T=jL8-%0#vpU~#P~$B$;q>RBL6bw$(LsAD15 z$B=mBky0j(Sc-$+%p-n2i0=a(NwFe*B%jiX~^ z$LwjROn|TXK`rR1=e7-)iKz2G$ngY?@b35lk~)y(%h2~31FYBA@i3Ft zB|#X$#0ARfxCCNyJ8#e>Fg?P4@0=XgknS=V-;Y{}Jn9xr!!}<6f3qJz_g;DiKCg(# z+J-y-iw(Churw$X4-~yOgJDpk44rmrMq5MI*4B1+cL7?ZD1HH9-{%P7$l!%vzmy4c z=--Aa(tB#V9_TqgHS*P#mBEC=Q^73r`ra{u5{QK|)6#f&d85ocw)&u~9)>#_X&=l0 zslC;cmrI7dycQT!`LdEqn`~_mbPr7wero#RKMp_=)yITJ<+qd2A zNL$6a)rXf$P-`g&IyCWj-YKP8e~IXo_{e}_Bwm7(LFA<0KF42rZf@=?^ww%xnzt7; zS{#*{ii+xc&_OuxdQijuds2;=j{f{=IeGcTYJC_c=kql_y;Z5WNff>V{W3b{aFgFFJ-$ z8SecB=FG{{{qHH0%LbL#QX+LlY7EYUKi#kXhy1%Rna2f@#QXpYc~)5@UXG9cN+oH+ z+Q}=Q$K>S+NE=vgbEN@4P1&{L*EnxJiE@H+HMO-V=x7T8uC5(v-_9oyWTd0aN?=q2 zFC+Lf0#YM*p59p669Ve*0ETjp1n7l~*ohfcgEfWNdlzlZpxn%gyX$fN0_3<8N4iom zH;@`^0U~&hiBrSja!4Id{XP@g=>avY1}TM&jqNRa9uv(09ul2gG@n_o2^NP0oH5wT z1jyIl);$PDH@5mCffH)WWipFLnL1glYVgMoKK0P$*uq2O_Cg(ydI99jd#^lCQNl9> zuRM`J;rNIUenYDFiOJ8_p;UGEU^h3txizcapP>W0czS@Dv$k2n7vHb8|1d!wh&#mJA6aDlaT!r za%$lqZK zsMga!mhvM&n&*FpH5B8xfJR2b`RvdyIq>*4fB>xa$YDM%i|n0cgJAzK$F0dtgG7D1 zS+b~2?d}IGLOwgu0e}X3HB}()WOB(yOA8Be8u)S$Xw4Z#n@M7gqH$R8To;e9x8jY0 ztK zME_UcT_BLcu=6lmlNui%Z{g-CN0_kErZjc)4({M73PIkpcyZ0$7}JHn_EVk(&1d`p z)(zA9zG=y$5J{A_1kXjcsj3gRuxY6Q1DfG%2;QG;W3vzr@St1-lOhU8s;`cqCm3O1G2-5*<67HZ=P5G)~x4*#l%3hKB{ zAxVz$wAul%8jGcxnb$;?`UXssb{012oZvxVltO371qP&GE~Ko?1cC5_xWBYmPHk}J z@Fw5HtNkc%QAUq|p2 zL%(;46{I+L)9Gr6-HJ)v@;M~PKl2`xJ3+Kh-WHvsv({#tsnK_?bsU@`M}mbA;d$Mi z+*L?QIGBy#rZV0N&aIY$1_Zdbq;CHMfnWDPm2pvVuF~m2WB?O;I#;3oHb)l#953QT z1rbQ~#~EEGy2BRi+vVWdPf8!)hg{;NzUAQIP7d2=vP0plrRiM9ycVe&d2K=YoOA90 zLqfPPPK>5C{9L&lL!rr) z0gq>`wx}8;flq7>Y?#6sjI=YCd--S1Xq#+Z0Gr;a&Bh!i$7X~tEQ$A&w``*{pQ(Rd zx%8(@&uD0jIJgE+oRhxsnG&ct%g6LPyx5Q;+Y_|Xfk8$qg{7}Dub-ufog;cOJtjb5 ze}QNezQ7kjQw1d8!m&>rDm5cKEDi%r%R3#1uLMsAH|u0cU_+3;Fn_&?q<7!O14RYU z>yzoA$04DA+WFxP{ccF`VZ)7M3&sz&-TJPCf1KZhLI7on_O-a&VP+$j6>K6@{T&iq zV)QW;L!*~$Y}2fFZRxwa?Z=NLl|1Zl<$caM#GwwpN>%8@#7YN@WC<@`Z@X)OugGnM zvY0t;{NVHBAK*UYTZ$qwTM=0h?$j`M>`cYM0RXWJ%E}@GiW{@M6vuHUzVf+5*>1ZX zM@2UMM*I_a^H#Sz%_q8AgIq>x!iXNyLjiU==0(60r-76O90+0*k8bDYm-hjY%a44+ zce*Zys0~d`H*Z^OLil#1=Yh6{m{I<;1mF*PM+iZ?l30-v5pZi&wm z!MO=%%B|e?85qu-zKq3T0VTD$%Sw`o_}r8?@}6yU5-5^2;X!>1ov@H6Eb;%<<$sg; z_;!O*tz2#%;<7WcqE*ZdiUZ)C`(Fk)+c#{+0@7pb^7#9CWo(#l^C z3*Vf$fO5F{*Ymljo02L|;(J=Ea!dmrgP5|s0Q|nC>g0kz>lf#vx2eEg!kJ8i>l7zN zK^nzy;-G%$KrH^L*Xa)*K9I2(`(6D2vPGceD0q2saSni?Rcf;@si|9Y3pIu(E8g=+ zvAgNuH=9*KzvT2M+JPCO41^V@pkSP-N1Fk+1lFVZ=w7b#sa*q-xC5`3gpN{(hsMNljN_oGEql_ zeK7)2=;7A1@vYUU^?8qL2Tzm&ZFBjFB9F_HFPktTLAo)%VHhLh;%GejKj-=etrXX znq(T~{euIHp(PIfj;ZRr;@>;p-A_oNjtp|jpT$G@9%1_Oj?T^`-Sgr;XdLF)fWkpg z)3Unnu7N;{HU#=#<>5QDOWMq7uR!3{J#d2vd%}n;3>Dg2ah!!y3q=txV+zNAI@az! z_Vz$EltJ=t-ANFL5@!YOfg&^;{IL(m!tgfms_>@JZQ--QMpVf255}*z#wubeW=%Xr z$-sS=VsaP}lj!)_=+n861Z@U~)kF_U7<~{Id}nYOp;Df(;?68v zc491T-qiQRUbkT3?L(QkhTXt*LxXQXM^_KiG^`7_qS9vHFaSvp3LUc_#FUE0HVcAA z3-|eIaG7CaFmleZ8pioPp`9Z*jK0m$<~{AwpdPp4Oz4_+PzxN*Ibo0`9ceTzA-JGz z&^dEfiF$x6zT{yWAoIkxW3XeB~;&TPuM4|@q+?IGs>c^QU%fo+%u z+I5=_TcE#Vq$g`6TXJMXBRg9GuN4iO0FU)j$VXUo1gc_4UvK5{{<~8952b|?LkGvH ztgwk5eCf|c>miZjev9u|(p*gSCO$4jO@<#m-uTsS)O>L*&@+|FF>Ul;a{lvIUOwAq z7ZQmu?Mn$sGwU;gh#%0wX-rDUEohB;=K?haLjLgn!p)_R0j8qBL-=JFRimb;`{`XH z)V^sj{+HaacwQm^@dAz6et+u9(Z=CEa+WM zG}*pGz%jXt4kC?_^Jk)6qp%OUo-3}H&fAwD!5gp>{`4NFadVR)9Uuu*4eknYKHwtI zhERIXjvF)FxJw!dd()AHYT1h8aOIq8unIfe`V&2qW=d_-9SPbm{PFzzHkG}iQ~U=M z441jznG_;ZOQ_ko-j@n@a%%nl!9UQ5G(Bssce z<`Ks=+1HH4O6piy<{Z#8*UypKdMxgxt9G+yw34{hOsWJ}+?~><+pG3TeRT?n82Ndc z!~Q2dg1&k9^IS~|kyNJ6rw#JTD2y?1e0DpRG~&lY2E~-6PqEcYlH}V3syq`oRN*nB z`lrKLm(4{djF_8)h|UJAS$?U#t?+TiNJ^Xy1Ho2*Q#JY$^G|f7mGd8neNXFw?feHF ztTXR78zuj(Z0k9Zfc;KFj5)E+nt{!-?eo%d;P;=CrJi$g+N)Ibl05G@(e45(8l|cK z>5$uDl*f-BT#WIU+?g~NLYU}W;BriBj4D#y;E2q>N1Jh((I{n z)L^qd8Wi;Nv;62+`pOevDx+b)-Sbg6fZn)lelzNf$dE=EhgTq;aabWMjq=8TCxbCE zA(x$B%1)fc-l#hzLXZDHdjSBX>~D-+$oMfd#RvFM0bK9sS2>Bo*aB+iEs3@;++gkF2;G ze}QA?=5E!o-|U*6Uhsa*N|<9JJ(-%H{{d)ExzMrij9uyF1Uc(o~jn^%yDg*5eyI>Gm5i=FvblxEHbRiqmEz4_2QRfg}?9VWXd zWSo9SXmaA)4IZyR*N#)JulxV?_`y!5+p5oy_EB0=>ayc`Y?(ZY(Bhqvxuhmi%E815 z4;g_k3gHhi4h}Ux$(joL)6?VS22-xx@*<8X`@;X*VCAFSu0k(Vyk0b{&+}gWFqeD0 zN~l1KJz#_Nwu+6B2YLq}X(hMT%{m5WE_jv{dlv&g8MDK&#LfKDeQtIJJnr8vBdtw} zk5oSdpm!?6@)&q{p~zaE2f}1wJHZV}8JU-*|Hr{fE-AIYQL@O7%-ms>CxmUYDiM3t zA@u$BnIsf?spPPYFn5i#HB}% zg`K=14eh;*__ujJM3R@lnym%`fv8icphMXnGn_i-E!a^&se2aY?cZC7FQ!nYXz2H3 z77<{X;`N~R_P^tF@LD%k#c6!KsYB4K1BHB86=+wqG!aEO=7 zNlii4W@YCUq^C3}!|wRgeg5pJ^w0+-i?_7l`;f0zu$Iom2x*|=T+q~IHk_{<75A&j zs~JT72j9A0HvwyZzG)#=(o82oUJaX6PhWYczHrulJ*VKk*h!=Ij|j;vHWAHi;{}zE zjsJc4GK6MvX?)1&I1#UEYjY8*Tt*!wuzUde4-1(FCkqG@XLL_%B~szZ9fA23mbN4} znE|yTjDS17oToFI#{mDQch%U&jFoX7bHt0?;XaX3K5BuovXOv8{i=6-NP9qm%nAEb zvv1TQ!F|(>F1HB!zlxk~9Gr5XDJ%SXSrfjyp?ETYzhC6^vsp2)&Wfc<9a74*xj6jq zqn8v!zv>a(a@HLtS!iQEVU$8};D#yXa=9Zp9q&{5vT;H~M|}V8TW~Je4TOLV6vkk# zsFh8NZfVTVi3i~cY00_A$GPGW&UJS`_SAGn-VyV13)-a*=&Gw=IoMu=0g-cN&D)Ts z+CHWG_{)K#PG^Mj=SD3k<;SiR!pwSPX(R#}C8L2vF=2gNdP&(@B2)?NNtmNb!rQcin-?u^Zg;3H%d3J$X>B6y~OR{5O z?`T|1Aat!J&Sc{i3Go-YJ?$JdU8|SPB6x5MG4OHXEI*8*$D236DzYxMs;GNx_uKF~&(VYL89OP3>O1W-%Bw=c4xO2TE4Bz&sBlWM6*hU4Q#P zeL>2Duh)LK>&I%pk6E4h3k#J8)!bW_Mb9N&tHL6BDd==+GHp#VXel&Ej%PdHETbLu z$b%)qNDebKbYl(acKQg}wgL_oS1$SWZHFq~)$;r1<=#j9t8SKN!+rau9{yoj%dOt- z>}z(+S3Q1lIOjNvlaq^!qqE3uwMv#xMIlETM!LzFe%>H5N(kSt#Z>@Zr?(hvZ5Q#; ziZzoNGp$y?bIdkSwbdR~e(unpUaE+WNW)_gtq!^k1`>NSe{|ZtMF1w}Q!{#<}2-d}AK} zwoekl8kRdBAkXI^?h9=ki zM66QNvc1!MGx+RouTMvd@W=6?8&lfwHidb&-)bm_0}A%s@>hXV9+p(z1ONN-;e{Dn z6qHR)7qYO?Hn`73EXO1*IaN(ZDF!aB<<*mH7v=pNafmnDw1d#GmwoElgcI`~OWFot z+QubW`!XScfL$6i7ga|}itVJhFOLOqc$(c!;nKGWr#E{6^hVNZA#eCE$GSEqJH4cISRzwWw9)NQp{ zhGkq(K@4q(P*+8>CAK>5}e{ z6ePbfeV(=VT6^ua_I}^*$NNY9frxY7^S;Lz=W!ipiI2KDvT`)cdb``(SY(gbLm$Lt zT|_dJQ+8_CO}p~IMLwg$nnM^Ft4-QJKbBy@9sHF12zcfjyyA_W#T%$;qI4gyFE9TK zOL8GV6dOr=HrE(U_; z7z3Lbw60^8pT^uY6u>Okobgm=JIpZv?D%=_hwQj?8A z{ZvWVc2$=5E#q%<#P0{SSD^06hCgWLpAvxGMJW&M^KUg>Y9s9Q3@qel$Hxy@wOdl8 znIc5EB<1hv8@9`gTAiiLWJVr1Ww2HI7D7gbs@hiG0<4336;THow{;Gqps`zXL=V|o0dXK_G}lFOf38AJIC5#nMq5eM4U)dhS7 z%PF&cg0ylTMq*dkVrT`7oXLxy$m*K2cJ%}$uxoty&q6QiR3s4r&uVv&zN!X6MoL=1 z?zURH&3;e9D%ju+3JrDtgas`b6L=mPbW(QPUCe z)}6iAbJX*xT)lSYM)YdNHtWIy3`@<1(R~w*5NQjiq?;TyM!sr52Bk#?yt=Z6LXa#K z;{nzh z#^jSJiG$G0#_YBav3W}k520uVaqFG>o&{yVNJ6hm7bP;x$rXxz)p6(BN_%UI z7HSzfnEBK72ET`%b4{UTK7HvgvgSp@BEp^IX5Erae*s(-0{;uLl6c7DWRSk-JvNEm zfdNx-QCS5gO1k6ZO_JPJ(WQx}zdO9@yWwWAc)N^N;B{Qt+)ohqmw3{ATmweo;^M^8 zzj{zK=Cl^#lCPPNmufKu2%KTh+e&TL3b*_APDHwH+$7m1xk~(R$jU-8u<>SODR-8J zM@G=?wB%RZlowZdBZIr2XiMVwe>UHMlG`OAaOv~m^&v^>KQWd^oC06cpS8^&Xwj3X^ZxBDfF|;na@Xl_fq=ba z0<-+6eo{SFsG*DzZ}8H1l?XKtx&ne~g!}DTG@5Xdnyu#07{EQHVZmzsk}XpJd5ny*u&lHvS?f@H%vzf~HEYiMB2d*jn0@th-zw zyxrS}2fu4Q#E>UHh&-i|#)cp$-_Y ztup#W3@JCT%pk6MCE%)0HEg-?iA$K2KP5h)gr`S)C0+Z1UaN`ug;VWfFU8dp%i2ZY zi!yNT_Ro>}Q20LOU49;_x#sXz1zaELt(kG+GGd5Q~JV+DD&s3dA~Mpi1&jg zCFCP+#)xkg{%K#alMR`D3jn{Qf9+@+hx8?zRv#`lva+xoU!QImU3cTYm)pwp{Qkm{ z9S)Tw*ts2KlhwpnSnj9mQc1SSqpG9EYOd}?;~tVSc_ioTJ+bkU4nw+dDb=?-v)k zy1Me%&z%I3-hH+w!529JP4fWI1B2nYRGAGVPe{F11HiGPMLl%US4x=}`|oY|6OaJ! zncxgXfeVnW2vdcdXM1ns3v72sLt2mvV&!M1r^z>gi9`%VPfxFU1F(1jac_>BtsWol z&9-+yQ}zJM^?vKheZ};?lYtR9oN)NRHnr9@CHDQVdeH-hES zxZ5oFQ?T*vOk>CNva*AK3qq#91~C=rxPAm#WxmV)!2v0tc@l>%YP0XlDl30L_zQ#D zu&FGcvlq{sK=(T&wDAYnWe++~xNJ8xG(f|lkQKjwkM+E?yquT-LzDR|n{@LR;mS2E z%n1a_By?1PFWqW#Kp>;ySb@p z+IZ$>zvhZStLe5uTq*b~PhQ8QX*kgNLJI`Zc(Y7hU0s0R>%5t+)GX5uI0D(#x0CPR z4Q2VPxWbDdVQY{IRPy`x?z`jU_IIbT{%s&_%DJg(_r3iIEHor%s2}5xpOPvSPBQD% zzz9mrPB<2Ul5skEdY6J{4$WStruHzfuzp8&MnE9aflKh(dus=PcUKT(DdUBGrCy`s z-nabb=4S8*1tq1hTL=T%oEh_Y2VQ@FvGwspnOG93WC*!!2@Q$P8K4k9#=H1Rcj&Pe zaeR>Q>g;}z+#iaIGg~_^GeejW$}iPUL6MQHMi6d;@+%lo5s{dYqSo#NFeb+G^Z;-K zkQ9%up5CeAB`Q|tVjVk#h6VZ|9&2X0&j3SJ^4(WMQ*new$%W1P__2t93Z?uhzaO3MfxK3o6RT3Q+yb^Y*^O?>b}Pq-A-^S+Rx@F4~|E=JMNFgNbi-rs7P ztcmYHP2;?X_1qHuVlRprN|fa9$>mz)L6UF^`#aG89sKPLAY7}D`0M3uZTE}S3PJpa ztmXXcY)dAFsM&cfaIwy3TvxUDmS|MHa6(eK%7G(wx53IsY09>~mN}{4^Y~bv)X`jC z{uy#Jz)XuWcd;LTxn%y~^KRST3NX%Z!Bs{JHOTpkTL<(*^xE6fLH3vB=7MA~4c$$nfyb1MBPSwN_)0ZgX(O zJ$?FA(DRro=JF5pCi5W-4#$hvz77t|RHO~L5~s&4$vG0aZ?)UkLJ2{-!A|gPW~N{w ziivhx;CKT65%C;M_ZN_ACIOuP_kNds6-HBC{jT%z>JjAYRi_|YuPiG&JRKnQjBK!< zuPG`js`R@ojnf7o&KuB3zD7rJ2e7~k2s{gJ@Y!+UkdHg4@G8b56g9eCn2p2wb_ebT zQnBkd%uHPn0v3EeEiEjz7%SUq0J*pDl#8DqKWhg-%Hxxh8w8KTzkekBZSa}md`6&L z=(D?_A8G(N@KXdb78XbFespx)KU{U9BB6YXAa8`Q`Ud}NAnZq!BcC< z;^bDh;$9b8z}DHBjhwv8ziw^KnU>%9VQ$r zBW0yHKPM+AW8<9kxSkl*;wiOd1r|}@XSln#y4old%T->zM@%237AQ}H+}ncqjUQ5l zXLt4yC3c0^wa{uTpTKVDQqFaKm;hY?R%Cv(mCt>Lh_55wUbJt2oi)_@0A6YK%qo_= zXZ_E5rgHp$1Nu?)rhrsuy9Xb*rUt;Lfd?$OTsCy6MxX$`Kg$ETo$&Ao;&UXLaI%(3 z@-dg5{wI~_dWfA>@HZ~;&n-C(GGz4OT_&w?k56Ynk@<6$H7-|*RGhx9^IlWh@$Agk zS(H@j!kjhZieAu89A`KjGRkPh35k*L2aiL4Ke9lJ&yyHKZb(iYFQCZPjuEsGF3;4) z3?` z6{}cc_!9rR0ZK$d(6=`GGhv3FZCs!bVfc%$^n>cA^_U}5`h=Z{QLlgfUW~;ey32D2 zoT2}qtbA!?lCJQ3#0R_?1jc!<@!Xzyfqoq4O-p@b0R(kdw)R!+1e>n@#T{iOK%BU3 zX8-w_GiI}dgZvyA9){zfR6XC6qZD4*nXTX^+MJ*&uK9@Q>*d{_bAiS_3C0G}_-+wd zCMHX(ho`Ux6ZB0n;$XemtG4uoV`K-5IB{?2@om75iE}797VC=>#nw!B?97>)RVC3KeVFxPw*Z z!|#i{pV3+9WOtrjVW(P}IXlbdDucSOYf_kP@0p8FH@9$8wi<60U-oxH%pSL0#$0C} zhWV<73(EzwsDhmubl9$`V-c*#kSvMi%+Kc5tb_#IE@`7w-5 zFUqAI6~|KuR6E$z-s`t7zd~C}W^$1z8Z#YVvJ=&|-|%h?$JFH&sn6j4!j3m9z|GAK z?&e@6MBBWe-{hRz+WKa)KmkGu`mm+Gll?O^9#{M;G{zyXDyu0dYtyAj)^^Y=eqQ5u ztFJTGe03cADhQe| z6>8fltW@OPH-kr8n(Askmf%MUR6f@nmYuVGla6$BerDGcZ;itMb1zsM>m zDNxi#kk?jq_j+w(g^&>ue50kk{o-#`d0D^Qj`|%73&-mh`t{a|;4G^O>xeqP4j+s0B{BksBNCSSz4ufMafBzk2>P>qR(;{gG|_eD%rCGuq=+Tp`+mE0Mr?HK zpC5A6mPFHr$=M_1eyq3YUW0pP|C|~;%yz&?fOU?3gMDf?-_B^}?XhR4Aa)=d8QJP~ zy3_YYCnH^50&sJE=JfB=VLUbG(ti$z?Xeni|7AF=XyEBd+F?*FFN% zqfoV*{$)5EP)(T<^>VS!NVUz3twYCnG>I@H5N*-Vw3{{1kXy&9OwI0vL0{^(^YoMo z1O)gJ2OA;7_qat9)KYzkc2S>B(1l^wZ~-Lp3el6GyR127r9+ts$Hf^Xvt8`S}nI3{lqr4|l(LYcAIPO1G;caai$|7R&i zDG#u<3juFrC@+n^6L;#DAY}{w`MHXU@&UmM=!*zBevTxe;AH9YKv%bABtxP)9ZhKl zjn{Kh#p0d0{$~3`=zBrO#2Fr8C(xRV2?*GNggND)9|GfK*Lyw7%OEo4U zB2Er;XV+ghR2O3hj%#hEEx;DBW-5YPLsg{S*z{>e{iI6pb`(6SgN=jd@n89HA zg}mv^Fi)V~TFl95ukLx+i`JVm-yojg>j+kCq+c2I;&T&=nr9+^hOScbcIHz= z?$&oE|=(QD$9$m#2fmPCV21)jO|ox@W9Qo?SDX zbai6P?{JylnI*nq$II3>zJYYGY=E-}h-SC8rsm`n*hfXY&!`$8`0%pZn6P7O2A|7% zs#TWv&0b-n)5`bn-ygufc>Qw#G@$dr?*A6De|qk9P~Hep!a@Y5j}Jc{TUuLP!hiVe z2o^n`Kl%ybS|QTU2h+e-$E~+FH&39U!SRZ*7u(vq@Gd&{>~FSH z0LhWgLol4IGI^dVwy4ikEaY(}|3*ej+P3Dvu>MuFmy%pmJ}C&PiT#?-klw9tmMIcdLT3mIg!r}v83DKC^ODGMY2pYgX~alY)T*aV(=?64YbP-Ys8_rcbS6% zpPsIDNIw2L*^rcRvq>|1lAhOG4~=1vkJV2>(<{Zx-8jR`H83ERs1q@*Nacsbgk`Lt zzP8*WWvovPh~`s3_yc%V^!Bh8?G;#Z0H*cPqIMpPTxK)r)NJYjJQ~dT91uJ% zHM@b+g%uz*eadakk=g;K3UF|6BuE{XTQ1)Kk0}_zdJ#-80Xf3~hg9uOJ{yF}K@bdi zEQ3GDIk@e5Ju+70W*2yKVREnM%wi|FI5~o6K7*|*2H5+a^an5gDDDohXKc}K@X%0{ z(qPWag;|BGsO;=4Y$Sd6vb;km22nS^t%io1f5OI)9Q3_|#fxncB8`Tm8Rzf9_uM9* z<$Vjo+sJN*AnlLR*^JR&%bo9LDs`l#aXh#COwsi#>)unD=QZbl_1_$=IG$!q9^U$< zRyd|H&BHV-m%Xc_Hj16BT{a_yv?D0_k)kJ`!$w)`?Vp~iHfwQM4tk7el^4@{bqAVT zp!(wHPsI;mR-=Yh0gK52sPC-fQ*gmZQ@66ejnw-AZ zGb~-l7hPFPlGh(H6Pg_-5Yczq@80ujii=fzV7=%<8Q(pONjesWHbK?onRd{vAs`_$ z+(OpSP^drui0R-#sQ;>5x7D|qEg4&;T0p>Hgu3Q>aE^85=W5hq(9>59`zW_xUxuIu zT^BijiLWD51dtPvTGWEwf(Z#?;=ps*sJm@3e4Z=Cvj-iAPX#?4e5W;1{?$WSMrN2O zg&1j^WicXqh+(1nX;j|^uMfRkToTKR2S`4Wzs}2B3oCSb!ovetC-Lle%ZlEjz*=q>#^KRHA)@n~ad4bot!uHKr9kWJGb9!< z?vPLz%v_lww)Zjrdp**gKqF+K#5(qAiQ-mier$=Y;U7v9MV!4@oM*Wb*_kCmUY6rZ=2L5ZoC0*u{$*fKX{Jl@ry>dARd~;i;+j+sHAWZZ zVw*YhT~Ftc@5{oO3d6WWob(Kg#HpqE8_P!4ys5_?;-ak7M)w2VlLc6#6^D=CAF8Gecr_ zU8Cpclc?}#h2$N(_;|(6FRAr&G-W--oPHb<7dM6dvr(z3!THsv4dOoU%b|Z(D#@e; zhq1n*BH`Fga?PCzfrUZdm+8Mj*@aW-uVaXw6|?`LUC&RRkc^zA8}mZN1tH$k^Xe*t zne}G(Vy)@KI$yq;+vf}1KWYTCxf7E}dC-G&KD>gB$XPAELoo?~ ziGGH46E7fHRn99u8^56V=8=t*LSGhxlB;`ely@$LCgGhXxf6Iao+>&6HQqK~oEyjd zmZgy{x2xTKz_QHI^SljDK^n`=$4g)yfHRPxY~^tmXf*;d`*kpu3WiAif$!n5Q1l+* z<^(Lv3XH@ZT1ZmB{#*a=j7*zq29mYYmidySWUrn;yF&>mIIW0TylU4h&9y~~N$fI~ zRU+j6#Kyuyz=Gq$5ZR|{sDX<^5S3kP@e)^n@^HZo%@WtZ<4Jc-NACQk9))|xIH_*1M-P{r#PX2}9WN;<4%0$NN8z!wm*0FoMi5xOYl`nq zqbfGcM*K- z7oEDkykq}3jLjw5t5;{%Yph>I_uOv9%@F>Xx=YV?yuq;EH77S^eVym1cHWa_;(3-3 zCwKZ?!tFH{$58?e0dhVi^*M&cL(`ZrR2xq&esPbAF*H)fT8&18Su61}cxHH^$)IQb zv;>p+_{cac`d)`~?00a53XMBs*-CP9s>RX*YE#qG!pEyUsG{;9$NGY`j|!Qps?*K2 z@b>a*s;pEhR=*LDb8=!$6SqOEZ2!4XJ0`OD{W}RddH|xdr>Cc~GTNVe{(}7$A*+da zvqH}!Y*>2n@8g%S7$108t7~g_HYjP)F-UME?!}Da zC)EcWDZlWnNrmblyoj__;;=u=8+LMNEpH7$5gB zfpONVl27vkTy8itSS9H)YXPjmdfz&C!nB7X<&%QP(l&Oj15|KoN;33r`|S+igkC&< z?l&|XbnY{vI#d-ArgFG{jCk(fAKQ{Lst9vcmh4q<*Bq_d_<7SA-B426kePHoJCru` z9mY%qY^Z}AK3ush5EId+fz+|d10-?t^Yb9U*&qcS9;A1xaM?Fgg+Q?32QkKg0X;4P z?t8zBJW|m8A$+r&2PWUZ`ftPp!Soh1P`nNGId0w|qgc?zJ#gIkl(8YJD|pbtfs_Ig_pNu*uQ^FfOte+eHx3BLIiz!}({39D7j z+gsLxBZX^z#BWRWSF7D+7cMacdEhK|mwgu9*H4Tj4)bx|dEI+PE0R%u-n{0+7n&lw z`RmcwG2i9}?94Mv%haY0F3sIz)An*-j=|A4pO!_tP(S-E>%xSZ$@pJerQ~sHWqX^Y z-6X-V*bWDouaw&Telwir_L zgmv#5vCr7l+#?_vU4Ojwf2_>Sh0=8#N}z7_{2@JE+R%XW_=&`86$)M=X5+ixtpna` z_i%sTJ~gh_ zRj`cI&8JFydP#AAQUy%&MkkRj3Qv$K+`>^Ozw~$K@FL}nnm#EIaxz2g)3MLWTl-;qN9 zb8&;OUo+F=U`YzGKG6~JiSlrBw}UV}43(4yBsFW$nw(}|*d?YU2xU86aH87+2@Z+3 zd+L`iqE+NvaZ)O+& zMVi%u4?vbab1N%M$j%W6rT!1hS*8@KZX0RsdvYIIx}B4ruI*O-)|Y|*AUh=_JuZcz z3zzP{#Q){ko-=HWxaK>IYrOd{%vt2-f>Sx+Rr9&O<5CdaphKb?HdAy26&G>_(;7s? zufs|`PgXMK$VIF?+Qrw$gU7WHDQe+w-20Q?lA=;jBw(C!U0{LPpx;rLJQnb`WW$h+ zZO)qOgbriW7XR%#<0Xb5NI=M?Kfkg`+T{C_AoiXUp>!cgAOQL6?3}W&u$TWFLSCmF zLq1Epz!z?ngXvSZcr^D}5c0M_5@%DYzoJ2CjYs-yq9hmYrk23CePPkSIBv|pz6N#< zPXZ{qOl<#3`hwWizzg-)PXd0Mg~-1CG4YeEcuo`_9-%5R<*G?RC4V}}e{I>W9@HTc_apjQh z1a>Ex;L}{V!5CqEMuI%GreL-F*ZR{=<4+LbUp%s(I+=lbU?&#eS<3}>!#q{q?{|@{ z0E(w1jD6rOFFZHAjAQ?FPNEn!+$2&Abt;a1n=i^G)~lX1%$lvszg;y*|NFoX*g#Tt z@p1Z*M@gKe?2-Y3ce)xSoBN_i$e=vCCtwuB0 z2@3NwoyszRQ{5|cqp+l+qN2Th`M|Io1>Wr4-r=DT5HF-AscUJ?PEEz__1W{_Vq<>| zg&V-uhcgFE%e|<6wk(ZKZG&$4Uz4gDlW6*_z#N;KYf`U^kC$`alHxZXUOJyLVq5NoNS14IOFMBY=*$>8)wCb~(W;JY(bF zgPKR5G26PWu_R5RVGFL1W5#m^O$MwBE@aq53GzfatJf+a6JmCkguy!I{ZYlU2d5R9 zr9)g4|CtIZrJ2@z*i_|jZm>Dn@XT&3kPgSHiH{SO;<=}O1(*1%>rw(iRMqwX4=;b1 zM4LAL1)ppOm{i43VQhM%8GWbTaB=n~vW!A8&W(WI(=JAvrQN4>Y-WLlX92e_(lw#K zq3U;7c&Jmoln|`5?a7H(`vEi_E$h4lyhnI+^w;5GL?k3iEogP9(^ys-novHN>4`iU zP+f$Hfib9_0MN5&C!aj<(3`f-bX31AZf?dm$`ta(SR^MW9}_zN0#TiH-ar^UAPebi zZ6#b})GUKoLIPfjcS_tr)NtS=rPo7M^&4QTZhJaw1ZWGYAK=G6 zY214P%>-BxIY<9$?7Vllzq#!F(Q}Dl$q`sp*!Z{`A*8P?TRFEoxWyldrmd-?rfJBG z{<4gxRGmVn)5H6%;z3JFpXsvy$@RCxG9Z(ui(BFhaX2DYw!XHVY)OKGN})`7uc4ti z3&v_JzAC*Eyf{_+B~T74WB+4Uz`Nz`cl9<_$RI725=@^pzx&1e3G?m-x%{Ozza({r zQ*3+*7TzEFGFH4_f4<9*VxNe|6tF9u*3?kbGSrC_du+uFJ3e!pIm%q7bsJm%=rG@u z78RlgMsZqIAS1F4pUPGK%s)eg{@>)E|Ir?zn>k%uF+x(X!SjWbx-pqHl6+2I($VmB z^Rq+7n$$9(DY6FClm&M=4D3PVCN6gz-ua$aOVRTfc}f19v+rVGvf6Zt2pcV!Ug?UI zdr*-0621=mb%rD88T}m}cH^CPlQX%k4NrPG5TgK`2$bi6p1MWQYbR5IXI=aD?H{`2 z(%d%y^~>CsJG$z)5}s;M@eCu|lo^u`dEE!dDAeXiTd=`~=;4(8Lr!Fsltf+Dd(C2n ze=fO=iN}F!kt`mxe?fL$fNN&+ z1V8YlG$?ksqLiZddrFz{6Q6@UMNx5oJqI<5|Grm+>)XHTRT=rOy(*^~|GQolAXELdib8)kP_U+L<6?;?#JjBG&npcZ282Yyv)TVO-TOS0>iBpM z83D!C){(L{0YE@0QKhiU-)Gpftj&EgFaI;br3xlt`=27*zXo_+{2yI=13eNRbwy1* z7rji`&gD{50?q4AqJ@7dq*6ZlkxW#rI-FTjhpf+mecD;@=TI+##GMRWgl+3}eo7iK zO45QWM&HhX%qyz$A2r!!b%b9fjHS66ZL{L|5(NLgIRz7zn^RXy8XN7^r0|=cIW4dV z!T#%z14fX*DW;#3UgO*b3A&#l;Bq!s-qtB_K;N##Q&PY&{l8lDq6D zbow0M6LWca*|_A2RF(kZIzkQqaQ5D?BTWqru#{PCE;ne!2?+OI{R~AXoj0MaqeG7q zsa399Z#_X_B@z}IYD_Bra=bye53#2_y9C8QL&IlLU3k)w1R=q zeW6`t47K0XoBN^zpqG-WBqBau0LueRXlDwO&XYY^RK(H~h+$3Wphk8LoS|~7)3K1r z%T{Wl_Czc}HBUXt?!5JT`qWTcJKQzU0gF@L2a9t_8NLrF5s5_oh%BJGScuy2hamhr>yoZ*WMr+jvSMU~M3@ z*9!-nh}OKnJujN%V@^Q1Dm=GgJ>RwQ_7K<>jc&m{d>1R&D;cNmTvLs zYu+qUYtBV544jAi<%ciZ9S=~Y({enHv@HS$NDl zG1I1jYW6@@s|IcBVRm+OI&$$n9eOgd#1nr)wSXiyiG}xfelD?}$DJtkR_dYpc zxA~=%#xiJD0jaEW=(T{Ivc4)*JPygR;kxMc2^mI!_ABIzs6$!{(LXbJXEiutUPcaz zvX1Kf(pI>v64#cf2LzLbgwrq7lM@r@==c>gMSG}M*dNs*FJRxiEL1jb`CBR5%( z8!o1M&RO&PLadu(ff%x6AXxKwp)l zPit7}IJxR7KQ0m8MAT82z`uOj!z0d2lw_v56OPmz^Ie{I_zUpPFH{tr zDOU?~uh&?_*4n^c{enmQSDrDGK?_#nUUvvm_>VLK$D0C$^za_oLc%%&=)IhLN(Kf* z3v8*BDAZkcUB&UHx9KQHi1@Hup;$TCCidUmCl$a#@?<`*fa3)ZVa=J)cu)OA4_ zqW%xSw&@Z5_C2|y%dywEPbOalLKcURk#(gw--6vq66aHM!dYac(rkMlV5|-Oo+#VK z+mqMz)Z49LP;76FgxsuGMXQ&!AAFaXyA5L1)PC@}rsv%QB-vqBd?e@07KuJg;cN+t_ig(@NHStAQ`CqTwh)KKu1 zcQ-E$Q`;oK-w6zR`VOfPr}G2h43pX~yw072vfRw@jn0@I7M@aUTKLv)`0EE{w{Wm_ z`vjFMEgeNoOU&p8vPm~JiCd8NstBV)j}Cw$JSu3!H1vSRBF?9{Uai6ayJMkC_0>KK ze7XZC)N^!1d_(Sg)<#u#Fr_{#flskCKhBK+hUPWXw|qj#1>|-AYAzaz>nf~U=ud}>na%QXrT=3!y?pZAo76WYKQc`l2F88THmc=eSOB_ zz#L-_1kLeXCaDoN<}_%L;i|oB#83M*X3H~Z5ht9bk@$_Q!Y*+Owz~C0KY>MGE>)l3 z$XuIqMOhN6@;n@JcO6$<5MA3ro9!ZUWUnJ)O^E>=E#bE{Iz$T!HpSIYH(jzt%jtKxdufb2v1 z{hZ7qtthT|%uT3u@U8JFNw)mu$)36>dwp;|3GZo?Ifv`TzQ_61E5l_a=C)3!9BZhQ zAss4}oyHJ_F3F67v%Wt~Nm=NubK*P!=i~Z$z>WLJMI%CTc}CxmKMIvRQrA9;HYWh zWW|Xz<}CR4qJjlnmH0K)R4zKQ&)n!^wbCSKXCX`9eioB z^&YpbYovd~n7*n5me#)@iyU>K1Ir)MzfgEza`> zPI78Dr0#nf5|el`6M~@UXK0rWCpA3sqd0Ybg*uXyRA`O8LO#pYbsUs^qE6)ZQ$Mmd zm4LivenJ?#rBaDiUXc+Kvweab&HIMN*zEoVew2&Y4kwXY6=^w=YR)>@yU|QRFmRHS zbN54bC)s(F0e$t4XEiG~=ro`%)?tqDhG7???#xNGPKZkV)&F zU%gtdYb~ptCPAgolAFyN*ZlAz=#XE}BfO`MD&}Q?=XJ6*Vl$ZMvk^IEYWuaF4jGPu zT>WJ3CRH0l3$<3LX{l8|l8R<@x`XZPIb=G=+iFADRnT+E#ryJ;MzfNuX373u>L9D8 zZ>GkG_0m=@(2inR*th&BbFra}6F)>z>+kpw2M&SoY_AV)hI>T2(g`wTVwS;UU4l1f;FnuFs#;aM7{rmbXYhG7ti8ff|n{O?{ zlhjY4Y#lt_U9C*cRK##2rA!=!=O`NmWco^2?Ebs@EDrmg?a>)jH&hBmOV$07FGTgG z$;tWgs+%7t<(E0HEtsdlm@6Q0b#x<;$st%-BM^CT*Wft7oAVgC2j(Lk5^2r%9lB zfx$08MXw#$tU;Zja+qPGBeCsI=cf^7x+-3{qx&Gd*?PI^qy+?JqpYRYP8iu*VY&k-D&o%2RMf_x}CVH{a<49L@uL;FGETp_lUCK$g=q{@ zBsmAL9DmR0dx!ZNYEjYb?XKCLEyT@>hx=R6hwF_We4m3}WUe)j{GHZ9TN0wRNQ;H{ zjCQ@00!s+oMV;bD_*zZh&Og0vh#`%5kgXEIBmCSdQCi3~AT--$; zh5Ci;lwe`QZf;@zi>0~yn^@IP^R+tV*bH4?j>6hL;aFFzljGL!_;xcq z`rvpoeUdK~5c=$OzCPU;kVOUE#3$GO|MXdunG6o{UY(tiXUxm(PO(;#Q+iICQr2hw z+#1R|CL-!I!DRpNw0gD_S6(Pd9}g#d1%a-;#Dl!v0V%|QM}USSUzE&W)Z}aU_HGU4 zkjck&n%g~w#dR2zPA+7FpYP!(ZZ6{UR$crkXVY-#nFPDWa>~CLf3X<9^9R7e)S~i0 zPS4XfuU}gqm3MZo9I!Ye*xK5zJ{$M*EOKZ?lo$)e6Udd)(<3}WLX^bF%6%NZ(ty$& zf@t$AV`q#^etZtj)%p7MYhR-gXjQO=6PgS4D$G3Q;N%<`8OcOZH83D{nUkxaSM%Q% zhpW@u7)qoy5yj}ok_-`O>*?#!|JzxBg+K~|91~*IERUTPfA2CVvLASa*F}BKAN;em zTcgW3Y9ptvm6G^2%wE~b=?*#GI`NNOINLVBi{dhKyWb*P~#Ef!Oog%pxg7bYve99Xf>DH zpGRybmHhvq`7oL2549})N}L7PXZ{5b7o7j|r8viaf~ft<^3~_lY8a!KbNDel77h~bUGt|FGULoSootgbH#UG z>}T7Ti(v1{!`EiN?)r2hY@lqr5E{>aCgPsEykDGILHmf5@PZnioHP2MJaAvoP~@x< z<;>pV9p7C7srEdLIXCD_%VY*!08>%D{yS0J3~`I`%*OY$a`udLZ7d3Yy28{$DPaTE#Scx6|0=wKI-}UT* z%4WJ<_xKa(aZ`93Pp~cD(Hi%Yt*Wk`sQ0JO6i3(4yEE(SU=v=ok19+tkdk^DJOjY4 zJ%meD@ytue*)XKGL;$KEQ%UIbtCOEv;UsVyPJK0~8o;oyh> z2R<0*5IyRCCvXhLU1<-W98@+oUO~pm_iG#(C)UuyPe6lCC&)nxg)jO$)rmLc#B{pz z!5|MQ6$}g>Q!$v?1wQYo`cc?5xS_B&(WX&n&)y(T#1QcZ{zQ^3Yiko~(fpp|e0^df zBo67C$Y_a)K}nUDjuxCf6D`<|iiqesZuK~l*a)Au?}vU;9rjUChRXCxDiWtF{uZ3R zNe86fkW$nyoW)`Sg{ldG!ejjpG=>!Q6#!6Na=5u zm6bujl%_$n=s~KbGsx5c2v@dXQs85LGZm;V1Ox=Iu&_&58~WxP8nYl{i6Ihfx2t56 zMaUvFa6+po*K{)9W!7m|hvKH-9v3GoEbzDhAD$($~gN zx=%J|D#LmT_lUMoR@!>C8XxayPdHNskAMIImbv-)^HX#a>{;MF+=sh)mywkgC1x$P zzAMGg%5U<~3Ry(NC!yikh^hRu9WY&@^~bn5R~|(B&R`akv`CY1ZYjaq^3N9UaGzCI zCg|pcO77A=S?Z+y;Yqst*z0>tp55VS?zr{II+gQ*nb2pn!=efv6kE9^%2enzVMIT1 z-l;-qeY^W0Ga0_+j^|}?hA2KGUCM5EJoRH;3`kHGKxtaQg zcwE};lJs7j88Sk->C|L0KIQPmhjTZTwn2wJ38B5J>vajf#1QMG@1(PDW}|LY6k;*f zxQ(lNcs0H5mcF;EcDIScntfE~BSSfdN#YmqM7p;HN)780e=rXcZnkExRlNg~`GUBYq3y7_;R1B~@fFyn+E9R9ks9u0w>|-b(m563JN^!{Xm~El?l>cO zoG494NMoOBYA|?cF80Ap_8;Q1JCiaN_dBAG{6}x$emq%(>;WLfK0d*HG9eUSNlMy4 zt-w-uy%k&cAGH=2pmc_Rrg$d2z+(~)Lq96!p{dL(PY3wC?9ZfJLh=_*|Z*t{3OCJLR4LEc!gSO*)LYlYY}X<6HMV9ros zbG-1z*+jEt(OTdU&{bIewkk0QXKypE$3ij3YP0Az_z&ij&kl3{R@0aKGpxBgHz+X& z0#)&U#bTZLQ_iCVqwV&Os+f;<5LHa11Pv+Ycy$yEEA~qn(!sL#BL=9OANtI~`qFnAkIhnl^HerhJl`#F@Ke}>Jp@8eLP$lF{(cEov*00Ow0$c7I-PPkZCCKh{*&WU#gMU*KPHIjIoW+PWNS^QBLnqG~ zoE0>Ff*pl46}m?d+7T02!b2#)N0~z=-gupgfUt!bGEmEi|!tEz;%%vl>rBMb(s9lN~mKy{o~+ps?#| zV31SfQqc7O@%GkHU2p5+?xMT9ySuxQ7D4Go5Rj7YknR$Y1_|jBq`SKtq`L$pr1?$O zUTdFy@}7I|xc84WTtgkA)b}&z`@GMSsVpM=o|X;14~n&Lz*@D0R?HLU;Kl8_aSf1U zeH&Fr?(jb(-ss}4{>Bb^f`!$|9~s7$lKt&IcR^F2m;#idAZ6sw_4RU?@*MK?#Zepd z5zQRfFm~#XQ zF^=C*M$a$@(eN!eas9=ekOF+j5lt&_Jn%tglB3^hOPVGPU^JEDD8DG9yI~!snx_Ll z#p{MG4~~5c8tReG0}a8++#)7qO>Ok($MC-iBQD5iFh=2{zlEt@R@})fQZaHq`bveJ z(((Lh=Y|LGUB5@b6njCTrgY$xg8~%C2t^ip6^W&a^t_wVKNzJ*Z6GCz6o}N4mQ>RV z4Norhm#qF}dVuG=2}B$M)rTmUA+&+5uhCOpj2CdVK{u2AtA^wGAM^hxAoyqO9(+hl z%)kiv;8_|Z(Eaw?2yfpGQik%MX}@KWe@XjU)GdAUbA66bfVareBj=>U1F$GAxZ=k* zq5_PYmUhlC6f>Sa&=gj*fBhbmN{+xkO$b2y`{z%Y$^)a?|OO(ppbi;QfXlfwU_xE2Q0;y60q?N`OzkO=D^i z%dD>q(7+l9t7`LM`QVYf?4%GH{Gv_BM1SgdB~9%eeo8FR5@P2mh_la-@~ZP`oQwKM zNW6vOD2D>c^?{@k?H_>}e4o7o>Y5qTn_>c$fIr}RveO6yt$|m<3JUl+{*15uZBD3= z49A-@Ut5B`hCjX-L52D-J~-CGKyTAsz8j^ZET50bjir^pvv_UDK6$Qd-ro-^#u&@W z*kOoB?5=-{xMclF6?T*GYfDt1?57kM0&!l+u4mZpu=uwm>}0) z=kfO|=*-f?G4T`5jFczk@!$)V)L`d(MJ$rs4Z3cOq&^Bwa?DZ zf@La69iyh|>gt@FPG3p`MbyHgB8hs?&Ln((6&~B9yaz|dE8#$ciq#fRK5xNi8t3_1 z&ElF|6H3_P<;#~_#r_eszE3`!63-pS?g_V*)PhWpL6%WK`Pj>CgSBTGCMGK&4gtl- z=P=;X;v&4sqzU}l=F;*~#cTTSIy@*HG^F^%;2~3^B+(CIB}C#d1cfsC4I@tUHT1+w znKvpbU!*w)e@XXrEvG0D`yk*C?=c%NKm;Yzh00+?{6*fRVJ68THS4&~1#h02}lul+& zliq2eoqdgx8B@Zz(7+V4+eoia%N@E&ZfzxpqIA9UBY_VAy-N*Rm<04c60Uqi+raz3 zuLS3ZNQn$-RzB%n$Hvend*@<)F0)Qo^f2Ojay5AY3{|=MCXJ}GDMYcG2QMZEoIJDh za!!1#FXiPw0nGIrQUeCDf*DIyE>XKRuwQ-hjCh&BDSZ9)e7*Gm7w%gD9`u$Q9Lg^%prByW`&!K{ zW^kfOAJ^6`t1V7#1oybmx2qZaq|RdI%PR}soWGe_7e7apJWy9zoluNIMB|g;#aHu3 z1ShGhC4JVUdPhtn%ImTJ)$Ua|>*t#9&f_+TuN&BzP!2BI*Rks1`HtuPBb>79bIP+B zxTttUc*2HhgNHj2v2yIyvMAs~f-eRJM9)_jth{YpPXhR0f?y1MpnYzBk6tQH4{@rG z#`==&SplECZmB_o5q0I4VLN(XdPX&e6BLQefnrm=I#U1GVQiO`Oihp0%7dJRw?WNg z^19#oc_NxLaAe;{c-uM2W_EoAV+d&&2dLaG)k4v_3D0x5REkrT`b5DMj^4h7Jd?Wk zm^wPi}F$A%IArCGZ9Jl$&QbQ4m7OmkX_01j3a9GMxHq0cNAzJi30 zu1E8JgpR(xzMyTiwGoCz=i%W|nGqikMfUOM%;Y3M7SXIg1kfT5iZEIh4HZpEc)Cn= zH+VE}9dI0W_kx0g01pcZ-;~}aE?zq&>+I@!uBmQP3C9An+OrF*U^VBMI3MK)f(}74 zezVAJQJ>UL0@b$kbe{VWYKt)Ob7-}`2FG1;pxt?HNOhJO0tk)qeSva@ziMPuLPA1~ z)trGml~6cqxZrXy49eO5pU0)MV{i>n@)`jLgT4z4H#!M~eD3)(3AfNOPFcKNPAiFp z-A=i(Zjce%=1N;{4@$t2N2Q1{vgp;k$;CKO+xdV@texFyZxoSQoe&k9D3ctwU_E$lcD$_nTD>gRP zW~mk<#HP~w&c&^a-+nFxqu`k@6Fck10(%LZq%+`NZ0`-g)9P9;S<*K_Z<9j0sIk`( z3_70(3j!jp01|4l+~{n|%9X;J3lr2MApueM!)%SzZBp zNK=i2_Enis9Ys+8sY)nuNv^Ke-)RZ`Hs=-tvB-eiFugZR^9wW8@475gkH=$;I6JC? zr%OwVLtJP!+y9A`MWyubpp@8A*8Oa@U6>3)i5juHLZ2674@E-#7G>AWIoWFg=O5&G zH?masL7y#Y2t*|&#N{@CU1nM_E!DipQ`CBA`;-0qw5wXwhjD(~a}qhQ7uoj`#MV?>HJhJZ&&xdG(Gfk-8|_~f_gmv6 zWM~L{5~Y;#e+0k#a`W@=(P(K>_Fuk2tkj{$zUklZAM(DMd@KS7c)c$xvmigTm~2Hu zK})*>j;#TZK=>toOsqZw#Do-_oS3u3$3wDK#~=(*Bd%tw8pXs!bknD)t&Kp_;C^m4 z&+weuI~E3184MAHpdjJdw-XTh-CRhD;qT5P8m*`V)T_i-FQ5;)hIbd|qX(chkjzmq z)|kEyomQ~_7~Lw3DDa`W=JBEqFUc@O^Bm0p(3a39`$y$BopYmfY%I$jci@{8cjZ%i zT=)_xy-t1WzL3}ki=%UR)xyUSGpU;np)HZ`u}ZX3+Ww;*nidxAQ8G+W&CKu!*Yl=T z;(_8r0%DTh*%lMI^?s0OVJ~LLVO}9_AtDkDie+La*xjo9K`%~ES;%9kef;xh@Z0WO zC-9&%>0@qT(-H5#gP+c+$CH*@>F#ZC%1;jYogN!+(eTS3*(n$V`NWiR)R@x1jx4u& zjD701@&0mE4skmSJ_bxjf>DT0e=dCe%r65ERcQDl?!@PJQ(0LV_xwnO;odB_31Z#yk)Cg^h1~7SNEPSn z{TO~12@GTe)Ib(9&a^X0`4>0h5dA{BfflDt!f5)h4G2hab7tUSMCjT7V53ZMR<%~5 zW@fAI@t307PELmZZqr`VRTuTLEQNrfm2z&Hmzt(`--+c#di4=Xen-t5w>`J)NiYM4rEd7B`O!e~OWao(}(28pJa_#1}*kGYRl zHRdhHd>H|k3l}T3gVH>wdjGhNbybRW!J0M6C+Cwk$4NX^xxx0Ul_|JH#O+?|j!@RZ zdKDc*4%2Zu>D~`I%{mnEo+tn!>q9)Z)H&>-ph?Jzx65_O zHHLyLJR__*IU0Cm-*TUmCCwiQHFUGH)(yhm)Y`;SO%_ zLdYBIh8(C5 zweu)c8N}r9aA;Y3?k8P&t8MV_HP3sVW|<8M?H+K=x2{$n0Y6Ah%)-BD8E4s!M+TVv*g95rTCIFlvbu zC0l#OgkG@92o*%u1DnX5hx~i#uwNY8tzhUF`XzPugdE1sm&Qgy7)%?c(~fcFb*7T< zWX!10bmJ*%W@fbaQr6-_V7pPbawG1`+%C*4u_<+_WGX5rY!Ycv;^)sf$FOKrmd*T> z*i@N9b3Zoh?BE{^=`9vPpBo_Ef0W;q>o@%bZNB9uSD_{-^uBRA;mzm5=_6>?6UD4s zRkXlPAbr5XLl?LQ=<5^k%szl0MBcJ$RjvV~{L&)Lj^yf1ODus=Brl*4~{bLhc{eH|8U`$hgMVv zw`snagIBEn6FsYUiEe9NVraLvwmDH@NW@sDfG3zli|EM#a(bOoje8T_8ZUckH+ll3 zri>4^$L((l94<o!-~gA*~nzDDA#*=1(Y%_!5d! z`)?=>!oRYz^GkvwM#m?LfU);3M@1DX2cA^Y04I z3QCiiQz7WPAnAfPr~Y$H?ADOatFgFYN3W-+Y-o0XdQzkzgfgIu6N^qPbwi=>q^j;} zNvvAuEoq1@QR$8m(LnOKX5`~tX}unQLUp`LN%~xZ%pFeH3;fzl)}G+y2cHYVx@HW5?*3kSEhgwLENZD zi}9%k)&@4nUF(ObBe=Wd*h0ucA6BCb;6N0#7@Yg@vCtz&8I}!F-8|l85JGSPxj3dU zQ&8xrOwYZ6($7Pf2di#e1P^OviNY zuHQd)jGg5y5qPdl+bqY{I77w2He^%0)2aGa)vJ!a2n!ojVH}cMHb1wL*PFl%{pTBv3;NhvV9?7QQ8vS1cc$?;3!O*5hbU+ zkYRuOC^=!O-5!baWG7IP@awyY8`<0pG9IHI=yd9fOL&B2&hqujs_ z-82sLcTo28gPVGZ?%^!D{81YY0=6UU_*pRx7;`X{r8Ujuf+Y3*jv~SFIx!-7Jf$y& ztgrWZWWaO%mCYL=2k*vfJqYg$V|LxL+4di!>t8JV97G>g<}NThRC)-G?fzJXK`;O{0C=peYbRM&$B08mITxNu!89#rqmknE|xTY;gv*z1KqOsqcrUQK&Vap zO{iUd`Xg%er`G-o7y6aayEn`EG$rd08|o`6>N9FWY=RSJbsg$+#WhPwAISYNakWWU zs2swWMLDmA>O}YhuGY>RJe6V#Yhw5WNv8mRqrf`J&{D@QIS0e-*5NdS>}NRXat@ZagiFuN8ma1rQxa zEni)uW3V7dB3|on$h}R5f8uCCC9GNqTjA$Sjnz8{H%nWp9^k~+BvWfpW5glcfAdE6 zCCA4DI7a-!&78MzQ6YqMjJcye+1Imk79w+YjV}*t3L!g{zN{B|HK|81sH&>U%O}A_ z^6U2%(H;!Q28ekxEq@M#)?O4OeA;z4cU3tK<|&V(%DQ&ge7gT)x*T;1Mb&n--A*!h zc_Nl4=9+EMv3;i8JToY7w!O9Nh`Wdh(IM{np))jC&GC3ZK$Tn!Ol5(bxcGc`Y%EJ` zRr57{VWxgu_RsMZi6Ji1bZ`N;iQxYVo+lJ89I?~hZ04q!pU+E#ZTxFR>n)>wKcl7U zYp9W9W9(O!EiWx+RmYLPzZ=5khH8*$XSfGRUnieHrckUlw=+uU^&xPSD+1%J{emoD z8;N_Kpw$6^bzX3tmh78(#a8@%H}`UW;}AC%h&jqr+(cvNvR8 zq6Ea)=y+H`3HJF#=a^?K-?s;*U@AFq5VMw>yFO#GkFR*J5F_(3(;x6X6G%U>_r?m|oSYXq6G3xBkSh@cV@T3R3l0?^y_ z5HeA4=C34F$#~w&56hMDx!6-BgqWF1O(ooI#wbMGypgn-x4LCVRGE>w#7O)=$jQy^ z6qM0X{I?3}6IRB&cDPz|vJ1No z1GJCRmEO6z;d=_0`}$c2z`(VkzCuxA5!0sG?3z1=Y{Lk7?8Ivo52CzRy1nSo>ES_O#oO?C*WgEF(8uCAizfnbOF?T=m2-$`xvJqrPNVyOMd_S`?}b=<$p z9#U(3%0m%H6KwAZeC76QYXE4SJ_!s3(j{guBT+2jvZcNv77^e%P<0mlv2l8eo;$8@ z(k;crP@xn<-u?adi0`?N-bxoncFR zJ%}@%CsN21tO*P^Al)~@_SK7yUDObwNE^wTqm8V*OWB&=ctDl+*H8+L34=$%h{_b^ z5a1Yn<2**H(p7tXsnkg#4uMD_dwCn$f4XJrYguXDKD%JmQ}V%XOV&&LLji@GKyDFs zPB;C+4Wfxsy;?5eW}IIMRfbZ)+zdtCmbJDN4(~JhCMs7m`WW}lG=%M81%*H+^&3!O z!d_CDc*9);1awImK-DlBCnkZAQ`d!KGwJn3ej55GY@FWuoEc@Lu>1H!kKYd%d z5;TqYQiL9_bw==7W$-t10q8agQ#YD&+ClDTSAl^#01*}>8ER3vR(aMZ_Bhd?|a$r!<3t8E_L5grzClYHWEdwY0Za|~rjUmnWBn%G^ z0|Ll2@pkF4T9C9^lEj%c1TG@ML`?|NgPLgf*WP5{8*6qS>n?~gIoS{5daiL zh=p8`;X2n(Eeu^V#$gEkoLCLhF_hPf3Nj^OPXkWDVJL(D)@}0vBu5_Z0P!@)RIvsJ zy%4+*TI#VFU{dAz+xABs*cVurwlF+vXszrEB#|sqgPa?+z%)ZIwz8;QEIqTAoG2}~ zfkz(i*grTX0&%1{OCE=i64*Y3F`3&vcea02v&aMOx&})QIwb?v3E6ZT#|ez*G6Uqp z?41@6;D>I5&E`IYRnCS#?u9kSuC?#=+wIYB#OlgXi;C)KJxEf@kfXkQbBZIt%3EBV zPg+hw%sZd_jUx_A#A+jXHi-8I8I6bna{YS*Ng4Sm5ojf2Zki;Sn%@dorPCU4ckbUy zw)o6DFCb{8y2V5z7G$J>P0A&qrqek1E}Ya@l+@kp`oi1ZoyOap!v4yGRF5c`mzvo3 zf!Tj2wA^Yh?es$~{%&YNTlSzWGhX-Z4x)^@OOrMAP5iMQJFK|Nbf_~TKoG4#)7AoS zW+y}Ng%3500cVF2X%=R-jz?;OMG?DZ9ctK{E<<$RyV4AT>nalha$J{>hb zZ1s2%Z`WS(1@8G9=}-j@bKBMx|5?eRd|VYUi9+16)nk5twDU~V!Im_kt@{UQl?bv^axA3PKr#e4?U{qe3T6BXrI7rpT#- z`%`Wsj0A8#nRxeeT>&n`5W@2O>D*$cjCD3kuKMT81J5PpqMmdky_tA?p=^uJ0iHojasy^j3;yT>SrF-s< z*byP!E#gX`jr(pPA(#Psz83dpoQ}x>w}HmtTb_S80>q#FGFz%Wm;Y2R^AP(Y7Qy=Y zpZT)uOF9RnM$f4OP_2pteB0US+0=Iq1WH~C)?R_2_p0^+ zdF1v9i-|+HQUtexaZ~1_Xhf0y1zg=m+{eqlrib}Ff@Q(MN6Wt0oX)jnQWll5$|7X> zq)nTKK<2BkoH$IwB@io>8x(_@yeOL6)jZIi*5dQ7Dj5xjxWc-)i#Tk%DD zVQvMHq^*Tk7G_%LFRDbjI<{5#4wthXlT6k2-&I#c&8rV+k-V~gE?33-a z)~%JG(afx9bSq_#A+;Fs-$shx?Na^`?^)TRi64tjgxg@#BNNpbAyTqs%4mx?H(IqA z_RjyI+HMXxTAg*9bSjpRx%ox%%4aE|m~;8M#lZJQ(+;D~W^cWOEY{YU1fkx0$(lAH z#xB}6?svUEKp6Fu=sp`m!m-Y@^QjPvxJyOSldV@C0T(04xSn+XrufF)a zC&CYu-z#aE9IC6o4v^ZIID8S4)11(cV>+=#*Ve%6C-sr&n3;=yDV8k!{fAuHxPa10 z%bHEY`_sYdgPZ1Sc`UTt3X>bn0A+B8uzn-uCAb@q{q#aVi+SF)|6_Gu=^;fWn~zKR z>Fk;d6%iM|Q)OEl**1##y3PS@pD8p2Ob#UJmqRq-3!J~QYF+{VmQ};Xu3p%1=ERxf zc0CrUXJ=YIv=B4|pxphbYJ`JQEZkPNFK@bA=A|^jnD!k6j!f=A;1f$GR?gy)OF#$w zOV9167BiM8?`D}^hTg_+-CTG-wmth;Rj%MG+B~5F~z)97b z`9C+-a{RX)$orV{)2+Dq%(BUe=^iy@e%)XFD$UOAMdy_0zous2^goRs?uJ>P2?3y# z$*dy|%=(s4U6C+#lr*JFS`4*#!9uHZ3)>P(gC=92iU#_vUHLRtdg}XO0z*A}C48GvdT%vhwYGb3p=zl%=xTe?V;< zHG#o#^KK5bNyx~t`)~jXqRROT3aZ6sFo`wt*8KY};3r1*wqWperm;c68a3B)el{P{ z3iOOs~&|1@ZD6; z;n3~IkOz_cetkh!=1K`c8F8$ zbMi|R;4u1=K1^l?_NYJq@A)Tsc@WQW@goU>cNexO{(AF~DH5FQ{Ks5`pc{kt{~r)% zqJUrNqK2Avz&(e-`OwwVz^?ta*s?|$6R>EoU3|~ZtAAGs`tO`!CN7XXZdE5)MX5OF z&WHS*%NY`Gopppvv3@*$S;nd0BX#}!(f%KWVlloa|2;HkU-ri{Ql=9J%(QYU9u}rl zvaU?ef;6&9UeDfi(|IM3SJU#36J%9kkmDS1N_-bh#mV9Ki2TNpkYG-r+x4)Zi3?KWCb8f7U%q2BW%5}H(4;7gD;cGvT5bAv$c=r-pr9hU zFBESqg!W~y+j8D#vaXhwA&6{18yj(|gO`QXQX~dOJUB=2`SxdTh~gjxeCIY&-5|5= z8|QxLt2w)B*hXoD%tF6;4kB|Pfq&+os;{WP4tzM=h_BVvSSO~m^z`(5KY!9rDFR|c z9TZ|LFYk-X%1~C7Es{Xk?%=?(!Z1qA8&GhG1E!RbC4as>1R=IPR=0rH94YG1^!&!+ zxfn#I#owNjMHEkOXR*QOD{T*W{##jDQLa;GRF{AI7oct>;Bvj%3?&5@5GK!hd!VE;27p}KX(!UNh2;S;=O;K04UUcVT>&rj z6DSBO6-?c%!i+6G8z8~L#qG>1U?Tu`3b%saD=sKt3_Xu|j!J+A?5y#y{BGct4<;t2 zB)%qc$!CroOmuxjh7^&NFHC|kb3ENk`76MD>KGn9z`f<$-tLMPs$EuQ#qoDn9D*+Uhr>xzfdSb*B)b6V1$0)?eb(}|oH zpo4iTg(mXdW{Fo#{OK&l_tfXkF;_?X_3MqxlC7;R@ZNEfnO^(cM*V2`7?VNfNQCy& zOC*Bbj0uP@gE_7j#(b{6^^tmlNj8O?%kkoiwqI}Bz5Fo2uNutj*{+X=pb1=qdzO$G zB0k$ZvTRt;xCPrV>hO}B`u6}ctQ#>C^1gL)cfT<~wY0Jd>)P&uf+q;B5Cxlupt)B8 zTq-Vo^zepFYYyv?)R+T)P;fx)@&xK{aAW$$#@Tb5xAISbQn?2~n>!G}lu#xUcB=%w z95RGWOZI_Oo*yeeS9}sBvuBFm!z2D~Cnr=1p0Rvq^l|ji;MO=GbTdIAh)Pt#DSiTw znDlXxRi1WpaB!CnK}r-!O<#>TQ~WfTzQd%Bd*xV*@v1@@`1%SYVeA2!Q-QtOBLU&W@S& z^xRxm>E6+iv^O_6=Nv6mgBs;%f4|$^aUIxzwJp<#7<@Ljsl@fjC@2gr)>!R}NmwYW ziL8HE^Hp$h;Vs@7O>ZyZ#zzhd3p0J)Y`>0{7H_M7qAgCWD2?804f6tW5$acDs;aTE zaoviCm8K@%8NHERnlXMRzdd9R>{slsG)#))H|UZM4e2h)KdRm#DG);bK3R>yyaUIw z6?bwq$oEC0Hx=0v^*W)?qovZ$>1nmp#nV$euuTGBs%R3SSmw(qmEw#X49-ii|3yMV za>}*N)JV%?HW5_bWx$^s8Vc{#47!U(zt_|?J!6^9QdD(CdF1VmV z-%et1aB$ou;KxP@h@qV1r`f)yZ+0#El_*NO)ZgC^^h(}I_m#Dk_aR6|1!Ph=^cM?_?bA9px_=#8uPN3WA!n zP0b?6tAMc>#-6Y1hFkMPy0fSG?|~oH^%lCkw^ok}HNVk!|B#K0i5SpM=gEvXQ2+zB z&$XzHWv?0W%)Uxwkn(#$Hm!a)L1LW@+6Zq9&L57PzL=)wOvC3yVp0n2&8{W?|Bykn z>Mh_-_5q5yo7$gDRUP<&gxyy*0-w;!Dpi+_#q{3lVzVKtMC%ckba6S=PD(@Dq1h0= zNB1MUz9O))%sZX`TsyCOcdZ*29UMUZeL;3+JvP)zXv~~1GS6uD#y7n(Pa2+~oi32D z*(Z)#X=rzD@iY~W0<*e@A3sulVs0+Pm0q0tog_gHZqxgzd{Bp$)jb^mN30`l?K!Iu7=(y8NS>gwvAS4R@O z%N5$5bHFM;H@7+?MJx_8ZIWs*oDk9>1 zrP&K96Ge&jp1f}Ud0PZ~j)AZ>mF(x8JZnWrH z#9`mQIui1S=KG-aBDZ6|xZLJcN4%-&?({hPon)0=od)Yrba>>N`&~-)3v_}0KZNVT ziwWt+00$+>dqv709$;pccOCw3fFe9scim0K^w^=cl%_p{F9Ibphf-VxMqhxq?dR(DcDJT7mBT7`=vaRE(h|uGgMOwg4rW#$k*pS&>@GHCt4ZSH1ym2S*g=0->R2YPm_- z29c)sKIPD!KJ8!yPjvEK&GWsBF61tTl_->%|2P>yel&6JiDU!I#dB6~gVfV;pxRSf zzkpgqR=zo&|7OuTL_HK9J%v7MQc?bRvHPwq4x(xS;AFh*(%yz@>WOm)umOl)2g2s~|Qnzi1xL z6eWNXq6eOet7yy4U`?bmJgc8%GBRmjNU4m;e02v_oKxhm+<*b_O}7q^io%z6&MLl9 zj(;%7x#qQV`rD>oKu@82uHIZL`m$axBz+b+M&{PKFkEt+8=N1BDf9W6TU^)r?Zl5y z)O*sbj^bYOE+uRAH0Jlv-VJTFNLxxjWi86imL;mSF{x+*Cm@f?m*L!?D0As!(W#PR=7 zG(}zr2nA0u&(cQ)9%zbbJZ^kKesnm?EAR|MvXygghsD@vA>$6TD9=jq1A56yfhFnm zU4OV(LRSM;e!Yv89-X4Yud+wUjLRVQ&aHecb~l4XlDp!zIIo#gm|406qj*Qc z55f(j;|%NHx8KB@$n}K0mgc7sCH9#9_R4kmLLO}tbQM%o%lNpgR3bdF3v=nIs%z>P zz%XGxci)_6uZ!#RASqFnJRT~oD9OCs&@;zI5BY@7caAU=_+UdW&;uXYTG^%<-yJkV z>zzu--uH)A-uB|ABmMW?H$+GJv%l{G{%|7;z|NugI@4V8-ln(ECK`WM_G}&#&RsSk}bFO>W_RRXkrw zFT7I_em+E4Y|U$BdDMKl!1Q&4fK?8s%wJLwxy5V9Qt)V`#Y+mWLfyf>Lz16zi#lvW z3c3$1=V$wiC4D9~^Ceq4_2a9f>o_>DzjGHnU>Kuqgkrh%BPyb2JQg0L-accFvM8hT zU7j|m*KXM)wLGd@llHQsE+|F?*Cr3uxtI8#-M=p+0+At%i&%1ERiXKk&4hMoThR$Yu zD{e4^+v#oY{SJPo!df<6*m#_;X=-{K4u@rFJ#;m#>MaY-<9}_*Vdn7iJLm@CCeUyX zz;rmI`VuW>37m!hWvy(3D z)_`-`BY_zUeS;p|w$~~7yr(M$?y?K;Lg?Uezj8r}NI>5%1Z7@7?-4Vq7+ZHz1<|1e zJ;Uv}+95Il`O5&!Pu`c(lC+e3{kZz{K(=U=>ApkIy$XbnT##F5z<{ZUk7e9B!xLcQ zVpb-jKR0g5ol344j6#)(py2b)+53Z`p{DDH(G8e#&zV`C4G{*8&pzF(?-&xU}us;r7Mz7IoHph z)FJTzOpM|%>#JSCJVR=ty(c$f2~bP z8=*~&X(@+iIjF41U(M;rpx2%Iq9J57WB!{%#9VvKc}&fflWo7jj2O3FGyAI&wSsQy zV*zh?ds|&!Z+rdi$NStT=dOa1RI5>Sy&sL%8A_DP@t*XY{LEbT!J4M5wpB$GgqiqtXIll0!z^BUFhnhBg8wMsReW>`;LiLKnjg{$b}MlO@=L4~t7&iVoU zw_GfS)T_5ppDaCxipY`QAauR7EACz8Y{&jAW0V6kuQN|qcZ)(H>&E2=JXV(`qOi(t5Zv8JwarzcUcR! z6EtV@Xz2uflj7iyiau2_-v8*#^XK+|XU?9=ik(!U+cIp$5V8bIAQL63NtmX)eqCl* z9T~~|b2y@hvfcocgZpJ171N&jGxV2BG?^2Jz7{BFl@!}(B_xVYRA0SNGp$#nMRBn2 zYWbU^E4tvUclnc}yF1~@Bl~%LC=2^Gp7rU~FXyusNQB0xHH3%CH5s%2*wEt>g30z} z#n$g`(#bWbw~$6N$<5Ely{&91>muMccB|rlndm9eW<%rqZLyyD&CK%dhX-QY((R#w zg!)r_`tLtJLGE#Q4J!}m)ma3Ep0nl-5tWdWYYyXi;K5lqx%4Ug&h)Efn}c-Qmk_CUT1d zBCw?0E)Q-%PpAv3-hG`?u48$(l(Au)VcKX%HU^OzkLWngFa6-tGF%545p%e+R>S~? zo()`^11SMFyl9>yrYim?Mm(mb(w^uKSkOtYL&>omttPv74~|guYk>&pOC0?>fAc~< z!PcGqw07=cts$df{nIRrs#D;#eg#<417D}^iBB|&ThaS_%!O+B$fHFQ_h<+Z;5wk2 z<0VX)LXK!uqbN6p3%GTAj|%)(zkmj~CNFP`5mEAVCULdta74XLSYYF;v=vrk2wUAd z*8^P>r!WK8$mwo|uLsM&@vwTap-j+ej=_A@sg|uN5yBjzq_kApsuOHaydo|`X79MC z7Bc&m&W#xcT}D(?R8*o7h5S;|-31j)RNN3VJr0V1FF0K&13B2g5Bm6oeXp=RciE!I z=)49g-D_hHDfQDQ|ETr#^~FV<+fsK{(RI3qJ~Cfo1tQdJE?WCx1;jGN3IUTzrTSjj z7Qg>MyvZR1w7?34+$!0wG&J9Lk}UB}p7!QzSY%HpK+j1xdfw_R3DwzXn{y&QeBI(C zm?notcw1ZR3}#7M{;?uz5U50v=nhsItj& zRZn)?J&G8E6qkVFo(QP;Ep2VjY2Y8yYuF&6oZT8|Kr$!RK+41WqjEqsDJie`PEC#L z1T%!-O%xHq`vEtQNB8!+Hnmd`*HcQ!ia5a`=3}8z%~lHxf{u_T&f`aU^-|v2>Lf42 z!JMK{eG}T7CtTv!1gq44qi^$nlkjh8KXVs<^#5#cD;m~q5}ak$ld1cgaT{7A{a-L{ z|62Al)nvBKiB=TV+b0EWu-|cr05*7#k)j3|H{Iv6`=#;7eo(KZq!Zs z_!a!l&M+YL2f>(B`4-qeq6gnvTQeiK02-wXc%Ng&vZXqqt)09uEPL5435kdv_^B(g zR2zN(CHK|w()!KT?rvA>$K8*qhlY>8k`kFTI~|TrPC`!B8}}C0yFxrYJ&o;Snf}Q6 z*_7tf2#Rbsgr)YZRuj>n*?c`otk?s|-_CxTu|mi!n1y3WsRHlu3Y@@4Ytye6 z&OSVDUp+_+ssJQTd`W!S`+7a(w92SyJ%4jH7sY=upd1GI5@jzQaCrTlT;`+K3g|&t z`$lYn+sG-9MhK(=4$!PL-+-02 zY(vmw_4DV?yIH=ZXgk#DuY&3Rt!sE5mP=>;wzIH?U)2x|4ekBm9(M|hLr65^Kae>6 zE^*+%%6#|edo>Cv9KC6~S>4BMCNspTYvyv)ZEP{ci^BW0BANGH{`Z#@HiHa#*tU~- z5Onr$%|5dbbPq=sY%ZTH>+d`H3(42MID7hi^)wp+rpyebyFO`Mtm|a*PG3D?K!6dmXwaIU^cFO;bv`83R8N=D8IeSJ zW7lDQQ4v9~xtf#dT5rzr@&k-*O*j&7#J&sI$B*`bVyDb|+GpRx9^go=?zyU$s~KSL zMTW3*o;Fbu)X^q%|JWL;OUsLr=f1;{?gTq|sZREFbB>x6i^H{*S!lCstgLg~=?59}Vl0SIz?jhHoqux88cdMGI>=%HGhz4!?O+o) zAwP%7lBcDQ!9rb9?mKhyq{PG)Q2HGYfgoJ$c=E$L`!&SIRYWxlCw487(LGzHyM7LA z^6By{s@vMorl3=;THJf?nNkLk)Kj*a>T~uGJ&;A}cAe5JCQj*N)%9VG zfcJ71WeLW4QKE5Nw*2OB4Atj`tfB2Sqgs~E70c^@c(|W%@RB!IMxRE{baY$!=KW!i zii?ZM!;|w2zT9r{Q=Oo@xq`8W4_M-B_L;jTNJ;$=+T3crz?r;NNMJ9RaSB^!y|A%# z;2>Q06Ij6f#zrxI5B|%LNL^C^>Bh^ff-*&k1yNy=>YEBx|MisSHqr zN7qHdB{gHNi-4oMj*JRR=*!$cKDM%a-=j$@cv90JoNn%m_aqW*KodQeL!f9Yfv&UO zc%{>4P22cjRM!m2bNy{edi7cOhNW#=y$`+&oob51HY$7?=P&v$Y9Kd;NkPTw3}K$(}N;1^dzS zX^C$bZg1;z8l6?rB>1kJ(BBUh=) z{Er{V-$W*4Dy9Em{kjJVmg|oJeRU`3LJS>_@deERrGsLttFFAv+@|aIkF`m7NQwX* zy_7P~3gjJ`wp>gmYK!bZ|1(fT(u}x8i7GD z=J_;r-@l)%_(D^}VHj7D?Xmqzk5q$}Wa2Esd8cEEnYN}KuzVNYUvsyM6#mV6rLx$U zd>kWYVcuM1Rt)DpSJ3yHR_kaztsD!Zq;=EA$wphBct>LoWAb;Cr$e69NIQ5a>5q56 zw>_zXVMM*gPGixmkF$DsXf!sXd=FtG+DvU&RZ$oVPW}&PZyi)+`1boM9TL(hA>G{| z-7PKMUD6>P(jC$zNT+mzbVzrnq;!ebDwB4l`$qgt+~E*u}z- z!KoULktYduCL13k?X#G_a*e)t>XKED)l;VR$ckta32f@QlNB-8>3K+_p_B}bB}!A< z8;-%+iqu4j`K(Pi8czEXN&M8F4`ZFsHd7Rqd!mGUIoa&OepSx$7_7*2Cz2eC*UG8N}bg1+5-ZT20= zhP~X2TFMwFK@G#a9pTYq+j+^0n2EytcET$^YQG%Xf2v?LfSmE2t?*1d75oX(%q!kzC*XSJI4P_Rwv8{Q{x50AB7 zw;@bL7*vZIt9) z40rjnI_K)LGTcRxZ%>ECr9PqyJtRuuiHu5rS@y=v4_q)KUEgtQCC+*NT9SQoZ29LT z^xQkFJ)#(|O&zsf=%eo)E}jVb8DmRJi}G<;Y{bNf+Fj~e(-@2BREgTK(RMtHw*0)E zw!;;BDztOed~BZn2CBni{SV8ug-I-v7%-!97>;@nY7sE04|)O^4*tcvMtyEsBd?u) zKP3$N9UU*&EU&1HPrr&ttNHqk9H>5^#K6NAvsg-b9!Qpo&?@K6SPo$FIL{~_3isw9 zRDAj3hz=tXE^e7Xc-&vpa%lTK=oKkp6e&W_yTs)t6u6dmoIHw#oM{R_$g(`b%kbL2 z;B%Hwb8YJ^@(OOHQEvYD)}(@M>*kJ?esWKZ5Qs;OA396fPW|gd0xBca?*k7FYV?1< z-?V@Vo``%B%iH+{Mwi79(pC9yRQ?3V2bknzD>JXC3(V_U)z57w-ie_1@zj)%&%^jN z6`p~!&I~w(!EQovxHuF^beK&NjNrcGx5J~Hw@4?xff57XzAoF<@<{-2XHq~tS|T}9 zbYRA_ExxBt-^qRr*h zoIo43vU}4*oH*2qUs+8`)bbgIOaxp^^l2zWQRD9zg(g%e^H#V^<@t0$w80ZYgfH7j zL^5?JdT%GVd0a%oQC>VzhTKMZb%o(BWOfQy~PBA_S(1#F0{%v&zCEvpX8W z?KwswjWJda<;+CCm)-j4wNGf80HkSCtkH5a#nZg7PiZu1qGL(2UpUgi|0P8*-V+Io z^I(4A`L&(y%&dfg5@ksS^08ntC}StY@Hy8_}!)2#i4GJsYeffuhve*%{Ka9o_N>8sXXuy8}V?5QBE@4y4>@mX~>H z1ga9C-Ogd5+UWOIKhG6N4ud4HoP7nfjxpv%5Deg_bG+r8PEJmMU5I4MVYLN2XRZ9x zF1-Q1XL%=BvDs4P{W`5-3ncxDR-}6Pd$jhqXmLXNM_(wp+xQRdWET-<`-8&F@}(nv zNJ99KYbOs6aT5Xg)pM-hlga(XF2of9h#V|T%pg~XU|a*A-&yW!GBPs1L)(DR^1`n& zjN}s!7uOBMyENCJ<8UKNMav}=T1CGb(mj&m}`tn;@$^oXbV6Aro0x{34&Z#KTZfjGRWTeoQn`4r2Bx_ z;hfY|Tr8|$%bdGD>Nu);%DFu0*LR@%FhYhLvp;!zAQm1Te%mx&D43>lg1h5)^n~x} z$2F5b;2R2)YzNZJ1$j|3Rzh%tE` zop)EqBop(y3CDWE8iB0c`Hq_gNbXrjhpw@4ew#6nZX|95d2*8psao3F3OH`~6OnWG z>oz$W0ZOb|417u;=z$pzU9}I}T5587t46{FY&a+0>yK}+s9?y1oUp`&J(QFtKyGh8 z5T?95nlwL0bU#~*zIH^sM<1L82LyQ!Z6U0M{u9d%oy1d$`T>%5R#x3I?LgV1t*s5& zien?e63J0KgWWhvAo}0h>8UAuU@0gmp*gWAP3 zod!yLzP>)17poh3B;N!2chG+kx_bhe2G~uP0n2uV5+FY|IC!^-=9dP1N+#%E$uHuv zv&SjSA}f@GoaKYJ#lelX!PJu|ezS89XW+?Q$ejr(@Hlv@ylB4#w=rr*zE6xa&iCxLZ}s(9 zfs=&nz+02U$;s(R)gEy70NGrD!x2JrXh=e~J6{&W)v_=e1VhH!`un_me&``HkOyru z$)#DH{WakHQxP*?Nzv# zFw&>%G6T+Z`MWRa>Gz=01o(sXn5ZZsB`#Bd_ia#$`h%1PdhS&h%p`~zq9hC=RfeWU zf0$mbRNsR{atqQl(q2PZMZ^Iu6!2oSJJRO(Tns_@A~2Q(AK_tOOo3>r3=*F&W9AOh zU__OA_wW-eu(x!5rhPzJ_%C=VWTy-=1#KZCcUr|cl+77ti|uHxkq)-S6x(bP4XTJC zQ?`iTcP-eM8prLcLD6hPu5Ke-UE7pgBf7W6?^&!#~p1{O!v zcm~Z~L{JumnEfWG6aPpjZjO7ub#Mb8GIG<9_! z0qgX#maJg<5Jv`NBbb4|%DT#>=Y+9<2>~{qQnf#9@RB49{0CZ#idU*1K0JbZ8jN0@ zBq^65EeCPn&xpY@dDvTOxfF(g{6`R*1Bvhu@G$8yG!=S7^{NZPRicJ%gKw7pVlNtQ z=mF9P1DZZ{YK&ns;+3-mLFdEA+Fz(IQw-}vkmn*3vuo64abHMAHewAV$=&~`OY&@ziOc4 zLen#N0+exvGf0`=UBt)w0QFDTezgEZAx2<@#x?{S!h5Lx`65C8~b0nm?aiI~7q zp82pZ5LxYeuU{ zF=KLpQ9Qla-UZJi$lmA%MdZ+^O}#pUS1?0fSz+T#rVXY?&md%y`a|CnC+w+;Xhd4= zhrbZ)N&to-uy|m~%{^_q9H-9qFa{3j^I_63$XFljC*DJN0(O=j;^KV;u|mUC(u#_x zrWm@dz)$}7=-}WEw2~)~2Pi<&SC^NU&60PS2`?;}q&GoG-j{yk0AEclEt-jNk9j?J z(6BfnN$x^bE?3_54v5ckeGX>JGl(Bevubp~W) z6~Gc9g&qtZj5DCGqN3PZ0e(ox3p;Py&$=T7oY`Dwzx?-Ez|J7ZtI2z}y9#kIiSCkL z?qVQ(j*T6_Cwm4@Z+a$N&wR2n^HW-z()B_3S`J){Zc7 z+(vwfQxJL+kfXPj%Y2SVhWzc_za`qJX>RVuj0iL2*e1;DezxksUue2 zZnZNA5%HW3wyPCg^K^b6@|%x>^CByc(Ar)^ltb^=q@`q`i%|jI$29|BgMr~8VoQtklIvt` zLwe~ECNTwhs%8kz8eNDmqXCaMD<-4QJ~i*jC^Yy_W3q|K6G~*;vBdQ%U*0 z6SCN&L52}&T0I5D73p#rYnYI~?z6MA)ptJ2T8cD;Tp1_{6Z}8dcS-+}+$hp^!%@i* zUBjwtYvbeM!s`uxMKeVcDpf4*-8Xv{-T@0{#BippFJr=-2n>9c%~C8@Ji8RF2JAla zKUvcQpTyAJmf?2OWk%|MZ88r}V`m4mNE8;mYn!I)^7)}T>wkH?sV&#Iz{SlZo<}ee zbJ~Dx*WM=E-4BD-k;q@JPf7Ev;wV}j@MDGytToM1=#s5fiIRg)`Ex9_fWbxiHQ@<*4 zi(TA(ftZY}yELzeaR%q=N~?h{TCt2cC9roB#=MS4xMCV|7}t}9^#*6_@zLluX52o; ziHr=#p{aAwnF|o*WL08fflwny@fM8RoA*LxDEmbOWea#4 z7&MfU)W?1mTJg=FR=7Rp!g=eP&X%(}ZVjr-<7dq*2N;#(%H{(oe^r>~5G5-}k$d#^ z1=TCso+ek?#Ien{GVM1Pd+m9q5L~FL>Q z4)#B)#Ylip)0k^BTw$z2DrSH90VU_Mj(u9S&r^a3b$$(6>I4;j4h8NHkZsObCwBtb z=73_RmAzGBT|2pjk*@7k@c7cnO;U;`PI5R(4D{J}p{S96oxQaczw} zvXWV;--e7)cx7$9!i;b#VMORz)3>oMU4LqHY0B}c+kVavBM*cp2^h$+)9&nRVp!fl z@wC#ka4oP|HQpY+{}a$0^1Z#-%cMu$-GGFRO*<&mrcM;S_An6l4inbmQg(u0&wlVR zb2KE9ea}gAS01fN>CFm{S9IuduMfR^?wdR~2kp{89-q_jN9Fka??hU@D;FGQ8c_!* z`sLfPDIRS3O-+-}^oI$m=lXN#Z*K#z=?(-3J9_)G6U}@i>pgNlnTq+rd5CzNR?`bw zPx6P(h?BDylQ2MbE$EtzspJSBFPVaaVeGW(@>{+Nai96fIz!nu76v!X?1)!LqD0s4 zcl`{^uT>wxeLRJYE>@+(+%l;~MS;_+H#$K|b9U%#B|)@btCo#{OD!+Y_fjjatf;{J zpvi!l7y?v}i}`5>G&4gv0$FJBL9e=S=$RP9F50Y?sZ31Ep$+}xUk z%Byj9=kwNKL?Ag4-p~}in40G3rBg*Kfc#K#nXDE}vW}7`)CnfS) zor%03@a`6A@$^<3diEmQAFJl-FQnPu>b!bQv0q)LWEglLQz`$xP<#@d|KILFo)D9% zk&kHnO-U`0Q(;zZ1f7rQJK;pUt3N?j!j|b#a5UWO{}h3HW@dx3Xsrj=57m=~3&yC$ zn32mT8sF5;n-nw0rRr6#XZ!OL2E$qEWEj4L!N40C`(fIkf9+l=@AGyTS~ll(c&-h=PRrkC!eTUN*#I>GE}h!Q_#_vN!iQq(`}NOuWw*=bHSJ?%eY)Rv?dE52OAZ`lrfgxf!~=2t;KlYJ*~FZx|2_=e(`#fG)Ml& z$O&vU?AxVsnV+9CaH{64>I>jn1S*3(sfP(mlaMHZ7x3`%bwy44z2WokAKz2XbK}4n zYQI$92J1VoO*JFeZv)%o;YuGmEsJz`?YJ=*nk>*F8M%7LR_5+y3p9K#yE=O3)$xbx zl6^inbdnx7AdA4&h+K3)DVay*Hw85Eg}U4X8TVP}hT{bq;E=Nn_qg8FxL>RmkKA*n z|2d0(vhHY*DzVz0F2Meh$BB!3C(dDjn~$E{b2t0w!UP)gax!gBas4 zX^vy{3SUxw+OlbA%rBjw4yFlx=(K=pc78S<>V8YTnPFyD_;9MOLaWG~o)6lDyFhmZ)guK_$$HpaO zY$U$M#owQrHt{_f(_3~wUadJYeBnK|t}VuMk1DwnIdJj=v8VDX^p5|i@-!QSJQhXn zMa#ofW^rKUq4N-u-X4eCHR1WSb4!KnhqQ#6OS=Tk;B(WGeg9sWSK7kFN|rJvz4C-t zHzD>iB~`QQO)zRyL<9qKnPrM$s)%^_aPIavPUJjq)xy_#XO+AFaa1DF+bf0k?AJLf z%ND-Z8!=NYNW~+F4lJYOv$#BVO{qIPsHs}`0m&bCL&7Gqf}NM!O!e&K{;aWw{ee9d za;*lLi@o{km+$w8hWP{fUfGNSc{!t-G?$!1E7WQJT+7IN#H@yu zJd;#sMLsjQS}lE@r(W8SK76D9QdWu2DQ}r-r;w(`Rbpu)3+82&hI28!fVpoc{Q;4W zHGX+p0cAF&3{q40-50#da8y+eH>9!EKoMwdo;wpUqwg-r==mxlVl$+fc6~X7^M2yC zA2JJrpUXkC+GF!P=yF%z>~?Mm97u>PN;s=mT+b4R49k7$y}?IkW+BjvDSjP}C#9KN zQ%Ih{7#qJm_*z?*_*-(DqJDILQe(-AkR4`MP28oW+7&lc(qmxYBzVR0t<>h1;o{

BMb*;*BWRG3YQhcFR=NLYWyTSxQtS$O2DOb~eTILVGy=)W8Gfkoc8xJUIrVkSh1W&|TQ zw|*m{1~nmkTrU4D(f6Cr_NF(PE=8vAzQ{{vX++A^yI!^ceFoPuPpaRW9LYqd?w)&R z1dUv@igpcYafglI*9%+ny)_@_Y&4PfM~+8oQS7o#U;J#Y`O2K5hC#laI~k)l>if8$ z@1vKL-EO~$6p|zJH^N$$3~*W4%ZBjj3~e0Ke+GpYf`D-*Bc(hPKD?-;rM~_f-3w@XvF`xhZ z>2oaMcd$^a_`>OmI9t*4s9ADc`HY?}6n5@a$%A(|mGtaBe^2l%-VC3>);E|J+H*2J zs{6+EYi`Ut-5nW(-7Dx%q%kojlp*vxUx-t~+I^;?Gbnl(2BD|OU&khBpU7wX2cksz zoTRT73}O6RS@EYienz1~&Y@P^@9!562@BDsq4{aec|jE;Z=CBzZ}%1P5}vlPnYk#(Dj^aU$2zayJPBro%? zJPUrL(8?Q?nM}&qyr~@w>Z<`KuGPH+%V!ZTtQ7 zw(2h(f;GPH@J_Yw*)+`d@m#0k=r}*TqWch!+i@9k@kimz`~F5)IQM z#4o$`sQ8@JQ_u8$A0@P%M8tHENpO~_E!oLEl6=FYVXciM$3j{ASBSM>2uL)R9e3_B3RNaN?~c71@- z@*+O(9X2%A%a}KYbBC-GUKF6^1Q0kPICbtcKfx0YVi;50E71+idRhL88oA8ydE(x= z(lk=_4GdO(>$IgwE{%y%WN@~sEpHpnoyW)gv!@I|ndtAFoV+iQGrr|&oo#9RuW7`` z$IE}Q%l|J>rjTeW_U7~%s!^vOHlyFAwZcbE%8&Nl{~MH%q_=6}o91!ZsfjEOQV`Mp zd}!YOn#n*!T}DnbqwU+o+PhTWshA0RJ`3mh2QTE40<(YrrmQwu3FU}S)bKaC@Tn(kg0skjbnuiz4$EJ`r>o*aK zU#tVW34`IA$kk~Zk!P*a6#HGQ$A6($H`FGd{le@~*b35~{t?M2k;k5Otk!%0KU|9r z&>8d;ZPuXMU>WKg2Nxcr7ZG75|5kqJ)5F2Ac{&w=HWpG&lIZ{FkH(c zE2wKNEFZSz8nQ-@NgOlJNaQV)GZOW&l~894jvLvfqpOHQKvSnn9!X(3mt1(h$xhmV z@mvBDrP5z~>lL}h16~{FpQ|S~77hk0?SBHbfhhUs&ox`exO9|IPL{l|e(_}WvLj_1 zOWmG9(fA~|fBx&$NZ^ON8;Nd%v6H_QwK=m_@F5BdQFPYZTi*!=HJFJs<5OzNq^2CG#Y@M#(>Zj25k$j|#Hsw-}XvvpaY z@oSUD&|z64R$7+_<$vIk%1onRqu4T5cXH7Z*K|jG;#F&f>+lRAND((*es)FzB?wxV`nZ~1kTJ9A4URP6O!cA2vV`hQL;U!|(L=w;VA{_Wmgb`Q@=+GrlS zL=n~4&0$V@8yphKkh#8ob75>`rE6O+u-%MnSr=8m!+wuj-Su@uTcB-Y{9c|*Q&k7C z8|R%%#JjsNbcqT(V(*(zM+zMS|25TPz%%*8khBD8J$a&z z%GV;Q8pLeQh4!;>TgGf=njf{Ey`|6bdXj^oH4|S5TTKs`3AdGuZUO^cBh^`~285&v zh7iJ7gkhTe3_7@twKjThU!mQ1%^7-9{uct#`Sx}}H=jUFah(F{2&Y`!6?%oYTr6RC zRGWueyBssX2A=G8FMjp#_t3YRKUBP+U1{}U?&qq!Gjb!-@&@zySI!UXf;DINSyje@ ztb9!Lw5z$ZRR;B2hHHoj5=55M3A2(FMk3Z4b|2e`7=_5aWcG?~mfLJA(~Ij{vAC?Z zWUIKKhh9g(bJKvLfPiRJ>aNu=<&dnF=Z^r#gO*r30-*nFV z=4714vG959l;xmdO}IpSu%aSku8V~zJ#b*#iBGQp_3Ku~-VL!g&S(aEIk%o65h`_k zyzPG?-$3~)HQqu{1=wHS$3<(){y+Re+3YYyS)2l6g$9}UM6`u{)JUF^;{uF@KSK51 zv0|8h86g*Cd)O@vcJCQJ+C=LLlV{K8t^g*dOjS#~>q%(dYn_U-03xPh(e8Um!qDt# zye648Nqc>wCNnk!hGj|n>(HyJSFiBi!plM<-1=d+UEPxH>C35(IS*9 zIxS=j0l$q%H|?t>l{YQ%*2+j~G@MJnVPc3l)%YgQKd6*tcTG$E`vyehnE{cYP^1cr zcjPS4WzCWig#>$DOD2A0Z9Y@2xEy6;uzyv}l<}f{n2H28ql4?4X5eDB-@oT9{1!uv z?X5Y!k>pRTv)Tz#NHW_`YVj}4-|!VB$IRW|Z~h;zoaTN*4oc-J=9SHwmS>m@e2Sc_ zt^^vz2PmQ_Uy4g>N)p~5q%AQuEij1peoI;@{A-@baVfPCZveT(cP?Z^uXdWr0@gxg z>a-)?+fNCz{~P}0g(wg{FTyj3TJ|vU+nm0Z*KsQKw~7zp!kqzU(ONz%-2Lwr4tOgv ze%l+RDCkz2BE984Ts5FRGjHk%bUdKK2>#F8AVc0pHZCTeL$_GZvMvHdvc+-Ktx()< zVfGDO3;6u?33|`+^`L|X&tmV2YcEYgh|H9KCO6Q0_3gjPf11qy$QF$MPiz4f?30Hw zOUM`6;@l#df5%BMtSW$!ID;jfsMp+Wf0bNRn3o6M_J@?OZ~Lmfl7a%U#S;*;7Zw(R z6LPK)9-aUUB4z*U z;6Rx!87DF%8rttZ(|CAfWP5v?^UG!;S@Y^CpqfE1e%B>0HwTA@z?(uAh863EEy@}8@lIlG3JzEA`=&#wb`Eu>vuP_Kf5WE_aGr3a*DUEjkY+yvRvb_A& z5lD;adH8O(3<-xAZXxetnq*&p+T*@GSso}n^z{23X|Uolt?-Px3L46ZAt4OPAsK#YHckB<)trvMQxe~Px5bM%prk%1S_tDH#4hlh#j^fKpfo6n>X zD1X273_X2VyX|PSnPYW6T4oH;JgwSI-(Fr6F21J!7$E)72G=GE?lXu^To{9Zn?Xg+owZ0gQYr) zSR$0LCV-^|FH!ODl=aSW_peC^H0QwpB1nW;6#2o zoCBsYeQXbuvQ3EfQB*^9Rmx^#UKp>N7GPbW~g$iJXYab~bw<^h=u6%thS+*en=<#xRA`pp|u z&TyLx@IE~RA*B!>1fUokeC~QlLaRYF`O^i6^CcxLc!+*^K`SL(Nt&3#_<@RE@*LD? zZXs7lBm2|Cp{9ZF^=_&`O0N?@+d+^3=u50cy$byGqVOD2P$eW^e*uKdeZ1rilatEf zG6*IVk#${sm56P!rf@$$-PGl5(m|3=tL1?^`Y#{}Sjfm;BB2zA`hw~`sMjfa4Dy)K z2tGX^3^cEUGV%g)Bh2{tuJh)wVU<`I>Lv{jpWAP9xG=Fy7DS_5%n?WpUCnd@CY9L$ zwfm`g!WVVG3lxf~4?%64myVe9^YrvIS#nL)kg4eS@5CWleYiKwwGp-~foVi)tAZMm z=zUiKe_7v5Fc31k(53UvOCc;sxbA2G6ng8Cmu(Wc<3b=7+XnzLlK$i#K(;=>wJ{2k?^bRAE(UL*pKA3*L`*AfZbrCO7$r`Lw+DQa zzPdjk+5z_!r!XQ6e0cK3CYmO)p88pU?h2pMm1yEu3hLnEo&}%3TrZ+s^9> z2a$knx^Be2v9~!J7>J80!D~0!Z=Z=ba78qnRsm3lN*B2}2+F=fgJhYEy4oT%xbn~l zqNP-%;lH7qVgL9~-GIa_vcGPpE4<)IPxICan}*t_28@L);;w_1*tXL5f&%E6IBwsh zd5Ff+<^UKgQ4kRW_yRL|oOcjl&=|6&rc`O*2VXTgD(9gqPQtCB+BP>gQ!8Z$3k0cN zxbyJnthpAxl7Ro+RTvsLDL09B@%8xF)<=|gqv_d=mYG?KYr{t5y~t?!hep&NNu7$m zO$>KQA0(i{1rg|)U5yi9^kth+C%YhpVKUS|*A%bfl*)w_AZZ;TXWWX@*mU7Cl?4ul z(J*#6<1c(3m6(`G_3hkBxeeL@r^5lrc> z3W-RXlhGKVjiSUh3LV)u${dc2SIp(TOuS=ZDS0o%b5Ds-!*1zMF@5G{XUP}v6whv}+k*N{V_Z@H% zL}?b7K~>zj?uQ@9^a7Tw5Ld2Js^Gq!;wn`IX=!1kwJJzZu{>k>#_=aG72S=v`TK|l zK_ZUt!SnM<1;uQf36a9MK?m~x{lKsvSn2~D2Dw`sohX^p(b0 z-M}FB75h+F-#&;Vga9W8^al44M3|D5K2=@;J%7Pmw$B-WXq*6{3BfeoSN`(LKqE?? z=Bq))*DUGs=YfVVMVbKw)cq!@w;U>R;lIxU$jEZ1H?V5tdX}41>^%+znJ93Akz!^^ zH<#(KLq1iJ2J;0Xb7Vs!2A+cfz5uMODjL@kT7RXFnQJAjA@F1p;ThsKm;MGENF9Un zW|WAPuu%8`zB*~f{i|YTdJ#sD!2aA$ywm82!ac$?p8s+U$UvX zmh=}0wMUGbfS@i3qx@e+ieBcRBpd=rDKX>w8jveZ>af_>Y!RaDRXA|%7_8hn|7(Of z2j-Of1F3frauoA#qhK@-O-LT`uo~Q1-ryBBu77%*Dw10t-dWWQ>%7Nkrot!hCRY`^yS5*9}qUWSBq>uX2D+#z-| za#YK8c~Xa)`*$r3#(s>91ljWvssH|Re`IY&JOCOl{Wo|>Cze~V_mL_0qDf=xel6d) z?l4u~ogbd8q-ADCiRU_!BIF&kaV0U>?^0z+ZY2?}ddhJ!tbpf#~yXt*wMe_%UTG3+iv^ z5Wk>9soGmTfdv>*dYFVK?=7Q96g*%|Y(sFhymKbaOyE$aOL-DgzJ1*Zp+y+&XFr(0 zDf6EaX>#&1C7XcB*r-Nk;*y(WNsgbU#cPPAL@vc$f8;b?=#DrL zXc}m!)nJ`W1eIvL^UA~I0)(S?hdzLT52b$>C0IGY44#%8xeWk!VfP3igfm{Lxwe)~ zP%uk|37;by?CBJR$dnqwOwIODFpzN-QY58uHrG5~?_h2C^w<;g9+>(2Y}JavKLh8Q z3H0iDZlB?fqBMF+a&kBQBUmQT?vL#{ix?02QpGPp8l8?Ab(_$OpMaRSrmRfv<`gW! z6b`gzUUxYLCEjP9`B;Ft(>XAl)<#WSlJ7&KeKH~THvKs2SDK{njrlKYafU?uhUaKH zI)GvCrTNA=)IIzWwyJK5&p9;NYbVf|QD2?cyEA;m!@R-^9<$zb@^?V0j?tGK2v|M{ z@U^EeQf&ScN0GGw1KPEZpr|RWK6AAn5bXt_A^61{n94zdfn$uLUqz%c1DD@kX=A}n zflZMvD(ErnsH@2{JXt1i#5)f}CFY|*+l3@anph;jkR>vRXi1`zv9hubk|AAzYgw%M z9UB*ygx7fon&i%3z-_k#xX<=K09+Q}%Nq#90v>TPV+LfG07A|c`vew=LJtQ8lXRUA zvsFTXRUHw$dOC3)0qQ6DadyYd2N5{rORvrLn z*IVe;*`$1No`)I+y@md857^_<#zs1OiWP~w*JMymx|vvQlRZ5>0Dqj2pUUjKA9FCg z^<2h&G_G3&n}aH0VWtn?UvYI@^4V|A{};_xCxmb!P_b# zC_%8IeH#mxo{#`Fwzu4BOYyw6{XBK6C$;%T;%~e6ZO_Xf-2jx` z^zCVQFj3%pZ?K_=6DsTh0z!s~xoW@+gOE&`bEfl#XYg%_z83LfmEr0H5T-N#lu7{1 zMT~L;)*J6*)H>Yc=%qEBE&uw$ug10cto4zTdtTJvvu0C^W1cI3#jw_wt|EFk$qlc?kT=@?7_ zQ>h&k4i~8CI%;YZN5b0l!@{cMuPya5<5zmvSy^A#zW*+tx4xnN;HK0+d$~tsjD3NT zDj`Jk3Cq*+{R@eRr;Vtcs>W5fj1?*zV{C4Def_oS&u2>60=(l+pAn2j9WO@HU|2u+ zb1%%i?TUX-zc`vV!o)=qLS>p5E49HNqAlQS$zqW?^BkN=i@ad2{y?IP+` zK0J8o{&MniQXZL{SljgU31CHz$Lxw~AN5g0C`7YjY;-|+;iHpzC!K{@hl+t}zgF~z z2vGV>vb}rEB3}adF-4iS8vXr=@D;|fbXaIFsLL?Xl@9MGJY}xUjLy#1zpKb26AAb;_rIAo&SP;&wqM$OKe!yim{M${^MZ`kXa53*8ywPoh|)@vCUF*y#&b zhFCG^7Ks_5iF9A=Q!)J!MXSU^n7jRKnybo^nBuw=!qR({dPd9?rcq(0$!bx0w}OQB zhArFT@Mq5hZ;(=7j8E+J7)CGIYQKTCUqErQx3tZ~@wBlpUPL<@8x|@k&6a6}$^aUCz zJ})8s2Ya*AjUcr>JK%G?gQ>?X)bz2vy2uPcU|^tSW}tgdt8O3rp+dF7pjBXytuU{7 zWOuE3e{xFV5(CpXPx!y6w+tm)oklhqOO}iNo~eY|;nAxk&yVb(#1E4{-{aIFO4(Z5 z3pxvE&Nj^Nrqb+^fvOzS3r7aUZh{r(rSX`%_R(8B@@$W?v?TX=#Cc?@+k0cxC$^XIu2iJ&wqjr8 z3yxqIb<;dko5uSU6v#HkjO1SbcW5x+cR%0>S3 z@MOrn4sQ0u6%`HSD%V!gd!LixE=0-uGDS^QRM*-$U!x#Ya_Qp+P2W)IZlv#R;E(sT z$%wy=mCP(z4)RfKG`o_#A6NfKJ6?u9dwLuCB&(~%{+elMa5X-PFf;zA5&VFKNlc0G zG&FtxAC2JFlrzml#&m?Q_-?UYA zU{8<)fBfiK*WP9ygkn#eo#D(J7UZTD>^DWCpv^T=+T9cD^b`XqqhmMO_e6|sQiRQy zaN~#!xaLT%@0um-u@`#jKAA7NZI$Xf4&Z=|s3uXx3QKe_$Vhj%#{^ znm43W@}dXoVctsw4f^E_LfVdQms6ZZB4pUtoM9JAFc}QAte=nR3UXt@eBn7Q-4XdX zEvBy5xtNJQE)#84p%$R!oL}KkqYU$kS6c0Jo82D0flQmq%x)J%kzwA}qkhG-r4)*br!M@ZViLC+Ew-#yjNDYe zrw8G32HGBpgRKoB8j^OWrMi#!t2dVgxQf^ssa^u7QFrx#lq*f1{z&F!8>rKrEwtm- z0xQ7xVUKoOtl!`yhNx()Utef*?YlCzvGK1ww~tOi{{A}w8d+hHf}7^{>Y*35lF4LTT&)K}=?XuFf9bTYzBHS%IJyO?tK$X?Z;m>5{`)L|*JFpm zr>;CoZM~h2&gMKlVdb4Li(5eDHf1qAs|XR&qB$m^XNsmq?>9L);{M5l(^EUUBmWk{ zW=H^QZn<<`Edv!MmZFuXIw-HMxXl=tX38nWUN>;N+pX8Q4^#&E`hEw7gy6(ZkzN6N zZbfvrBJRF*?UCkTLGMivf(_R$M8`JTimc0TW&66+1 z^Jul5hgjE-3sqUV!p1UGoLhQ3kCL))ZjMt;eV&p^-cY>uTi;kDj_!-x7b7F|hG?Sj zk`fXrsp=eZ)gz?j;%1Xh7Lolq{Qn!9!wT{C`LS&p_djrRTy_oo|Np?b>Tmy_;9SqC zaYZ}SN+(*mnnYUjcF~^}xw1VM)d8)n(8Jl4YPwwH1|OXy(Kncbn;0#2qKXl0GlLb< zgwD&0x}tKile5%@i9d=nPI_QqVJMD8%IDpO-^*5qD!z2S+M}dHV{#H^b@RoCte+m&JK+v_Odd8BatFW~4TacMW9sjrA@B=bt&8;KV4ni$j0uH$P)iDWafm(dvQ|FDP>|PDO8cLH3v)| z1}g_+e-6qXYK`;DJ6IY#f8i;%ljV*2p$K2CZoiPe$hl_7bjGToN&EwMRaxaXi@NBK z_xr!CIPh^9GMU2+VIL-!Y(m7Z;1ux!7{@t?_$}JqiX1yeI~bNH=MGWPvj}Y-E@UXl z*iey97W!8jV^8|$B@P#la4MDW(yyvYzG6W+_v_Z6xWx_i$g!`+z6uu9QvJ|uHDvAK zuN6@$Cuc)HWt=G324C5-XxU+Z4@X636!O8^r9#~bqhR};>*JqkGqea#^HBNye(EKf4SAxBhJUNSD|C(T&mQzWhNWM5Z$ZfVuv7X&iQk08@ zF)!>`?B6_2m4leNz@eIa_j%gWiB)COhzu7=wtne3@A?&-;11C+J7^T(b)q{9J>nYP zG)jV;LgR-pzA%x7pBKSUr&JY&-2B_@!iAYO$sSTLivkFTQ`JePWn{T z5qUYA$8Reil@&IPwt`t`K8eT=M@!VPO-IO$`0_7B6Y$cpk@LyUDE|J<>gpQ(3ir`1Ob_PYca^cZzi{(neeYLzU1^6GPcA;#ER56_2FJ zCWE!SZ07=p6Si?b21V^j92Uf~%uFt|nRl~!3er)m7#PY~5DML9@kU6XYJQvXaN}au zM*e$#Nc^_}Z>(&8-}FcE*MXv$297pYTeL0HV$<^Ban@g7_w@sVZHVuZcUWD2s2$?k zcoxJ&;^Fmj>FOLFo5bVH?W2DPLxpajQNcll%jJd%)en#F4IpKB%aD%~a(+oC-!w&jGR^+)AoaxJ7-akpB&AA6L>6uKBr{#!3i+5mlfceP zJI)O?d=wvV6g|d>S}aF@SQ05=HuTx4<0D>L6x?9larrAA^D)&&$#(s+xX{Jm?*77W zZ@+w4=Fm|XH&#K%!AX^|qYZ1)|DVvgN!~0yL9ZYtVbcSpeqm3*DL$p*4+tE^3$MDm zxxs6XuDw)~60}*@8>PZ@(fcUZs)^oHC8okbCM4;m;G~_q3wjfbiBHMdG9k(-YvS54 z6O`u{`4!kDPskN%Nj$R8?-MZr6OR?sV&p__FtqMU8ea)6#&PQg=xDR#s(Y zgUNaWv$q)x@YP0+<6SCFo>~&fodT}ssnc>|KQsRupVM&#+_aSnQoAIPGaSzP(_Q!D zrYJE9c3(2r=U9C}%xB{a>6LG1v|{TdE3YvWiidliEBXfv;J-03uje@lW{LjJ_Me^= zZlEl)Za}KcTHO>!{94;Ge&RHuH$F^P8WhC9QpLyDDmPp@YMxjQx5=|;`<*vktOW}Y zj2ddQqRUOXmHAV{;pfUls?D~CP!7Rh7M77wj05yqb$My!(ww0gR>U$Se#wToF8$yp=nox% z^|?vo=Z@d$_V!my~+!zrjiz*X3v8VH=?4-Dv3`;_-o% zX!Lr%Hb_KHo`W`+{@}i+c!us%OgKM-Zvno$<#9VwNWmO2rJ_l~P#6umk3akgMk-Vs#uUKbKpZ@IcAaWuSdP)kI9-<>Qi` z9zA_zcqCzgeAX!I)VJ|%{SL5HM$_{%zh;}H^nZZ{eZVQLEB`$H@?v6%H&+G?2R9}o zZT;oTS}O=zgp{nyNN7TTn+K;DIzY`xiEik}3^`|KEoAd8L9E5?I2QWn#TElqP*FCS zcedjAudVa6b`5%c-ZV zy>Cf7o*yq-QM$p1+Du&spngIQ)6rDp+B|7lTl}s zR@EZ#)8yK=DpqzNTFN8Dxksa0E-1fAg~bW<$o(ho2vQ_l%C zDMprul&RV4g=1&j$cFdcF-8+>#gkcfAeF1ca(&dy|od z&Az|ZjZwjo?k71dq+s->OpPwCOoX%UhgKl609QsM3uCZgrvX|Ta6?jR`3T(0trz02 z2v>7e;M<+-mKy%WSqDw{e>m$b!FX{XFm1@{-9;bNAt57T!Smk+JhsE?Jv<^`*b{Y0 z_*_7fh2Tg!c-V-jAUDI?#%-wqZlftPTd8vAZ~E!LbvY%$M}!)Jp}w?W$5koPHQ4}7 z;nm}aQ&T5M@mTVIBMv`kVmOTSc0_!6yMO(enl5c4QPv_NC=@}KWjBPW?IU z%@lPFT5mg&RcB^*g0T^^{r3R8}RisHOhLe@w(Jp3>XPd!D=9VpB< zB+8|PAmt02FV2_I5t|z%1qH+%%x2MCAAnEYkT6MKWQl%s@GH@#=gv+_IpiL3(w;}g z8V(^r%D9Dp9xiO&l9d*HrvEGQWa)RsZeA<4iiGs(?j}pHE80{~bW3NhvXoaO(54hg zJ(jW3PUWb{s}H(|y8}+y+QcVcD^h7NBH1_qrx&k%SkV8qR!$-l`O9vB2{#T$YMX&f zB}AX@Y_5igBg5}NlB``DKMXr<$?#>39pf1wJ08WOX zd*>*U?1HiB>x_69KIP#5;p#)nky#zJ4ScsTAq$xKN|76>$x;JGOd}xB-K)A@4?1tR zA#YserMhGHE3*U!UX|P=Vgj}=4RXyBCK#$XG9+JW8YEA8sY2oo-R@rQAqzjrOe!ie zkBuByjj)7!iVQdx3BSvI>3|T1CvE?+W6eyx+(4cM=>`J;l!QNGn7${B!BIk9UB^)I zj{vl{Qbk&gWM4Qo=3s3VGizVjmptng6WgJl<$;_6j!&3 z-U8Pmcl{78rvaRUn;U6ZW4aZ1-In^;wV35Mu9EKg>lluJtpDF^g2_8~fIoWGKM#=S zi9OG~~d#jRds0=^1PHXJ5N!0*U1nnjuh`yO~|B4&tzN5?ViA7oGC zI6T=Cv((p!=6_}Z4i)n2oR+U2MbBoDd$_A(_TEc=HM0*0e~kj)B=lMu0T3UeZR9b` ziZbH(YrQwZA7>$e*9N}(_TK<5z#9P=jz2k$*9jGEAaIHPFVqMdA(BIL;ire*VKB@h zsor?0*Q*SCjWEC7ISjo|$6g{ykvQx!rJFKZ1u5TP?upxU`2Aljt6}j!Z}ffn7Rb-U zjSoL;DV)3E$Xk%5iS`$H#RM66Iy5CS;{enZFGu)K+pyweKd5HuTy(6s#M)*J>B>(t zRm@v6CSc}OmGW1RrYLfS>&_08XkSGB{Lvxb|DTfFBwm`XxdJ5bob?_&Bu8p@5=b+ z9&o>3FR}mx2M&b^+Od~md*}BpgopnWT$)pigQ978DU}m^05`)C@Bu51_a|Xk@#|*j z38YpLbb_@;viOaPXDP159T^rU+GHtu5sEjSg|ij1!YQ1Ks*N(NACBj?4F4t8+XlK& zf?M@OGo@@|0=n$4dykf+QRCdaVnv_}nLcL8IYi<|?09>1b8~|e`LIqrT;!qoXbBrN zedwg*8MKesOh!(wk&ml{yZMj8qIFz_4%oYyQ)Oi=-FE?KfD$!2a71lOudniuFy-oR zh(QkLTGlt>!4nW@Yv88h9xLWJ6Mc>Ii zU2R&3pJEJ4ecXsuV`R5ObN}FY3rcI*4t9)G7uIH#m#=xpLFIvHbIqg{Xiu}7kht!! zAz2SrE>3Vymm4Zvdc_Lh*17+pQpE<7y8Ig~pjEEsi5n@-8CZlql4qRmOfteeOqqh^ z+E!mZiH_0Rs%{eWHH}Ywcu(D<%6^pJSSo;e@Gq7?@`&qNgFwd2$KGAbb5ASAjrM+l z1=?si$4gYzfIy2wjGvh}n?@$Rv~xyx3C_l{PsOm&vy4VP)N{r+`hCCE-l{ok<+Z2x zRGUku-rb**|Hb7==42E@{^{aK0Q61$f|;Td+?c35`n-wm;f?Pj(XsjO?SxU zIW`V#^CXRO3>}XJY-bp+tNbjNTD%jq$}g}MyD$G8H1MOn*4pBx8^L1RPM^j0b=PJM7!8WfQqSE&e)!(KGzrMl!0U5ui}l1I*2r)GmNnRL=%gl<^Jo)Uy%NPYBi+vOn09= z5b*=KH91rk-ftDh4+Y#|mx=ldBUo!RgB2_*SIGmE-*G@Ugrkx?>{n;*4c^aY7HC)21na%k9X_dqISBicwg4FkyKHK zOVjS=6`G98+E|(kxRch`m4i#b#6UZ;&eQ$0i#Z?)seh2Zca4ml2wJ#HsXciZpo4ml!}kz>4g@ceT54cSm>m z5ulmA>F$O`gqAlUeat_di{39K5M$nU?9DWD9`g{3DHl^Aqe|yflTbOj~kBa|qS_9{eLT0}ylE*=@#Z;Wn!+8)4TxFu-cnBf`X8UjB7r z3oK|&D^uKP)YhG%sYwahxa9}G{ta&cbQzJbxEh|@<&^I^Hg3mnZRX6FbEr_X&CPH^ zga8^F0*6I)vJN9wP5CV3Z;~Hw&qGe7Nt!{wG5|6U;tYLLGVEyUBj(4(mRN=PQSew{hdFa#jh=J}Yv4IUif}5$I&{Nd3V8zXbjODJV zvI(DigN^VI@q?Uvmh4$$QV~U!sROE22Rz13go?$p-dQMSV^?nY${+UBsX4s!XCh>_svE`YwjdOcz-zMI3Nx%^tBw1=n;ECNQMP?jyub!Tv2jl4TUQ^LJDg zED*@jNY{XRoeV-cbS&Bz5(Ta@P0aUF?-IimT7bauM1ViZU#g4^JN|yT%Ps3*u6CUv zi(^tJ@h(Kkh$W{8wINn&bV+~()#+X;^vHX9WN+Xs8H<;GqxBds)q7Ka+pui?QOdQp zjprO{9L@=0H0SF~xM_MkG8`6qh7y40Zqetw#9lnH##Z<$D?2&|aACB!B^G~ zu0}@W%_^iKD%R15memZLBI)Xu(-w_I3aXW(HWotM0Vm&$s?J?=Aa{!tie?qOjGLW% z>2-1AV-!4|T<TbY!<$j#1DBrW_5hyv^J{stq|NW3pJJe2Bs7P0EC*6UOlU|Go1ebel zLUS!TOzqT^|ED&S`rVJ2dGBI;3oXf|v+HFDx-33P{XG#U2d4)(RYV7-DQ;AHtzNSd zx4wyfqLtYp{Pr7rwz6JhY2mk+ZeC*y^$=84Opn`C+3>n*r6m0LIo3Igk(xE+*1fQ0 zDnz{Ki5Y~6B+gfn17p@B!>Jg4RpTTr8HbY4%Ji}aJ_NnNv^84+Pv(TP2%0RgcdqW4 zLz$Nxo{r2^e7;%pV}9^y=F6Wu0UjWGympR!T%&MvyfR9Y^v6*a!xit zL^?D0=V%i`^U9(ptAi7;<7Hx`q~)jI77-4e=l#>1@-H-kmnj2)Mu7h5zt9Ls>Qbi6 z*^$MN$wL+=7QhPO6F*6qCjBpxO18{VHkI=f9c^)kyPAoX-IqqGVp#lt*Hn@yq2XYq zEe>%x%e4;(IM5-Ww72K-^gfju8ssRaypdE>R1Ie(x~Q@o^jmI$v)uzc54LS>qa4QI z6IgBwcyW_4wML!Vr$<(n=Dmy!8QjwmKa4E>PzCMzv3ASl3a^t4bdE=^jMDY6P%ujr zZ1v<>$pml&3zw-~ApKa`PZ)i2t3)n1xWa>&<4N#E$~s5_1mjmkoN?%fW(WrB2?ve? zm4EyTw7^uUy=px^ej)!EDT;(0J;Q0|<+)txQ9hqNtc+P$xQOK%$o z9t<{1qhAKxI$aip0@;*5jaResx(VudMH{tnJ7LX{>%V(vJ)73cnjNE53=&3I&eW8s zVY|w(wC&#Lf_upsNaNP!AVx4d^j^1kPVzlIFtus+Tcwqvt7hfFy6GKNVmGL>3Aa$8 znD`Kx0OJEm;}ISdM&JJwQF8Dg7ZcOa_we3+kBES`u(Y=0)~$Kp^a>!r-s1~?dj-F( z>8u9n*#-1-WPW??=jcxCS~pbUYTTXUj8u!O)j$sj>^0^-sWOTA7YRjJ_IlA0Z@wa6 z3|I*!@f_chiu%w(_LcQaG97~yG{u|@BzfY`nD6ITrkDP-bUY$v@wz6U!&aU`CD|Mn zaL7DJ+&yRm(Z8IQ$@Te8<&PoJIQL6*dB{BD4cy#w@28YpJIev3F1QiPS!68gSK?BX&7-Y(zo*>&8f2_m19tC0RY$;?2ygL`lrLBng*V&vx%QVHE zcdL?2!<$02(od3$1ad89bbmEV_-QN$Fv23%07!9y^c4zYRg+ptHGkVQOU|XV5tX$M zK(oX~bZ4P`qmEd90?J0VRKES<)Vo#arL|RXJlPCqu&$btrZE?~ILZ?~+q(}MX111& zwf^{ss-e|!|lcv+E8HC&M$^6sPZ?l=2h(HICyBq z-@<+YhK$`gXN?r(?ZDn0=vXm(LlGHOSzTjQT^Yv@nr;|BCPwfvc3CG&d(O4`(y9^s z0Oh=w)r4*G80t}#eP+Gi8qI5qO^oxDI|pfnLiPQ}=%v4VDE@R$pzWuzh(<(35qDtE zU`IxsoD@8H`cykADepA_J=hMDS~~pZX)!Z11ugvCF(L5tC`rb^s$PZDO-Hp-gW!_0 z?2wbTl0hct@{3`V1LSVqzN`O@oD)CL>h5OoB__|d_hP1#udK*dK3wT;%n{Xkcg~2K z1R4(S149yO@j#3o-XHIjl1&F^c0uI0q#~mXdZtZ--y@s=Ka*MhSU^aYvfV^l;gCS# z(A}rpCwo%}y96@$H=ps1g8-g(^NL&wu(pdpiovGKnO19DxeJhwFx$D_>qt#Hxs7!_ zLq*%HkswBaF_4dQL2JJILM{QQV4B?R?Z-QSX|Z8p-{1k_7d;unz0Auy`l^GV$oJiO zH1n;86!{4;+UKIC&;dRnJ{U1d9yde@qJX6i1rsyvd*brcRGS9~CE$a2z@T?l`+x(h z5(;$rK%QFsmtx5gAq-U~EZ7I>KH%5{g9NA9Zqmr#MN3YN={8MJuft`F*zH5!P|)bM zx8oGPxSh@m*3?v>c2o;BZo5)~AK7P<%U8cd##f|tvFvxm)9_6L2?h zjIw(XZfNc=iRA>4SSXdVOmb{NZJwuR#W#e1X)HJ6wBJ!n&->b){Co9lIFpJX6VM2o zU23p-W=u3pOr#AAR@ugxB`5F4l(>x~@>!jR6HCCM!hk%X^CIhx8G`VZdWGZlHAdSF z?p&R)FwWA?>hC@NcoKuPF6P4eGQ20g*XA0Y4V^(wh%G+P)aSK^7=ZNdr^UhDun%me z$>1vOyH)n5^!RM%>{d@ws;IH3Xwn$%IN?}Dp+yq?ijeBG%B`%dsLule1B*?aA02=N zoCb1EMPHvt@0+V=*Her(t{#o-008m+eDrOk5CMl2|u!wFE$T_5O)eHRxr9eLe7geIqBGT$kkHA@??&l8s@ z%h>St$k`dE!HP3qyql(6DvyKy4G)}gmj|K)!)k-MLT&*lJrQ}0tz0CuA-bG;pQ~zXr;nY3gQEpLa02Vv ztPGGbC>ind^D` z$vGmqPD2_*@}-uhBy`4nRs(3RXb*e^BD7|}1`cwI_WL&~1>iL%>UFNA+2|GM!Ge`4 zC!q-f=@^LQWXxQYQgX~_9kbqaP8y;SuHsr48#@X9T!bwCkY(Y2ic!BY2Ky^J94(>Ln z*7&1CSfBm+v2<)t6gQj{_zE^4`d?rLWz|JWgFm!(ZL1bT#Jl6;1eyBDN1sscy!h-( zxq1rWHa-!LBfwlS%A|}cr49H)rjV>24zxmfxVr-(k-y#v9uizeBwemhfts;>koq;_ z;k!hbQZ+o5;sG#daoDHH;H(&ngoQh+?>`Vbd`X z=5|Db5!w6=k%FAC%w7Pn1bP6@)FrIkxaceJ8sZ>XvKz31bCwnjeS+O^n3B-IfY3|v z(#x>1rR_r}z@Vn{7yc^A4M-c2;wt{I<^^)dAB6n74__1hXxp%DB9g$Q9XsDI5T}6| z+@V1%a2Io7JBf}vpIorer%l*Qq*c$-7bep-2&*t_O0|T>j7AEG9QyxJ6(~dzNrd6S zfVKqQ*R}*yEnsr5^+%P^Vf}kj`8uNx8TNa)Lhx%>O{92WubPyaLypg&W2*{WR35r|hPiFwhoZp%zx`z|tQ`XJd) zob#Uw3LqVsoN&dX=@k4%n)z4PyF)R_f|L>u9OoqQ!7L%Nh&0LWx+_?#IU270EXc

)mxx$I1VY6)+KjI4fTOiAJzSaNQonVI(Bl>Yf(EX?kSVXY7j~&_Mgg@Q2l^;M5JOWM*w9D%kemkT zkT?_}ESin>6zm(Ks7Dv;Zf#8k6~&Ddtfc%rG#jnF%j%}4^O-LXHo22$f#cqu{O9^! z&&T0`6!puF$9`@Oz^`ReBGw-o`Ji&_cMS5fH;DUIls zke{F9sDZf{8*5#ASe2JpnOU6GRn#PKbrD7mlOB2!aObGIwz48XTR~rMa2d7&s1s*PegZX>&%&Y!HFGU)0<09W1o@=1x7=MxzBDyjt7Z6`Z#B2W>#>rdU9S zQ=573RKL%C?o50h3)eetMsxGF=T?dHY$T(p{S@Z&zD~H_Pc~r#NkW-+qLUjBA>_hN=wiYR{H^7o7>i}#p|bU|B(aY z0TAMt1oO*e$?R$NHD0$kf(84@GLkPVgmW_#KRY9S*y5`COjlM2cJiT@`+X+q>!dVL z3!eBzwq75Os8r;+P1_Y4COJ<8@S@c1=^4!LY?fE)&cOHbRoUVaFN0i`VxS7<;R)f+ zwcG;)`G`z5;c)>-!wz^UatgD606_{es@)Uc+}y0s*{ZzZPTygD7BvpOQ73QS`pT^j zZpqOp$os@X|A9W$^W~=62+$&vkI$)p=F;^|csDLd`}th++S7@A?VgD@h)-Y8 z_H04AvZeF#DbxFrEoy6suH@u#VGm_*QlbsOjz8D-zt{pl?DcrP=TE=;CKZh67ApD) z)f`z~Akjzg8jSybVj|}8qzx_A`yqVo<=#eKx87;`(Wd>}&;SPi;*szPf6RE&@95O) zPbEOPT3m-q6~s-@HWKYkg|aO2y$q?^=+h6+{!yf(ON zt5>DZ>bd%VRr&~4e!opHg@4&b-qsUt>=>I!ymYHT;l*;v!T>enGmA~ zNFdoaE&~U#)kDsVa3NP%PQD6AG$=0>q$f?Gsm4LBG{wIkEhs+sV~u#dwFMCKZw9E$tqAM$fJ?Eq!8L zYi|P~3Mu#Qskxr5M-tAbGcH#w)lKf@obPyGHwCNMt=2J{i@?%rw-J;f7-z$hx_<5TJe)b468F-h4|-nqd7+Y6aK6-q&A*6D^87U! z^3N;)R6Yq&0ySk`N7Q&KiX5O(SyJpz{9*8J&_^)1h9>NByzNZNPEylKlkr73lgHyk z2;}vgp6-Ey*47_<(lVM#gpQF#tN*f=iUk)Qn+X&zdfkU-ihGL_BEI`7LFn~#HW^n; z6YBFcn#-~2eXo0*v@uy?3`|5SCym~?mwa)0T_&TYOW?E2EhFKlghEj2_d zc-w0uCLSR49KANoqS&?SbGHBNvgS4(ycS*Q+`RP@$rFOEvh|7y{dtG2MuT2n;E@zx zYv5}z(SwjCDjM3mTT@3Bh&XRq&maix?aHj8yv<{m+xZ!epg_nk=gFobOYH-O*Fkm&Sq8{14cv$#)KbcILSI<8->vL1yU zfLQKmkggtR6pMXX#M0{<^1AhWlUZJ#L;N%X@*<~Y^&6D~WMz>U{6DI)P1@bU{tnM3 zTv1hCOI49fzDlEjb)8h5_wj`R7!0n-UWTYN)~K{hNH2rOk&)hK<=SOW!|A33VPXGR zCd|adcvYZ&d?YoMR@)GRElz!Nor{ApythUV)r)6oePRXpeU5r? z`0?z2s&CUrT(e!+hm)YOxjJzcKt1PZQGel zWj2Z5C?=N8SM7p195dD~ztxqPCWG*C?;*`-e*9Gy@S9#JYFFAy^;& z55B!emH2VKZu)Fwg;RJW-QF$KF6OVBjNg$k>MLH))R7_ZxD}(Y!UHDI_v3;V7B;uu z2$Hg+Ee8d$lkRx9jBOWEbLI8s5q(d4nVCNGx_u#w58`n&Hki*%&GRKQuqUInCu9^E zOZE>w`#Z%$fuW)h8qKO-aRZ=7A{5tsiq%<8=*~2*=qZyA3Co$l@dvUoG>_)+G4O@f zkg-Fxce}y2e|g-`%Qat?-eY_j8WVhLZo4F+8lZ`urhzPbzlz@qbb3Vrlq#r9v^uEo zW{`RvR4SuYpwxMNm(fEeP-^IL|1!00Zv6d@%@eY|EahiaCkFa=^rtQ7Ez4t<`tqQ? z!!=Cl`op=hvf^?Ni+*c*D3|syt@WFm7jM~X1lgisjK0Yan|FgcZpJSyJa?An=E99X z8ynxz3~YSd?&{4sv}x5ILlWF@`gGnzw07S)PP|m*XU|{{SKVX^+HhHQB@tk7Td4NF zs9k$H9|wK-aVm7Ljn3eGGYk}|WqE8=qcyq!g)NpB;^LiK8Gfhv4G}j<4cFy>1&jRF*ehL*E>lrLg6e{D4`e8OP9^xf zbA|*52Euf!P<1c&H6yY_swNuCl zhBkW{-FWlmAz|=KE@wTN8J;It(VnUY%EA#fCv=tV;fAva^Hx_bWoOKjI3BzApbFhq zFuDdLrnE&S8(N0)j=jXu!=1{*`A~T?gYy`@VnU=$pSCw#;3vqQVv5d}eRak=sv5-5 z;bBrJ4F)T;JQuqN7H9m4@$sJpItt$O12ng;y*+OaG- zZjEo*-PMJLfoZz#D+fOF!I(A)ympinJu3}XmkeBb4#NoAKwO|RL2QJsI#sbK;Wez;G;l;kC$ooB`6P`dnIHF%k#M}e$qi##G+BUy_ z;<$~Cws(N}al0(rr?(84BZF|y@;Wtxu$O*1fzy(-*gVA&gJ15NY|`6)*q7AtG}Gmb z3KxQJ2pQ-A7>pKmDJ>3x_QXBp+l&_uFkT^rL4t)@s*$DZ%QFj>30mIKop&jW88kJK zO@gS&0TZQBIMcmIbNaUS7t4GnF-Z_YJLJkTBTq=)6W?N(tCr|(zJ*CNhepFF?a$(~ zYrDti)JdG(VBbQI?Bd(|y>s%6@%6|3(cBYC(ONEQGpe{+^>JsZOr@Y)!gaUmu~@Vs zNxq>xsxwc=de;^~60j2+#At<)qU0vlN|Aj~`8kB3-0Pwv9`A?sNo-05AHwbcsDkDP zHo3fKs58WIyvUmla=W&-@PUm9!%2p|(h5pijHSY^aJ_p`Dj1pWXD`iX*Uv;R4gC0l z%V-CgKF_1WO#E(}6HBk#J?2s86T!2DV4+rzerbEZJ-Jf>2hSwu57SiNZM^rWgg~=I z-dEH5YZq;gYd-V+imcwd`abNbGON(kc%G-)`n0?*4HZviQuz<-0)}kr*sKHcgZE@> z4=LOOzn_UP5CqVd*St-USkF#ULBb|Oa8Z>SKOuapEP@EGTznoLPc!eJ4BH;2+H-PA z$9MH(X4mC?uF6xUrp4}qKSjTGV*S>!A7JW^TJ_uX95l0OkZMbLe z3hsB(O_b!^_6Kn49Hu;YCyx$2ty47D0>+#$`>J{CgPpPgH%_~vD~@I<@S z@O*`+!FYA~3MjYDTBqWomX(bPmh?}E_>srtb<(+HvkD7$UcqPMcT`zQq=cNFn()(V zxJi{R5rP}jT>Rvr!>U2rF8Gt%`W8|;zn9u-z&9Sj&pBi}l?K^{26!A`Z3$%82!?~o znCh%7lZMJ^7hzJAv{R&>KvMC+`db`A6-|eo-L#>=YN4vBp`XUNqM{~vB_7?`jh%Y; zfSb#F8*oaFJ$*UbYome#LCVvq34#uyhe#NdzTlg#OwA$U&>UZ|ah4`m5j%3HAu}8( z`HZqk8Vo;lGEclg#rX1f2-wwb!bJ1?m%l>5gnf<<7R}uU0YZm~eS;mXZ;`C@)Pl@^ z6j)$|O94Lq5z0v)7#PGZI`~lGnqdOHbjGAKdmSBgJsE`r8GOeglK0yeVZQ-_a(M5y zePfIjKlxiZ&&!8|Lp7#a_;TTZTx@)L&dDA!EDW5JDx|1q4IQofU#ZQn0aUxUx)~(7 z^soaFlIDAnbRmbIUEmSvZ(eO9A@us+??95FdukOwCF4)FyqVb7dvAj$vn0$vW=n(- z5nw2)Do1m1)5dK7%w&!fDTNl2ru9!`W!R9$z~#v0MG3Umw8!OP=>&Y;Jv(x0tDI+cX2H`&9K^s;Kp4H|(&4?3e+i5`68k@3~+ zPKfF=x?W&;pGBytc4RG|j$(7Jc%X!Uxh2GQwGktLsebZEcqgyZ5z8hgX}H@|o~p1N zs1VS9oqOtqk%B;5+M+}c&65Y}MOLxzk0J8DEuHYre4g>QTwc#St@L@sN_L`>OE<`Q zFN?=Hv6(VE983J$;XInqO&oBVw_OE-M7=viGoQI>>lE=N1Xx&uazAVSAWoGe7(4+c z2O2cg^yji?#isgx0wEt(U3X|aS&Pd?K1dj^^b1}XM2e%~<6j+Sf;wE1$ol0r(DoP~ zW3sbJ+F$aE;#^Mo0^}P=@A%z*mvQY{aoNOG=n>*o3CvgO?#Loo|1Rg!4K@UN*f$?f zksUM`6KF%}!~jJ~OUlnA>k$b*aIAuGFT`iAinqz5jOp2?e=XDf;9yVR7(CF3iMRX) z*a`+fK>dylctoD101nr}oa)43)@@sD{=vOGB9ZQsfe!HK)&`AigNrxU-e)q^zIz|V zqnNNHc3xyx?OX`JN8fay?vt zR(sf7Z*@=o>!^bPp<$a|*VSwsa54fe?ci5wZ z)mIny)_YX^-9-`^^mqi=db;Y?Jl2yvDeUw&^TMQQFUGJ4xjj*(xCJ@;@Q5l1n8wsN z!#;z1AZNyIJXe@}4L?Q7!1|4w68qNwYJ~hhv(_UMQB~I75sQ1-e5NT4m}x}d8gm* zzxsM)-ZDcnxuJ@L&^1aLd3j|Ou+%7NHMzENLAcYHhA!yuKHYu{gC&37^Ze{C%cR4usiWv_yHBH(f9)3 zC#*aCFcxE*iB&~&`7bLAL6;LrZ4O(Y8PYI?UsE-#Dm5<;p86gstgZj{>NlZV0&j46l;f?`X=1 z_#NSc1#S1S=NuC0&rS}Hqk186`cX+%>npOEW=lUU;6YAOxSw%%V*{22M3SK=M#Z4a>BI z*Lb-^LnCz~fNza?B+W^kPv_Z5?U|ag90VnC4bpu-DE0;Bkk(q}tb(N$zguyNDvCcV zElu{kT~hGgBvz`3v$V4Dg5}T46ryyF_X4J8mnVoUPjjW?CLjAFBcR(2mN;ZQvBMXR z3;=hI8h67WKyRtg_zBG_NthiFy16vHI6J#K`L1_|?+1#Cf}*6GxOq*z7%OV0m)qYt zcS9wsfYB3fmu$1eZ}+)Bu^6an1MdRnx#(L8Dze+E-GCiynUB5c+dA|r9JXm?Ag>%0 zZzmD{>bWw%|0a8=p3|bOr8vr%063z~Nwph3op~X|xp%hkgbI4IFZVi?+W)j#sdDXH z23msR3daXGaO?WIJ%COTKHg?hvTh8)^li+saUe?vRdRArx0_A}7@2<)1oW--5@Fzb z0cQ%}!~rKS_gWt_g9RH+O6zMIYO*r$krgmjB}t2e`~oi{Q8hY!X!b=d4U_)3s={aq z80%jH@6-+viarfEbDK5!Kz*Y=h?Yrrc9t0?$?GBxrd0hDa!`JuC_W?=rMQtP*X~{|j;Lo5SPDbCmWrC@R1AZAu^<(Nh{2>oEapBJOO-e) zc%Av=ylxD!wN`S`{t?^R>j6rhHtZx;kvq*z)q)rN6?O183xl=3q4sFJht(5d zQ`Fz~)qu^<_}Jai?xdNskPo|T;NLmW7cOg#a~1n2cr^s)1DZ&$-w-;8&86}IHmppV zjo16<)0WFwXDJzp5%1iOrZYX)`-Sd!(PUe&T2VZu#aACJ05<)*;Gm5lus3B9sV5D$ zhifnAJ}-|c{oTWXDn9%d`&+ABKXiGM3?j9z-7^)h$U(|mZ0l+ES)P7}Uq`yJea((e z4g!T8NBim=Sf^2HEKWOtjfhY~(_?S3F)(Mzmg}(-RXpp5Y5f-ppFnCu@aTwT!pFcQy}04+$`)ER zhXH%I%^^O~_02%TK)4fpN(^QFwHHLSZM{nS72kUA?RzXSJmrcA=+_;lvFYiG?jvp< z3x`6uZy_-onQXb5sY%!~t*jv(+qrc=-ulx!#d){<3m-jqgxFJHS8EH@^0m7yJt{Jh z2_^nEWV9*dGKU2*HuciU_Xr{}#3rJj;o!M6{E$D^fF0E?l5EeF>G0CC z0pr|4`*C58K|~I{B;+_TiX7U3AsL_)Pn|~9mrO!T{DVZOi-(7du+Edk0*N>Z+)RZo#ZFu#R3ZLpo$MGo-JmGGNH^jdZ`wMhRm zQ{`|y1|qTeJeh}VhRN;WLbMIPz8?(owlb2^quUrQy_cSkxH6i_8i=$vCB9qpMZ4lc zrr*xAgcNznmr|9Q2o{e+IZWbN-p`R)iS<16UDneO2xqC+-)luiLLw+bU%o{#(nS)uyHVNpG2%gs;^crZTIcnTLsHxUYqU|ka zf)ruPK{q@V)|;H9LZ9&f{EV9Y>nyOF(IkBvZiuC#ONnR@EHw05%%=(JYwkCJ>Vozl z-L|bEe4+0ihmS-ZQpS^mVsLxe8V5_;sPg+bgPc0dLehwu%_i>>*Is_dK3%?Bd)ydT z?D??adOd`nsk?9&8%eNOhdtP0S&bZVak!RoH#<;X*;);*r>|xHjsH$Cj`zd!dVe%R zxbscVNwn8*1C3bF?P6uy#iq~LeIyG%_l8iyqyc$KW_xT0GbPEpXsqb z`FT&7Y5lSSDaT0y=GT(3A?6!2$8Vrb*b7gvrNJPJF zxwjpz)6{CKI|Cr%~#c zBBH=eMI)nRf_cjho|gYGDLUJ6r%$jokTo~;sx`W|s*xaQyHlQmVV1Gi|4dN7aWQ73 zXY95M9!&z{Iy$SK)^wQiRE^UstX({&v&R(PbNr8fq*e?S?j~EwF+{@bu5TB0rS?IC zLNhU)5huTKb@^=#Q+>`iU7W&DNb~vh%GHDshPHYJIx0m;+i<#7S6tSyNBV%G%*c3g zGNzZJb;*xX5#eFb%J2j=Y#clWdA~DwItCoyUCEGAWW7mENhw#eP;oReHz(#;SUvu7 zVEg&t=zz)1Mca)BBAhGb-}w%NI1RkNzq7me&l$1y_YH+B620)8$J<)?cZK+91o!nqfNwnY15^cYzT*PC@q%>+1DxlEGnDY+z zF#6F21(xZ1XzDStSU-RYjq)gb_Mf>hF_J(hp@pli9;ac(a5illxNg%w%MbBVwR}KH zh1D;eeAZtPE`!_oFZbGo+6@oDI2E{l_nOpdKUxaB?f>mRZ*O~Bx+<(j*l_FLyy|VY zf_skDFM58*p0Y~(>c&o??y0!uNxrZUO zoF-NZK7HY-0|JWe8BYwf^a_n%f8!0*3O%2gUKlaI+t!0Kqzh=1oc%kKBYtfi`TIw578bR% zufq8sh!GhI*O}}^JY~MPVYEnbCswg+f#1-$01ne*XDFZ$*P}?fTL=dFCxC;P`2O)N z%@#_H#=|q(T_keYA3Vo(eOyIJMgMiDS9Fq-v8t7n62J9a3^JVM$kfFU#qLqXi5~f; z_FHgiw9IOjSbJj&Z()F#b6@tbd2exm6o=Iyj7Y#RdV$8V$`UcI9# z=LNok0U~kWifA>gx;Qkla@YhTqtWUisbM{~Zm_}%Dokngr_rDxNf#Mrmu^xDZ_hv8 z(Uy)M=GGbH9DHJd2eF5s4G2BHcAv0`AvHzklh*y%L&vVsqut)kQMvhLQ0 zEw=;9(GV=sq0!-9=il2`T^|L`+N!e1_Z)e5#_T0S&odta_Nh9MAGiBY{Ne1L%U#Gs zTwGk*W}MC{eV+2gb7A17*L}F0d3>_pSc997>ZQcb5{D} zu>Qtx&%C=z0#T2CQq)o-v0(nl5nm!P%BJ*{D@3>I$>I8|X!SX`bvh6UJXZ0;%h_|LxSiY1MLx6JtMMUZgir8pz3bj^4bx2vx{}fp zm>91vSHlHfr_Iin>&1dIRkvlspm<;8+61lOT@ny zdQ7w#0LYNl8s%P){+;z_HQuAF+&K2yQs`Sm&z(u;!E0wL0s5f8LQsTzKt+Z1TBr;ms27Roesx|3 zd>U`n_u9q89}Nftk@ju^h9i&c zEcAJ_!z@supA1sgnq-QE-aqQ{>i!2~BDzHt4AkP9k zS;5)Wzw60fG%1t~IacrLt-VN-bN-nHobgR0j#89#erG#}=i0m`Nc_I2q>+3(!X~5S z*j9^(QS^Ob#Zc3ZVi~ znR?5k@RRa7o}GE=0!Z!rST{D~-Acx7xx9-*?CXFASEUSSgH+yzU9qTX;&+4|R1oL$ zCJ!#4y#G>K+6v|GiGA0;Y~gwxWx+c(sNr#uR*|W8_wXCX8sy}Io$WH!{IF`qOaFPt zvrk5+stZv2`&xy`bi}sqc=zmZJ^uL6JMNZUT@1E6w(0`kw2E7o(T8FHE&@@>hDMWm zJ7K^@-OwEiY{;^5DtMb%+^=Lm#R-O|$MU|MZ{K>Hie^MPybj~JU!I=7ilvhwV4|{~ z0?DGL)bOY9X;@p7R{iPb3icLxPuNH0miy%jfAFv$5~Qn?TfO9*4@U?_pj<6KROD(BU;S{=k-hM40W3h zgjx$=GsGtH3E-&kWPQibx*&s4GscvKb#AgB7HI0-6T0~3gh%et-}`*nmL`CdiKy=> zo`b6h#xthD0ZO;Wb~fqg8J&7SO%Hxn)sL%_0MY|#O1{32QNiM92-Y@-1(Efksm^X1 z(@}v))^i#(!CndHCvD_E9Bwg;QC<-}-1;o$sni(1JX1l&`r^7aEm>0GS_2t~e))?A zNcn9ctp@>1LYL@diO$?jTb<|1U%to*i*^@ChILQ`&SSZHF(IqecSe0I5-W@;GC7-l zt@1sbW*^Wm);&F;1PWwXqMSsS`O-(6#8+Ak!6d-EdY1>v)(|wf4`5?U^!>bky8jPY znVpms^M?8P`2s+?bqF?&^G(%8)WlH8Qp7r1gy8|Jx~z;hM?T`{-NlsSHn}T&%CjtnRJlbRzm0wg zMI^kdJ~}mKpwRXqeKmNC`07|=H5AEFcox3>ti6D&1f5sONQp8Rql>P+Q6%ey9mqLD(6@Gy{r&sPHU8OE9lbY1i%8zo_9bl>JpKLy zNc`Cw(q#vAeLTub{UUDPsdOXe53-wEjP9S_BdP0*>rUw)%FZ}c-|97Je2AiC&qmA* z;y?Ly?(&Vtn7_^*KLG7fu5$5V=FpZpA>{2-w!tdITiJo^_*nwg6RuV$ESM(FOG-=E%XA zK2*LGeO`3V5t%o>zfX3kNS?8o8F1FLfsgOG1MstO@bDcna0EbzzP<~(v`zA$`m2w> zk$kG~u$*i*Gl*A{BiQqzHtGe6KURSt+Apz6NVpbSlO<)N#G!#hl+dMqB#=q?U^)>w zG%M^G{BHzMOmDj3PtZ+BqmNK}39z$^$TiWNWF=<^nveoO>9&o0P)^p4X1i&c9OK%+ zHwr)q#j9tHvd&%!e24uFQLmf}<|S+CaOF0@0{oXh8PXI>kppA(oYJM-hyia0=zV7i z5{=mT@KtavCP_eKqTBU?a)~^|SIectb`Lw_FKPwih9_v*BeG1aFsKIcx&_z?FnG?G>UjJK`?yM-Vj41$B&%~r=`P7Td8MsCpog=|@!5oFV zaD6dby?^5q0Wo<1RLs-zVY7BXtS4{@(3qhsfdPrQ>Ix&jO4 zhb8CLvTEL@PUGVLcIYE1#&+^=EI|oCFP%VV|9%#Jv#l}XDHXq9Z}#@vjD)|eMD;?_ zrcQA9VC?RCcMWcf=9G1@*U`rSRl`$#YV|;9>CjSH!y_aLRY&ykIXyV@8dko9uH0jf z&EG7bDK?hqGvegA87=eFwKu;gK+%!Ku0)V}dM<7trB^N;*}L9}nM#Rs0<(aMG{3N- zvA&;i96S$|BLG+%0Z?^S!36Ga)M3^T>WhF0x~}^SdL8t2WW1zmE&+8{KwEmNY}VM?UA})ITe)b( zP?O14ZO(WvboUjX!nfZ;58 zlf~KKg(h&0|j2?J1 z;U(oA5zPM)XWdh{=neDn{{1{G&ka`Iq`KhVS0)GHU8`r8Pe-5pManzzT{{V`pWHAH zD{xJ9p|9WO-X6zo$$)6N2+n(TlSRS{Rs4w;LL(|E$*3#I<;xVW;4+1d+?XW5BMVU{ zxcpbVFy!SgwC298!)SFVGqZC59a>a$hyrNA?2V3ovOnMzlvExe#jYx9x*51sJ4a1= z(*#5h@bw-nxoi}!ZvJHLnccf7(JXlgfdREU=}Fp)XJlChN?U!s0*`FR^}olUK}pO` zaiqlOHCnjg_=K#CK*v5L@RUrJs?{@p`H~0<^12O&%Q8E=TH`)ieNidp@mca z?B6h9KD7FVBueTiCC9mKlNH;pF11?f*NBz}edj`>{c(90G-5!|4QEAUF)r;V{LDU_ zeD8+N0ga5x{qPbZyEr7M)Cg+>PO{bhA%cH=J@v zmqqP_MY+#_557rqm0cF@8N-nq`(8i4&XqLKs7JMtASMkTg796;WppbFcVdbI0Wq;* zcQWcM!woQ=C>xEe*#eNe^GL~z8H@vCIKacvP~yr|+7|#kJ$sx?5(1u&ogr;u=f8;q z3!o04W^N`Gb zJm$A}n#0ahAH|`%v|SEZCvQZp8hniRE|)!KnXw_j9~p@Ic0t{c8Fe;WtneZPY2gsO zI1s$6koP%i&ZJ-!h?XEbI^I)Gqo{w3!H1CX0Uk}VYFsss69K*bhdW?F?XNo^r6=1-$PxjK z^yNybr=P$3mvyrm^Ea9BkPw-Y*~;O{y8Qb0-T)r-SFkYz#h=_zE?lx#@teD&Xx12r zB|HoprIoUIvpe!o&NhlKoq!J~5fvnQtKdTwkRU5IK8k`U2SH=JvTW{UOwyV91DAme zKu~ipkq(ibG}}E^RjWcgnv-xahyW*ezr~BiTp)?S=un#k)93WrMpKYKK%mc;UnMmA z_T?Oh22gR+a*;qEvxT2e9$#ajm-RWDUp9FcZ>gs+d}g(p-kN0%v!D5%&~Hb3&gaA% zCzIv7+eqxjnaxnogg3&qwlIf;i&E`AS$r4xiG5FyCY7R;`o?huV4r6| zI~1?Xzo0d#;uxV=6|^Q@6av*_sFs|chuNXj_uLLbbsxSR1pi(@ytPW!akEf$<*Q5; zf#@Ms`<~)e83&IzRt9caB>lu9*-6`hFqGmK38V^Magz>Kakzb%oLG@?F2!yIY zJZ3f2A5dN*DXAL0Sxv}j!3e32o?Y4MndkhB!EwUZNXmVhcx-lSB!77Yf#P#@)HsGa zG2!rp4sG-UIJ88Qf7Sw6)As?}1{rBj(BM*!K>BXntmV;^jNsRA%`Gmf%dDAa0_NW2 z-ncQSpKJ4ab-lYjQ&o7Y#4LwDgoN509#*yg-YY+4`+c@xXPIcl1Y=nLPE{Jk+|<~M z6dT5>#mc*q(k*8rq|0e6UOo#lka^0vrFUc3$qtt>f%ZeMB*k~sWlL&UlKXZePTbu?|rMW>^8 z#?&ImL*pnJ8uT6jPT~fgB3Q(DXmKQGxlg49v_(`1Vs{5mqzIu7k5hgsmtSNJTx+j-l(hlXy(VoIm%jFXiLl-?+00OGo%Ld#2gq#OC>bozy_LnDhU~9$HV~d zVvp~blh*9=o1NCp-u=-bdC>u0gMW4xZ)ouUMgj11`_k6luFk=h+X^Gh=ebam6*Vr_ zAeWRfPQ-$+vVa;b9upQUQEryOuJXjxev(7A0T2LC32fAJzYhGT03cQbRIOs<8bvp! zy#1@?PW)W`EM>fL-BC{ec&q|YVPy*Vfn@NDx_Ra7KUg$an5w}2+M?AQ8D`LmwB-#t zbBOBlp8Cc2;OK0zX}_|r0{$34%EU!K8`5FesZdc*&W#=yn;b2gZmYdGxnhdZV{XEsaDw_J>=vH4$iQN)?)Pj$nQ7~7j#>B;s&pw)(S_-aR z(1kzuDlunFu27}6ckPv4k;ySB6N&kR^IEigoDs-zTj2vq{MK#9$j=Yz9()7J^gQS>V*iLOVvDxmh+Gcwe$0k>PLJVu zPnZK!kvd0B`+tGxgi=3`yX){*mC(J}D;v6P+39VWBNfaw)_QQO zYFRo3z5P#i^w;W8UxQ_p=Nur6qE4baVOgrp>`Vjiu9)nhK1ZcWQ!aU4z_^_UdOKGI zZ2mB@bZsy9sW3)0K^$Q<-&V`}Hq=fI0YGM={y=8l=8$_(-@-}e>JW?dj#&r(-1Q`6 zz4rp&ojB;|N~@ZBH*E($9BNA|TWhDDV}k+V-CtkWC_h>kHerAg=Fgj;P7NdhzaLto zqriWM%amLAh>?ahINdEAma-VN7YkQR|F?LV4**`qEb_QI_^2z|njDzzt7s``sHtHa z(EbgVSu9t~%F0?bpw|x2)qT_r#6qDXnV(ou$dI>mbmwhHLbxPsS2hn`rbpp*Hs%KHNNX1gy!gTrHapC;ElWmL6-zi@vcUi9(5*CYxgdWjxT3)lFexIC>>Exk*`3t z$i2vn#5M8IST{F;Kl@tK`OO%A&O)h<<+BQqxm&ar3lbIzevwjDd_2s4atJ>c4Gf(% zu~oI6tN2lwo!{iXKlqWg&ivGS*EAVu<)%*2ESl(Ce57(NpC_29gBIT0%8{G-321I$ znUh1hsjgWPfh}`X;sD2twu`=Thg%Upru#yu_^C?A2PNXJ9E%33 z5e_v9cOw#<(a5YIeA5zf^Jouy2n28>K6&z}GbDKeXB?=?(Ek(3kJF36U07Ofm<>r%BUIToDK|xhpC+HL;AFK>n zKs}-eYDI=iHsrgUe8MpB&<*Rs5@rf#%^kk~_lTJg;c)nWLCowtlpXKJwR0}BwZNCF zR4M(Y5ENGqhy2^|qllyU*^542HGgE^lGk%8)Dws-*E;V{J@W@5gYEwDUm!9qyQqbo z5v!yxXa>QiYgT-C&7#Ggk;X9bW3!5k%r@1;Y)l*Yj?>K9FqKP>5@>H3Nv~SSe&-DBjfF8qU_z82 zcy;dKAEYxyoTI;LA7N&8L_81YpYxDD8GJ#@oK9 z=;e}R40d>eGX5Cb+5~kNGeD{UF#Q8%cUd;aom{@;#%9M0H60xo%l-_MU0vSv)6uFb zOAhB;-jM@=w2Sc9=uQ#>6d$rslV*bHs2GCo3$lyPc(|)t+kySiMP=lTN&GPVx9{HC zQB12=!*(C{HyRR|N@I2kKLe)snGl+Da<qA>&>0#esxWJ427wjB<<#%A~Q5U)4ExS zmB&*3Y+`y@5gY9htSxXC({M9VF_$f(yLoDt$}DhJM~NFOkE>x{K3J&K+^ix$rmx>q z%pvD&S4U65sysjNQ^M_~c}n$oF;Xd;&0wPC>d!LH4^M6pWhdyW8+VLScKTq=5`Dx| zsyMR=G-DQT-gKDsFWqE-1vbIZ$N^9Bjy-2TN~&2w8bcc4+q#j)!{kSTq?Jy$z9BVd z=}h5ci#CJ>Sd_-7H;9C<)+*`QKthq?Tef|-vHbfyPP2^Q`2DGc_`%d3h!9$7Cw^Oi zOr`cwKZf-c@m*PJHiCuej{5A#j$1IjYu7CRYGl4%WdNi?yPc;&`!+3k?CyUbK)?Ij z8xhml>Fp3HJVX)0lwg{JH>p!e(nYN<%6mh|G+mB_S+QJ0!li7$AiSSTT|JyIvFpyy zy}iKA)4t=_-gHV>CXr-DWS$@imxY4O!F9$jP%pXRi0s~#BWGSW?4Gc}{KynEKQJ2d zu|W*XOYf~o68RG6^Sm4J)M4E8PvZGowkHNbe$1FRnd7svCM z%;`g=q$uL77aWPX(WZgfsYfg(P-lS(Jl-jCl;D3fERwZ(c*}%{>HxU@7&c6eNV__B zSD2b;MW$?o7Lr683W!1h^U#29N#|we1`60IIsB>+134%~Eas`>2f=Vm3LL|`vO;-{ zq3?o!?w$xewX{uP%W@R*8`9tUK@@xzb>+wl6JQCl73p*Q?UwY_9$5=r=|R_` zjN?v1lxiCR=MM<*k51-7nGRZ0rtw>`Dc>dlZUpchV&LDXFYiMT{%@!+6E-SL+pAq) zQX16;Q3TAVS~?{D3vvHsQ&TUS^e^9E+lZsbfu(#P6b*Bf5m)Gwre7Dt83vPIq=nT!GN)v z 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⪻9Zzf0!nE6Wq(;qFdNmWLHytqBC=1 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 }) { + const completionStartTime = logEntry.completionStartTime; + const ttftMs = + completionStartTime && completionStartTime !== logEntry.endTime + ? new Date(completionStartTime).getTime() - new Date(logEntry.startTime).getTime() + : null; + const hasCacheActivity = logEntry.cache_hit || (metadata?.additional_usage_values?.cache_read_input_tokens && @@ -284,6 +290,9 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: ${formatNumberWithCommas(logEntry.spend || 0, 8)} {logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s + {ttftMs != null && ttftMs > 0 && ( + {(ttftMs / 1000).toFixed(3)} s + )} {hasCacheActivity && ( <> diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 3d9d73bb09b..a9110d31d30 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -62,6 +62,7 @@ export type LogEntry = { proxy_server_request?: string | any[] | Record; session_id?: string; status?: string; + completionStartTime?: string; request_duration_ms?: number; session_total_count?: number; session_total_spend?: number; @@ -270,6 +271,25 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] ); }, }, + { + header: "TTFT (s)", + accessorKey: "completionStartTime", + cell: (info: any) => { + const row = info.row.original; + const completionStartTime = info.getValue(); + if (!completionStartTime) return -; + // For non-streaming, completionStartTime == endTime so TTFT is not meaningful + if (completionStartTime === row.endTime) return -; + const ttftMs = new Date(completionStartTime).getTime() - new Date(row.startTime).getTime(); + if (ttftMs <= 0) return -; + const ttftSeconds = (ttftMs / 1000).toFixed(2); + return ( + + {ttftSeconds} + + ); + }, + }, { header: "Team Name", accessorKey: "metadata.user_api_key_team_alias", From e335dd70f899d0f23b23fb49f0a8116ab495ab1d Mon Sep 17 00:00:00 2001 From: Guilherme Segantini Date: Wed, 4 Mar 2026 18:03:59 -0600 Subject: [PATCH 150/171] fix(sap provider layer): enable response-format for anthropic models and improve compatibility for GPT models via LangChain (#22804) * (sap) ensure tool parameters have type='object' for SAP compatibility Fix SAP GenAI Hub Orchestration Service rejecting tool calls with error: "400 - LLM Module: tools.0.custom.input_schema.type: Input should be 'object'" Root cause: When Claude Code uses tools (like web_search) with the SAP provider through LiteLLM's Anthropic experimental pass-through adapter, Anthropic's input_schema format doesn't always include the required type="object" field. The adapter's translate_anthropic_tools_to_openai() function was directly copying input_schema to OpenAI's parameters field without ensuring the type="object" requirement that SAP's API strictly enforces. Changes: - Modified translate_anthropic_tools_to_openai() to check if input_schema is missing the type field and add type="object" if absent - Preserves existing type field if already present - Added comprehensive test suite (6 tests) covering: - Missing type field scenario (now adds type="object") - Existing type preservation - Empty input_schema handling - Multiple tools transformation - Additional schema properties preservation - SAP-specific compatibility regression test Testing: - All new tests pass (6/6 in test_anthropic_tool_schema_fix.py) - All existing Anthropic tool tests pass (57/57 tool-related tests) - SAP tool parameter validation tests pass (9/9 in test_sap_tool_parameters.py) * (sap) enable native response_format for anthropic models * (sap) filter strict param from model_params for GPT models only * (sap) revert Anthropic adapter type='object' fix The SAP FunctionTool Pydantic validator in litellm/llms/sap/chat/models.py already ensures type='object' is added to all tool parameters for SAP API compatibility. The Anthropic adapter change affected ALL consumers, not just SAP, which was broader scope than intended for this PR. - Revert input_schema modification in Anthropic adapter - Remove Anthropic-specific test file (SAP tests still cover this case) * (sap) gate markdown stripping to Anthropic models only SAP GenAI Hub with Anthropic models sometimes returns JSON wrapped in markdown code blocks. GPT/Gemini/Mistral models don't exhibit this behavior, so stripping is now gated to avoid accidentally modifying valid responses that may contain markdown in JSON string values. --- litellm/llms/sap/chat/handler.py | 2 +- litellm/llms/sap/chat/models.py | 14 +- litellm/llms/sap/chat/transformation.py | 53 +- .../chat/test_sap_langchain_strict_param.py | 424 +++++++ .../llms/sap/chat/test_sap_response_format.py | 1048 +++++++++++++++++ .../llms/sap/chat/test_sap_tool_parameters.py | 160 +++ 6 files changed, 1693 insertions(+), 8 deletions(-) create mode 100644 tests/test_litellm/llms/sap/chat/test_sap_langchain_strict_param.py create mode 100644 tests/test_litellm/llms/sap/chat/test_sap_response_format.py create mode 100644 tests/test_litellm/llms/sap/chat/test_sap_tool_parameters.py diff --git a/litellm/llms/sap/chat/handler.py b/litellm/llms/sap/chat/handler.py index c24cf3d279f..1390b2a4785 100755 --- a/litellm/llms/sap/chat/handler.py +++ b/litellm/llms/sap/chat/handler.py @@ -181,7 +181,7 @@ class AsyncSAPStreamIterator: def __init__( self, - response:AsyncIterator, + response: AsyncIterator, event_prefix: str = "data: ", final_msg: str = "[DONE]", ): diff --git a/litellm/llms/sap/chat/models.py b/litellm/llms/sap/chat/models.py index d8039ff5618..1b09ce9a756 100644 --- a/litellm/llms/sap/chat/models.py +++ b/litellm/llms/sap/chat/models.py @@ -45,9 +45,21 @@ class FunctionObj(BaseModel): class FunctionTool(BaseModel): description: str = "" name: str - parameters: dict = {} + parameters: dict = {"type": "object", "properties": {}} strict: bool = False + @field_validator("parameters", mode="before") + @classmethod + def ensure_object_type(cls, v: dict) -> dict: + """Ensure parameters has type='object' as required by SAP Orchestration Service.""" + if not v: + return {"type": "object", "properties": {}} + if "type" not in v: + v = {"type": "object", **v} + if "properties" not in v: + v["properties"] = {} + return v + class ChatCompletionTool(BaseModel): type_: Literal["function"] = Field(default="function", alias="type") diff --git a/litellm/llms/sap/chat/transformation.py b/litellm/llms/sap/chat/transformation.py index 2b1573bf4ed..a019ba1767a 100755 --- a/litellm/llms/sap/chat/transformation.py +++ b/litellm/llms/sap/chat/transformation.py @@ -157,9 +157,9 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): "response_format", "timeout", ] + # Remove response_format for providers that don't support it on SAP GenAI Hub if ( - model.startswith('anthropic') - or model.startswith("amazon") + model.startswith("amazon") or model.startswith("cohere") or model.startswith("alephalpha") or model == "gpt-4" @@ -169,6 +169,7 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): params.remove("tool_choice") return params + def validate_environment( self, headers: dict, @@ -203,8 +204,18 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): litellm_params: dict, headers: dict, ) -> dict: + # Filter out parameters that are not valid model params for SAP Orchestration API + # - tools, model_version, deployment_url: handled separately + excluded_params = {"tools", "model_version", "deployment_url"} + + # Filter strict for GPT models only - SAP AI Core doesn't accept it as a model param + # LangChain agents pass strict=true at top level, which fails for GPT models + # Anthropic models accept strict, so preserve it for them + if model.startswith("gpt"): + excluded_params.add("strict") + model_params = { - k: v for k, v in optional_params.items() if k not in {"tools", "model_version", "deployment_url"} + k: v for k, v in optional_params.items() if k not in excluded_params } model_version = optional_params.pop("model_version", "latest") @@ -286,7 +297,37 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): original_response=raw_response.text, additional_args={"complete_input_dict": request_data}, ) - return ModelResponse.model_validate(raw_response.json()["final_result"]) + response = ModelResponse.model_validate(raw_response.json()["final_result"]) + + # Strip markdown code blocks if JSON response_format was used with Anthropic models + # SAP GenAI Hub with Anthropic models sometimes wraps JSON in ```json ... ``` + # based on prompt phrasing. GPT/Gemini models don't exhibit this behavior, + # so we gate the stripping to avoid accidentally modifying valid responses. + response_format = optional_params.get("response_format", {}) + if response_format.get("type") in ("json_object", "json_schema"): + if model.startswith("anthropic"): + response = self._strip_markdown_json(response) + + return response + + def _strip_markdown_json(self, response: ModelResponse) -> ModelResponse: + """Strip markdown code block wrapper from JSON content if present. + + SAP GenAI Hub with Anthropic models sometimes returns JSON wrapped in + markdown code blocks (```json ... ```) depending on prompt phrasing. + This method strips that wrapper to ensure consistent JSON output. + """ + import re + + for choice in response.choices or []: + if choice.message and choice.message.content: + content = choice.message.content.strip() + # Match ```json ... ``` or ``` ... ``` + match = re.match(r'^```(?:json)?\s*\n?(.*?)\n?```$', content, re.DOTALL) + if match: + choice.message.content = match.group(1).strip() + + return response def get_model_response_iterator( self, @@ -295,6 +336,6 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): json_mode: Optional[bool] = False, ): if sync_stream: - return SAPStreamIterator(response=streaming_response) # type: ignore + return SAPStreamIterator(response=streaming_response) # type: ignore else: - return AsyncSAPStreamIterator(response=streaming_response) # type: ignore + return AsyncSAPStreamIterator(response=streaming_response) # type: ignore diff --git a/tests/test_litellm/llms/sap/chat/test_sap_langchain_strict_param.py b/tests/test_litellm/llms/sap/chat/test_sap_langchain_strict_param.py new file mode 100644 index 00000000000..63bbbeae354 --- /dev/null +++ b/tests/test_litellm/llms/sap/chat/test_sap_langchain_strict_param.py @@ -0,0 +1,424 @@ +""" +Test that LangChain's strict parameter is handled correctly for SAP GenAI Hub. + +LangChain agents automatically set strict=true when using response_format with +json_schema. This parameter gets passed at the top level of optional_params, +but SAP AI Core Orchestration API does not accept it as a model parameter for +GPT models (returns 400 error). + +These tests verify that: +1. The strict parameter is filtered from model_params +2. The strict parameter inside response_format.json_schema is preserved +3. The fix works for both GPT and Anthropic models +""" + +import pytest +from unittest.mock import MagicMock, patch, AsyncMock + +from litellm.llms.sap.chat.transformation import GenAIHubOrchestrationConfig + + +class TestLangChainAgentCompatibility: + """Test compatibility with LangChain agent's strict parameter behavior.""" + + def test_langchain_create_react_agent_style_params(self): + """Simulate LangChain's create_react_agent with response_format schema. + + LangChain's create_react_agent passes parameters like: + { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "AgentResponse", + "strict": true, + "schema": {...} + } + }, + "strict": true, # Also passed at top level + "temperature": 0 + } + """ + config = GenAIHubOrchestrationConfig() + + # Simulate LangChain agent parameters + langchain_params = { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "AgentResponse", + "strict": True, + "schema": { + "type": "object", + "properties": { + "thought": {"type": "string"}, + "action": {"type": "string"}, + "action_input": {"type": "string"} + }, + "required": ["thought", "action", "action_input"] + } + } + }, + "strict": True, # LangChain adds this at top level + "temperature": 0 + } + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "What is 2+2?"}], + optional_params=langchain_params, + litellm_params={}, + headers={}, + ) + + # Verify strict is NOT in model.params (would cause 400 error) + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params, "strict should be filtered from model.params" + + # Verify other params are preserved + assert model_params.get("temperature") == 0 + + # Verify strict inside json_schema IS preserved + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert prompt_config["response_format"]["json_schema"]["strict"] is True + + def test_langchain_structured_output_style_params(self): + """Simulate LangChain's with_structured_output() method. + + When using model.with_structured_output(Schema), LangChain passes: + { + "response_format": { + "type": "json_schema", + "json_schema": {"name": "Schema", "strict": true, "schema": {...}} + }, + "strict": true + } + """ + config = GenAIHubOrchestrationConfig() + + # Pydantic-style schema from LangChain + structured_output_params = { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "SearchQuery", + "strict": True, + "schema": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"}, + "max_results": {"type": "integer", "default": 10} + }, + "required": ["query"] + } + } + }, + "strict": True, + "max_tokens": 1000 + } + + request = config.transform_request( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Search for Python tutorials"}], + optional_params=structured_output_params, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + assert model_params.get("max_tokens") == 1000 + + def test_langchain_tool_calling_agent_params(self): + """Simulate LangChain tool calling agent with strict mode. + + Tool calling agents may pass strict for both tools and response_format. + """ + config = GenAIHubOrchestrationConfig() + + tool_agent_params = { + "tools": [{ + "type": "function", + "function": { + "name": "search_web", + "description": "Search the web for information", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string"} + }, + "required": ["query"] + }, + "strict": True # Tool-level strict + } + }], + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "ToolResponse", + "strict": True, + "schema": { + "type": "object", + "properties": {"result": {"type": "string"}} + } + } + }, + "strict": True, # Top-level strict from LangChain + "tool_choice": "auto" + } + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Search for weather"}], + optional_params=tool_agent_params, + litellm_params={}, + headers={}, + ) + + # Top-level strict should be filtered + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + + # Tools should be included + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert "tools" in prompt_config + assert len(prompt_config["tools"]) == 1 + + # Tool's strict parameter should be preserved (it's inside the tool definition) + assert prompt_config["tools"][0]["function"]["strict"] is True + + def test_gpt4_model_with_langchain_strict(self): + """Test that gpt-4 (which doesn't support response_format) handles strict gracefully.""" + config = GenAIHubOrchestrationConfig() + + # Even if LangChain passes strict, it should be filtered + request = config.transform_request( + model="gpt-4", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"strict": True, "temperature": 0.5}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + assert model_params.get("temperature") == 0.5 + + def test_anthropic_model_preserves_strict(self): + """Test Anthropic models preserve top-level strict (SAP API accepts it for Anthropic).""" + config = GenAIHubOrchestrationConfig() + + langchain_params = { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "Response", + "strict": True, + "schema": {"type": "object", "properties": {"answer": {"type": "string"}}} + } + }, + "strict": True, + "max_tokens": 2000 + } + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Hello"}], + optional_params=langchain_params, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + # Anthropic models CAN have strict in model.params (SAP API accepts it) + assert model_params.get("strict") is True + assert model_params.get("max_tokens") == 2000 + + # json_schema strict should also be preserved + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert prompt_config["response_format"]["json_schema"]["strict"] is True + + +class TestLangChainRequestPayloadStructure: + """Test the final request payload structure matches SAP API expectations.""" + + def test_request_payload_structure_without_strict_in_params(self): + """Verify the complete request structure is correct for SAP Orchestration API.""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="gpt-4o", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What is 2+2?"} + ], + optional_params={ + "strict": True, + "temperature": 0.7, + "max_tokens": 500, + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "MathAnswer", + "strict": True, + "schema": { + "type": "object", + "properties": {"answer": {"type": "integer"}}, + "required": ["answer"] + } + } + } + }, + litellm_params={}, + headers={}, + ) + + # Verify overall structure + assert "config" in request + assert "modules" in request["config"] + assert "prompt_templating" in request["config"]["modules"] + + prompt_templating = request["config"]["modules"]["prompt_templating"] + + # Verify model section + assert "model" in prompt_templating + assert prompt_templating["model"]["name"] == "gpt-4o" + assert "params" in prompt_templating["model"] + + model_params = prompt_templating["model"]["params"] + # These should be in params + assert model_params["temperature"] == 0.7 + assert model_params["max_tokens"] == 500 + # strict should NOT be in params + assert "strict" not in model_params + + # Verify prompt section + assert "prompt" in prompt_templating + prompt = prompt_templating["prompt"] + + # response_format should be in prompt, not in model.params + assert "response_format" in prompt + assert prompt["response_format"]["type"] == "json_schema" + assert prompt["response_format"]["json_schema"]["strict"] is True + + def test_no_strict_anywhere_in_model_params_section(self): + """Ensure strict never appears in model.params regardless of input.""" + config = GenAIHubOrchestrationConfig() + + # Try various ways strict might be passed + test_cases = [ + {"strict": True}, + {"strict": False}, + {"strict": True, "temperature": 0}, + {"response_format": {"type": "json_object"}, "strict": True}, + ] + + for params in test_cases: + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Test"}], + optional_params=params.copy(), + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params, f"strict leaked into model.params with input: {params}" + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_strict_false_also_filtered(self): + """Even strict=false should be filtered (it's not a valid model param).""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Test"}], + optional_params={"strict": False, "temperature": 0.5}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + + def test_empty_optional_params(self): + """Should work with empty optional_params.""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Test"}], + optional_params={}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + + def test_only_strict_in_params(self): + """Should work when strict is the only param.""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Test"}], + optional_params={"strict": True}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + # model_params might be empty or have other defaults, but no strict + + def test_gpt_models_filter_strict(self): + """Verify strict is filtered for all GPT model variants.""" + config = GenAIHubOrchestrationConfig() + + gpt_models = [ + "gpt-4o", + "gpt-4o-mini", + "gpt-4-turbo", + "gpt-4", + "gpt-3.5-turbo", + ] + + for model in gpt_models: + request = config.transform_request( + model=model, + messages=[{"role": "user", "content": "Test"}], + optional_params={"strict": True, "temperature": 0.5}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params, f"strict should be filtered for GPT model: {model}" + assert model_params.get("temperature") == 0.5, f"temperature missing for model: {model}" + + def test_non_gpt_models_preserve_strict(self): + """Verify strict is preserved for non-GPT models (Anthropic, Gemini, Mistral, etc.).""" + config = GenAIHubOrchestrationConfig() + + non_gpt_models = [ + "anthropic--claude-3-5-sonnet", + "anthropic--claude-3-opus", + "gemini-1.5-pro", + "mistral-large", + ] + + for model in non_gpt_models: + request = config.transform_request( + model=model, + messages=[{"role": "user", "content": "Test"}], + optional_params={"strict": True, "temperature": 0.5}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert model_params.get("strict") is True, f"strict should be preserved for non-GPT model: {model}" + assert model_params.get("temperature") == 0.5, f"temperature missing for model: {model}" diff --git a/tests/test_litellm/llms/sap/chat/test_sap_response_format.py b/tests/test_litellm/llms/sap/chat/test_sap_response_format.py new file mode 100644 index 00000000000..71c61bfc219 --- /dev/null +++ b/tests/test_litellm/llms/sap/chat/test_sap_response_format.py @@ -0,0 +1,1048 @@ +""" +Test SAP response_format support for various models. + +SAP GenAI Hub natively supports response_format for Anthropic models, +so no tool-based conversion is needed. This test verifies that response_format +is correctly passed through to the API for supported models. +""" + +import pytest + +from litellm.llms.sap.chat.transformation import GenAIHubOrchestrationConfig + + +class TestResponseFormatSupport: + """Test response_format is supported for appropriate models.""" + + def test_anthropic_model_supports_response_format(self): + """Anthropic models should support response_format param (native SAP support).""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("anthropic--claude-3-5-sonnet") + assert "response_format" in params + + def test_anthropic_claude_4_supports_response_format(self): + """Claude 4 models should support response_format param.""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("anthropic--claude-4.5-sonnet") + assert "response_format" in params + + def test_cohere_model_does_not_support_response_format(self): + """Cohere models should not support response_format param.""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("cohere--command-r") + assert "response_format" not in params + + def test_amazon_model_does_not_support_response_format(self): + """Amazon models should not support response_format.""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("amazon--nova-pro") + assert "response_format" not in params + + def test_alephalpha_model_does_not_support_response_format(self): + """AlephAlpha models should not support response_format param.""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("alephalpha--luminous") + assert "response_format" not in params + + def test_gpt4_exact_does_not_support_response_format(self): + """gpt-4 (exact match) should not support response_format param.""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("gpt-4") + assert "response_format" not in params + + def test_gpt4o_supports_response_format(self): + """gpt-4o should support response_format (native support).""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("gpt-4o") + assert "response_format" in params + + def test_gemini_supports_response_format(self): + """Gemini models should support response_format (native support).""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("gemini-1.5-pro") + assert "response_format" in params + + +class TestTransformRequestWithResponseFormat: + """Test transform_request handles response_format correctly.""" + + def test_transform_request_includes_json_schema_response_format(self): + """transform_request should include response_format with json_schema type.""" + config = GenAIHubOrchestrationConfig() + response_format = { + "type": "json_schema", + "json_schema": { + "name": "test", + "schema": { + "type": "object", + "properties": {"result": {"type": "string"}}, + "required": ["result"] + } + } + } + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"response_format": response_format}, + litellm_params={}, + headers={}, + ) + + # Verify response_format is in the request + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert "response_format" in prompt_config + assert prompt_config["response_format"]["type"] == "json_schema" + + def test_transform_request_includes_json_object_response_format(self): + """transform_request should include response_format with json_object type.""" + config = GenAIHubOrchestrationConfig() + response_format = {"type": "json_object"} + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"response_format": response_format}, + litellm_params={}, + headers={}, + ) + + # Verify response_format is in the request + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert "response_format" in prompt_config + assert prompt_config["response_format"]["type"] == "json_object" + + def test_transform_request_without_response_format(self): + """transform_request should work without response_format.""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + headers={}, + ) + + # Verify response_format is NOT in the request + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert "response_format" not in prompt_config + + def test_transform_request_with_tools_and_response_format(self): + """transform_request should include both tools and response_format.""" + config = GenAIHubOrchestrationConfig() + + user_tools = [{ + "type": "function", + "function": { + "name": "search_web", + "description": "Search the web", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"] + } + } + }] + + response_format = { + "type": "json_schema", + "json_schema": { + "name": "result", + "schema": { + "type": "object", + "properties": {"answer": {"type": "string"}} + } + } + } + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"tools": user_tools, "response_format": response_format}, + litellm_params={}, + headers={}, + ) + + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + + # Both should be present + assert "tools" in prompt_config + assert "response_format" in prompt_config + assert len(prompt_config["tools"]) == 1 + assert prompt_config["tools"][0]["function"]["name"] == "search_web" + + +class TestStreamIterators: + """Test streaming iterators work without json_mode.""" + + def test_sync_stream_iterator_basic(self): + """SAPStreamIterator should work without json_mode parameter.""" + from litellm.llms.sap.chat.handler import SAPStreamIterator + + iterator = SAPStreamIterator(response=iter([])) + assert iterator._done is False + + def test_async_stream_iterator_basic(self): + """AsyncSAPStreamIterator should work without json_mode parameter.""" + from litellm.llms.sap.chat.handler import AsyncSAPStreamIterator + + async def async_gen(): + yield "" + + iterator = AsyncSAPStreamIterator(response=async_gen()) + assert iterator._done is False + + def test_get_model_response_iterator_sync(self): + """get_model_response_iterator should return sync iterator.""" + config = GenAIHubOrchestrationConfig() + + iterator = config.get_model_response_iterator( + streaming_response=iter([]), + sync_stream=True, + ) + + from litellm.llms.sap.chat.handler import SAPStreamIterator + assert isinstance(iterator, SAPStreamIterator) + + def test_get_model_response_iterator_async(self): + """get_model_response_iterator should return async iterator.""" + config = GenAIHubOrchestrationConfig() + + async def async_gen(): + yield "" + + iterator = config.get_model_response_iterator( + streaming_response=async_gen(), + sync_stream=False, + ) + + from litellm.llms.sap.chat.handler import AsyncSAPStreamIterator + assert isinstance(iterator, AsyncSAPStreamIterator) + + +class TestNestedSchema: + """Test that complex nested schemas are preserved correctly.""" + + def test_nested_schema_preserved(self): + """Complex nested schemas should be preserved in the request.""" + config = GenAIHubOrchestrationConfig() + nested_schema = { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "addresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"} + } + } + } + } + } + } + } + + response_format = { + "type": "json_schema", + "json_schema": { + "name": "nested", + "schema": nested_schema + } + } + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Test"}], + optional_params={"response_format": response_format}, + litellm_params={}, + headers={}, + ) + + # Verify the nested schema is preserved + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert "response_format" in prompt_config + assert prompt_config["response_format"]["json_schema"]["schema"] == nested_schema + + +class TestTransformResponseWithResponseFormat: + """Test transform_response handles response_format correctly.""" + + def test_transform_response_strips_markdown_for_json_schema(self): + """transform_response should strip markdown when response_format.type=json_schema.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + + # Create mock raw_response + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"result": "success"}\n```'}, + "finish_reason": "stop" + }], + "model": "anthropic--claude-3-5-sonnet" + } + } + raw_response.text = '{"final_result": {...}}' + + # Create mock logging_obj + logging_obj = MagicMock() + + response_format = { + "type": "json_schema", + "json_schema": {"name": "test", "schema": {"type": "object"}} + } + + result = config.transform_response( + model="anthropic--claude-3-5-sonnet", + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": response_format}, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].message.content == '{"result": "success"}' + + def test_transform_response_strips_markdown_for_json_object(self): + """transform_response should strip markdown when response_format.type=json_object.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"answer": 42}\n```'}, + "finish_reason": "stop" + }], + "model": "anthropic--claude-3-5-sonnet" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="anthropic--claude-3-5-sonnet", + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "json_object"}}, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].message.content == '{"answer": 42}' + + def test_transform_response_no_strip_for_text_type(self): + """transform_response should NOT strip markdown when response_format.type=text.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"data": "keep me"}\n```'}, + "finish_reason": "stop" + }], + "model": "anthropic--claude-3-5-sonnet" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="anthropic--claude-3-5-sonnet", + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "text"}}, + litellm_params={}, + encoding=None, + ) + + # Content should remain unchanged for text type + assert result.choices[0].message.content == '```json\n{"data": "keep me"}\n```' + + def test_transform_response_no_strip_without_response_format(self): + """transform_response should NOT strip markdown when no response_format provided.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"preserve": true}\n```'}, + "finish_reason": "stop" + }], + "model": "anthropic--claude-3-5-sonnet" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="anthropic--claude-3-5-sonnet", + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={}, # No response_format + litellm_params={}, + encoding=None, + ) + + # Content should remain unchanged when no response_format + assert result.choices[0].message.content == '```json\n{"preserve": true}\n```' + + +class TestMarkdownStripping: + """Test markdown code block stripping for JSON responses.""" + + def test_strip_markdown_json_wrapper(self): + """Should strip ```json ... ``` wrapper from content.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content='```json\n{"answer": 4}\n```'), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response) + assert result.choices[0].message.content == '{"answer": 4}' + + def test_strip_plain_markdown_wrapper(self): + """Should strip ``` ... ``` wrapper (without json label).""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content='```\n{"answer": 4}\n```'), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response) + assert result.choices[0].message.content == '{"answer": 4}' + + def test_no_strip_when_no_markdown(self): + """Should not modify content without markdown wrapper.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content='{"answer": 4}'), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response) + assert result.choices[0].message.content == '{"answer": 4}' + + def test_strip_only_for_json_response_format(self): + """Should only strip for json_object or json_schema types, not text.""" + config = GenAIHubOrchestrationConfig() + + # json_schema type should trigger stripping + assert config.get_supported_openai_params("anthropic--claude-3-5-sonnet") + # The actual stripping is tested in transform_response, which checks type + + def test_strip_multiple_choices(self): + """Should strip markdown from all choices, not just the first.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + response = ModelResponse( + id="test", + choices=[ + Choices( + index=0, + message=Message(role="assistant", content='```json\n{"choice": 0}\n```'), + finish_reason="stop" + ), + Choices( + index=1, + message=Message(role="assistant", content='```json\n{"choice": 1}\n```'), + finish_reason="stop" + ), + Choices( + index=2, + message=Message(role="assistant", content='```\n{"choice": 2}\n```'), + finish_reason="stop" + ), + ], + model="test" + ) + + result = config._strip_markdown_json(response) + assert result.choices[0].message.content == '{"choice": 0}' + assert result.choices[1].message.content == '{"choice": 1}' + assert result.choices[2].message.content == '{"choice": 2}' + + def test_strip_with_whitespace_variations(self): + """Should handle various whitespace patterns around JSON.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + + # Test with extra spaces and different newline styles + test_cases = [ + ('```json\n{"a":1}\n```', '{"a":1}'), # Standard + ('```json\n {"a":1} \n```', '{"a":1}'), # Extra spaces inside + (' ```json\n{"a":1}\n``` ', '{"a":1}'), # Extra spaces outside (stripped by .strip()) + ('```json\n\n{"a":1}\n\n```', '{"a":1}'), # Extra newlines + ] + + for input_content, expected in test_cases: + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content=input_content), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response) + assert result.choices[0].message.content == expected, f"Failed for input: {repr(input_content)}" + + def test_no_strip_partial_markdown(self): + """Should not corrupt content with incomplete markdown (only opening ```).""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + + # Only opening backticks - should be preserved + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content='```json\n{"incomplete": true}'), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response) + # Should remain unchanged since there's no closing ``` + assert result.choices[0].message.content == '```json\n{"incomplete": true}' + + def test_preserve_markdown_in_json_value(self): + """Should preserve markdown code blocks inside JSON string values.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + + # JSON with markdown inside a string value - only outer wrapper should be stripped + content_with_nested = '```json\n{"code": "```python\\nprint(1)\\n```"}\n```' + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content=content_with_nested), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response) + # Only the outer wrapper should be stripped, inner markdown preserved + assert result.choices[0].message.content == '{"code": "```python\\nprint(1)\\n```"}' + + +class TestResponseFormatErrorHandling: + """Test error handling in response_format processing.""" + + def test_empty_content_handling(self): + """_strip_markdown_json should handle None/empty content gracefully.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + + # Test with None content + response_none = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content=None), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response_none) + assert result.choices[0].message.content is None + + # Test with empty string content + response_empty = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content=""), + finish_reason="stop" + )], + model="test" + ) + + result = config._strip_markdown_json(response_empty) + assert result.choices[0].message.content == "" + + def test_response_format_with_no_choices(self): + """_strip_markdown_json should handle response with empty choices.""" + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + # Empty choices list + response = ModelResponse( + id="test", + choices=[], + model="test" + ) + + # Should not raise an error + result = config._strip_markdown_json(response) + assert result.choices == [] + + def test_response_format_with_message_no_content(self): + """_strip_markdown_json should handle choice with message but no content.""" + from litellm.types.utils import ModelResponse, Choices, Message + + config = GenAIHubOrchestrationConfig() + + # Choice with message but content is None + response = ModelResponse( + id="test", + choices=[Choices( + index=0, + message=Message(role="assistant", content=None), + finish_reason="stop" + )], + model="test" + ) + + # Should not raise an error and content should remain None + result = config._strip_markdown_json(response) + assert result.choices[0].message.content is None + + +class TestStrictParameterFiltering: + """Test that strict parameter is filtered from model_params. + + LangChain agents pass strict=true at the top level of optional_params, + but SAP AI Core Orchestration API does not accept it as a model parameter + for GPT models. The strict parameter should only exist inside + response_format.json_schema, not as a top-level model param. + """ + + def test_strict_param_filtered_from_model_params(self): + """strict should be filtered out and not appear in model.params.""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"strict": True, "temperature": 0.7}, + litellm_params={}, + headers={}, + ) + + # strict should NOT be in model.params + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + # Other params should still be there + assert model_params.get("temperature") == 0.7 + + def test_strict_preserved_inside_response_format_json_schema(self): + """strict inside response_format.json_schema should be preserved.""" + config = GenAIHubOrchestrationConfig() + + response_format = { + "type": "json_schema", + "json_schema": { + "name": "test_schema", + "strict": True, # This is the correct location for strict + "schema": { + "type": "object", + "properties": {"result": {"type": "string"}}, + "required": ["result"] + } + } + } + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"response_format": response_format}, + litellm_params={}, + headers={}, + ) + + # strict should be preserved inside json_schema + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert prompt_config["response_format"]["json_schema"]["strict"] is True + + def test_langchain_style_strict_filtered_with_response_format(self): + """LangChain sends strict at top level AND inside json_schema - only top level filtered.""" + config = GenAIHubOrchestrationConfig() + + # LangChain sends both top-level strict AND inside json_schema + response_format = { + "type": "json_schema", + "json_schema": { + "name": "agent_response", + "strict": True, + "schema": { + "type": "object", + "properties": {"answer": {"type": "string"}} + } + } + } + + request = config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + optional_params={ + "strict": True, # Top-level strict from LangChain - should be filtered + "response_format": response_format, + "temperature": 0.5 + }, + litellm_params={}, + headers={}, + ) + + # Top-level strict should NOT be in model.params + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + assert "strict" not in model_params + assert model_params.get("temperature") == 0.5 + + # strict inside json_schema should be preserved + prompt_config = request["config"]["modules"]["prompt_templating"]["prompt"] + assert prompt_config["response_format"]["json_schema"]["strict"] is True + + def test_strict_preserved_for_anthropic_models(self): + """strict should be preserved for Anthropic models (SAP API accepts it).""" + config = GenAIHubOrchestrationConfig() + + request = config.transform_request( + model="anthropic--claude-3-5-sonnet", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"strict": True, "max_tokens": 1000}, + litellm_params={}, + headers={}, + ) + + model_params = request["config"]["modules"]["prompt_templating"]["model"]["params"] + # Anthropic models CAN have strict in model.params (SAP API accepts it) + assert model_params.get("strict") is True + assert model_params.get("max_tokens") == 1000 + + +class TestModelVariantSupport: + """Test response_format support for various model variants.""" + + def test_gpt4_turbo_supports_response_format(self): + """gpt-4-turbo should support response_format (native support).""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("gpt-4-turbo") + assert "response_format" in params + + def test_mistral_model_support(self): + """Mistral models should support response_format (native support).""" + config = GenAIHubOrchestrationConfig() + params = config.get_supported_openai_params("mistral-large") + assert "response_format" in params + + +class TestMarkdownStrippingModelGating: + """Test that markdown stripping is only applied to Anthropic models. + + The markdown stripping behavior is specific to Anthropic models on SAP GenAI Hub. + GPT/Gemini models don't exhibit this behavior, so stripping should be gated + to avoid accidentally modifying valid responses. + """ + + def test_gpt_model_no_markdown_strip_json_schema(self): + """GPT models should NOT have markdown stripped for json_schema response_format.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + # GPT response with markdown-wrapped JSON + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"result": "success"}\n```'}, + "finish_reason": "stop" + }], + "model": "gpt-4o" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + response_format = { + "type": "json_schema", + "json_schema": {"name": "test", "schema": {"type": "object"}} + } + + result = config.transform_response( + model="gpt-4o", # GPT model - should NOT strip + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": response_format}, + litellm_params={}, + encoding=None, + ) + + # Markdown should NOT be stripped for GPT models + assert result.choices[0].message.content == '```json\n{"result": "success"}\n```' + + def test_gpt_model_no_markdown_strip_json_object(self): + """GPT models should NOT have markdown stripped for json_object response_format.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"answer": 42}\n```'}, + "finish_reason": "stop" + }], + "model": "gpt-4o" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="gpt-4o", # GPT model - should NOT strip + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "json_object"}}, + litellm_params={}, + encoding=None, + ) + + # Markdown should NOT be stripped for GPT models + assert result.choices[0].message.content == '```json\n{"answer": 42}\n```' + + def test_gemini_model_no_markdown_strip(self): + """Gemini models should NOT have markdown stripped.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"data": "gemini"}\n```'}, + "finish_reason": "stop" + }], + "model": "gemini-1.5-pro" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="gemini-1.5-pro", # Gemini model - should NOT strip + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "json_object"}}, + litellm_params={}, + encoding=None, + ) + + # Markdown should NOT be stripped for Gemini models + assert result.choices[0].message.content == '```json\n{"data": "gemini"}\n```' + + def test_mistral_model_no_markdown_strip(self): + """Mistral models should NOT have markdown stripped.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"model": "mistral"}\n```'}, + "finish_reason": "stop" + }], + "model": "mistral-large" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="mistral-large", # Mistral model - should NOT strip + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "json_schema", "json_schema": {"name": "test", "schema": {}}}}, + litellm_params={}, + encoding=None, + ) + + # Markdown should NOT be stripped for Mistral models + assert result.choices[0].message.content == '```json\n{"model": "mistral"}\n```' + + def test_anthropic_model_still_strips_markdown(self): + """Anthropic models should still have markdown stripped (existing behavior).""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"result": "anthropic"}\n```'}, + "finish_reason": "stop" + }], + "model": "anthropic--claude-3-5-sonnet" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="anthropic--claude-3-5-sonnet", # Anthropic - SHOULD strip + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "json_schema", "json_schema": {"name": "test", "schema": {}}}}, + litellm_params={}, + encoding=None, + ) + + # Markdown SHOULD be stripped for Anthropic models + assert result.choices[0].message.content == '{"result": "anthropic"}' + + def test_anthropic_claude_4_strips_markdown(self): + """Claude 4 models should have markdown stripped.""" + from unittest.mock import MagicMock + from litellm.types.utils import ModelResponse + + config = GenAIHubOrchestrationConfig() + + raw_response = MagicMock() + raw_response.json.return_value = { + "final_result": { + "id": "test-id", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": '```json\n{"model": "claude-4"}\n```'}, + "finish_reason": "stop" + }], + "model": "anthropic--claude-4.5-sonnet" + } + } + raw_response.text = '{"final_result": {...}}' + + logging_obj = MagicMock() + + result = config.transform_response( + model="anthropic--claude-4.5-sonnet", # Claude 4 Anthropic - SHOULD strip + raw_response=raw_response, + model_response=ModelResponse(id="test", model="test"), + logging_obj=logging_obj, + request_data={}, + messages=[{"role": "user", "content": "test"}], + optional_params={"response_format": {"type": "json_object"}}, + litellm_params={}, + encoding=None, + ) + + # Markdown SHOULD be stripped for Anthropic models + assert result.choices[0].message.content == '{"model": "claude-4"}' diff --git a/tests/test_litellm/llms/sap/chat/test_sap_tool_parameters.py b/tests/test_litellm/llms/sap/chat/test_sap_tool_parameters.py new file mode 100644 index 00000000000..792d2f3fe6b --- /dev/null +++ b/tests/test_litellm/llms/sap/chat/test_sap_tool_parameters.py @@ -0,0 +1,160 @@ +""" +Test SAP Tool Parameters Validation + +This test ensures that tools passed to the SAP Orchestration Service +have `parameters.type` set to 'object' as required by the SAP API. + +Issue: SAP GenAI Hub Orchestration Service rejects tools with: +"tools.0.custom.input_schema.type: Input should be 'object'" +""" + +import pytest + +from litellm.llms.sap.chat.models import FunctionTool, ChatCompletionTool + + +class TestFunctionToolParametersValidation: + """Test that FunctionTool ensures parameters has type='object'.""" + + def test_should_add_type_object_when_parameters_empty(self): + """Empty parameters should get type='object' and properties={}.""" + tool = FunctionTool(name="test_tool", parameters={}) + + assert tool.parameters.get("type") == "object" + assert "properties" in tool.parameters + + def test_should_add_type_object_when_parameters_missing_type(self): + """Parameters without type should get type='object' added.""" + tool = FunctionTool( + name="test_tool", + parameters={"properties": {"query": {"type": "string"}}} + ) + + assert tool.parameters.get("type") == "object" + assert tool.parameters.get("properties") == {"query": {"type": "string"}} + + def test_should_preserve_existing_type_object(self): + """Parameters with type='object' should be preserved.""" + tool = FunctionTool( + name="test_tool", + parameters={ + "type": "object", + "properties": {"query": {"type": "string"}} + } + ) + + assert tool.parameters.get("type") == "object" + assert tool.parameters.get("properties") == {"query": {"type": "string"}} + + def test_should_add_properties_when_missing(self): + """Parameters without properties should get properties={} added.""" + tool = FunctionTool( + name="test_tool", + parameters={"type": "object"} + ) + + assert tool.parameters.get("type") == "object" + assert "properties" in tool.parameters + + def test_should_preserve_additional_schema_properties(self): + """Additional JSON schema properties should be preserved.""" + tool = FunctionTool( + name="test_tool", + parameters={ + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + "additionalProperties": False + } + ) + + assert tool.parameters.get("type") == "object" + assert tool.parameters.get("required") == ["query"] + assert tool.parameters.get("additionalProperties") is False + + +class TestChatCompletionToolValidation: + """Test that ChatCompletionTool correctly validates nested FunctionTool.""" + + def test_should_validate_empty_parameters_in_function(self): + """ChatCompletionTool with empty parameters should get type='object'.""" + tool_dict = { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web", + "parameters": {} + } + } + completion_tool = ChatCompletionTool(**tool_dict) + + assert completion_tool.function.parameters.get("type") == "object" + assert "properties" in completion_tool.function.parameters + + def test_should_validate_missing_type_in_function_parameters(self): + """ChatCompletionTool function parameters without type should get type='object'.""" + tool_dict = { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web", + "parameters": { + "properties": { + "query": {"type": "string", "description": "Search query"} + } + } + } + } + completion_tool = ChatCompletionTool(**tool_dict) + + assert completion_tool.function.parameters.get("type") == "object" + + +class TestToolTransformationIntegration: + """Test the full tool transformation flow similar to SAP transformation.py.""" + + def test_should_transform_openai_format_tool_correctly(self): + """Simulate transformation.py tool validation flow.""" + from litellm.llms.sap.chat.transformation import validate_dict + + # OpenAI format tool with empty parameters (common case that was failing) + openai_tool = { + "type": "function", + "function": { + "name": "web_search", + "description": "Perform a web search" + } + } + + validated_tool = validate_dict(openai_tool, ChatCompletionTool) + + # After validation, parameters should have type='object' + assert validated_tool["function"]["parameters"]["type"] == "object" + assert "properties" in validated_tool["function"]["parameters"] + + def test_should_transform_tool_with_existing_parameters(self): + """Tool with parameters should preserve them while ensuring type='object'.""" + from litellm.llms.sap.chat.transformation import validate_dict + + openai_tool = { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather for a location", + "parameters": { + "properties": { + "location": { + "type": "string", + "description": "City name" + } + }, + "required": ["location"] + } + } + } + + validated_tool = validate_dict(openai_tool, ChatCompletionTool) + + assert validated_tool["function"]["parameters"]["type"] == "object" + assert "location" in validated_tool["function"]["parameters"]["properties"] + assert validated_tool["function"]["parameters"]["required"] == ["location"] From 9a4bacd85d8d2e66ca33318cc3e21178e84ae1c9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 16:07:05 -0800 Subject: [PATCH 151/171] fix: add missing spec_path column to LiteLLM_MCPServerTable schema (#22820) The OpenAPI-to-MCP feature (PR #21575) added spec_path to the code (_types.py, mcp_server_manager.py) but missed adding the column to the Prisma schema files. This causes "Could not find field spec_path" errors when creating OpenAPI-based MCP servers via the UI or API. Adds `spec_path String?` to LiteLLM_MCPServerTable in all three schema files (root, litellm/proxy, litellm-proxy-extras). Made-with: Cursor --- litellm-proxy-extras/litellm_proxy_extras/schema.prisma | 1 + litellm/proxy/schema.prisma | 1 + schema.prisma | 1 + 3 files changed, 3 insertions(+) diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 5abe7a0a2b1..6d20a71f039 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -277,6 +277,7 @@ model LiteLLM_MCPServerTable { alias String? description String? url String? + spec_path String? transport String @default("sse") auth_type String? credentials Json? @default("{}") diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 25ee2750548..f7c07112417 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -277,6 +277,7 @@ model LiteLLM_MCPServerTable { alias String? description String? url String? + spec_path String? transport String @default("sse") auth_type String? credentials Json? @default("{}") diff --git a/schema.prisma b/schema.prisma index 25ee2750548..f7c07112417 100644 --- a/schema.prisma +++ b/schema.prisma @@ -277,6 +277,7 @@ model LiteLLM_MCPServerTable { alias String? description String? url String? + spec_path String? transport String @default("sse") auth_type String? credentials Json? @default("{}") From 28fe9fabae30240ebd73ec576b8222ca730e48f6 Mon Sep 17 00:00:00 2001 From: tombii Date: Thu, 5 Mar 2026 01:18:49 +0100 Subject: [PATCH 152/171] fix: complexity_router crashes on list-format message content (OpenAI multi-part messages) (#22761) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: complexity_router fails on list-format message content (OpenAI multi-part messages) When a client sends messages with list-format content (e.g. [{"type": "text", "text": "..."}] as used by the OpenAI JS SDK and other clients), the complexity_router's async_pre_routing_hook skipped those messages because it only handled str content. This caused user_message to be None, the hook returned None, and the router fell through to selecting the complexity_router deployment itself (model="auto_router/complexity_router") which litellm cannot dispatch, resulting in LiteLLMUnknownProvider. Fixes: - Extract text from list-format content parts (type=text) before classifying - Return default_model instead of None when no user message can be extracted, preventing the crash fallthrough - Loosen PreRoutingHookResponse.messages type from Dict[str, str] to Dict[str, Any] to accommodate list-format content values Co-Authored-By: Claude Sonnet 4.6 * fix: update messages type annotation in async_pre_routing_hook to Dict[str, Any] Consistent with PreRoutingHookResponse.messages type change and the list-format content support added in the previous commit. Co-Authored-By: Claude Sonnet 4.6 * fix: normalize None content to empty string in complexity_router message parsing msg.get("content", "") returns None when the key exists with value None (e.g. assistant messages with tool calls). Use `or ""` to normalize None to an empty string explicitly. Co-Authored-By: Claude Sonnet 4.6 * fix: strip whitespace from joined list content parts in complexity_router Prevents leading/trailing spaces when some content parts have empty text values (e.g. " ".join(["", "hello"]) → " hello"). Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../complexity_router/complexity_router.py | 19 ++++++-- litellm/types/router.py | 2 +- .../router_strategy/test_complexity_router.py | 45 ++++++++++++++----- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index c4e0c55adc8..6ad21606669 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -326,7 +326,7 @@ class ComplexityRouter(CustomLogger): self, model: str, request_kwargs: Dict, - messages: Optional[List[Dict[str, str]]] = None, + messages: Optional[List[Dict[str, Any]]] = None, input: Optional[Union[str, List]] = None, specific_deployment: Optional[bool] = False, ) -> Optional["PreRoutingHookResponse"]: @@ -359,7 +359,15 @@ class ComplexityRouter(CustomLogger): for msg in reversed(messages): role = msg.get("role", "") - content = msg.get("content", "") + content = msg.get("content") or "" + # content may be a list of content parts (e.g. [{"type": "text", "text": "..."}]) + if isinstance(content, list): + text_parts = [ + part.get("text", "") + for part in content + if isinstance(part, dict) and part.get("type") == "text" + ] + content = " ".join(text_parts).strip() if isinstance(content, str) and content: if role == "user" and user_message is None: user_message = content @@ -368,9 +376,12 @@ class ComplexityRouter(CustomLogger): if user_message is None: verbose_router_logger.debug( - "ComplexityRouter: No user message found, skipping routing" + "ComplexityRouter: No user message found, routing to default model" + ) + return PreRoutingHookResponse( + model=self.config.default_model or self.get_model_for_tier(ComplexityTier.MEDIUM), + messages=messages, ) - return None # Classify the request tier, score, signals = self.classify(user_message, system_prompt) diff --git a/litellm/types/router.py b/litellm/types/router.py index fca731d1f91..d917d845ad2 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -866,4 +866,4 @@ class PreRoutingHookResponse(BaseModel): """ model: str - messages: Optional[List[Dict[str, str]]] + messages: Optional[List[Dict[str, Any]]] diff --git a/tests/test_litellm/router_strategy/test_complexity_router.py b/tests/test_litellm/router_strategy/test_complexity_router.py index 8282bc7199f..2ca823f6a12 100644 --- a/tests/test_litellm/router_strategy/test_complexity_router.py +++ b/tests/test_litellm/router_strategy/test_complexity_router.py @@ -483,7 +483,7 @@ class TestAsyncPreRoutingHookEdgeCases: @pytest.mark.asyncio async def test_pre_routing_hook_no_user_message(self, complexity_router): - """Test pre-routing hook returns None when no user message found.""" + """Test pre-routing hook falls back to default model when no user message found.""" messages = [ {"role": "system", "content": "You are helpful."}, {"role": "assistant", "content": "Hello!"}, @@ -493,21 +493,45 @@ class TestAsyncPreRoutingHookEdgeCases: request_kwargs={}, messages=messages, ) - assert result is None + # Should return default model rather than None (None would cause + # the complexity_router deployment itself to be selected, crashing) + assert result is not None + assert result.model in ["gpt-4o-mini", "gpt-4o", "claude-sonnet-4-20250514", "o1-preview"] @pytest.mark.asyncio - async def test_pre_routing_hook_only_list_content(self, complexity_router): - """Test pre-routing hook returns None when all user content is list type.""" + async def test_pre_routing_hook_list_content(self, complexity_router): + """Test pre-routing hook handles list-format message content (OpenAI multi-part format).""" messages = [ - {"role": "user", "content": [{"type": "text", "text": "Hello"}]}, + {"role": "user", "content": [{"type": "text", "text": "Hello, how are you?"}]}, ] result = await complexity_router.async_pre_routing_hook( model="test-model", request_kwargs={}, messages=messages, ) - # Should return None since we can't extract string content - assert result is None + # Should extract text from list content and classify normally + assert result is not None + assert result.model == "gpt-4o-mini" # "Hello, how are you?" is SIMPLE + + @pytest.mark.asyncio + async def test_pre_routing_hook_list_content_complex(self, complexity_router): + """Test pre-routing hook classifies list-format content by complexity.""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Think step by step and reason through this: design a distributed system"}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}, + ], + } + ] + result = await complexity_router.async_pre_routing_hook( + model="test-model", + request_kwargs={}, + messages=messages, + ) + assert result is not None + assert result.model == "o1-preview" # REASONING tier @pytest.mark.asyncio async def test_pre_routing_hook_preserves_messages(self, complexity_router): @@ -526,7 +550,7 @@ class TestAsyncPreRoutingHookEdgeCases: @pytest.mark.asyncio async def test_pre_routing_hook_empty_string_content(self, complexity_router): - """Test pre-routing hook returns None for empty string content.""" + """Test pre-routing hook falls back to default model for empty string content.""" messages = [ {"role": "user", "content": ""}, ] @@ -535,8 +559,9 @@ class TestAsyncPreRoutingHookEdgeCases: request_kwargs={}, messages=messages, ) - # Empty string content is treated as "no user message found" - assert result is None + # Empty string content → no extractable user message → routes to default model + assert result is not None + assert result.model in ["gpt-4o-mini", "gpt-4o", "claude-sonnet-4-20250514", "o1-preview"] class TestSingletonMutation: From 614a9fe9b70cad259b6a3a5f2008ca1e4be8cd9f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 17:14:18 -0800 Subject: [PATCH 153/171] Fix MCP server search in playground (#22824) - Added showSearch prop to enable search input in MCP server selector - Added filterOption to search across server name, alias, server_id, and description - Search is case-insensitive and filters in real-time - Added test to verify search input appears when dropdown opens - Updated tsconfig.json with Next.js auto-configuration (jsx: react-jsx) Fixes issue where MCP server search was not working in the playground. Co-authored-by: Cursor Agent Co-authored-by: Ishaan Jaff --- .../playground/chat_ui/ChatUI.test.tsx | 39 +++++++++++++++++++ .../components/playground/chat_ui/ChatUI.tsx | 18 +++++++++ ui/litellm-dashboard/tsconfig.json | 2 +- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx index e00f27c39f4..330ea4dee32 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx @@ -373,4 +373,43 @@ describe("ChatUI", () => { const customProxyInput = screen.getByPlaceholderText("Optional: Enter custom proxy URL (e.g., http://localhost:5000)"); expect(customProxyInput).toHaveValue(testProxyUrl); }); + + it("should enable search functionality for MCP server selector", async () => { + render( + + ); + + await waitFor(() => { + expect(screen.getByText("Test Key")).toBeInTheDocument(); + }); + + const mcpServersText = screen.queryByText("MCP Servers"); + expect(mcpServersText).toBeInTheDocument(); + + if (mcpServersText) { + const selectContainer = mcpServersText.parentElement?.nextElementSibling; + const selectElement = selectContainer?.querySelector(".ant-select-selector"); + expect(selectElement).toBeInTheDocument(); + + if (selectElement) { + fireEvent.mouseDown(selectElement); + + await waitFor(() => { + const allServersOption = screen.queryByText("All MCP Servers"); + if (allServersOption) { + expect(allServersOption).toBeInTheDocument(); + } + }); + + const searchInput = document.querySelector(".ant-select-selection-search-input"); + expect(searchInput).toBeInTheDocument(); + } + } + }); }); diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx index df04eece289..3272e9b589a 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -1636,9 +1636,27 @@ const ChatUI: React.FC = ({ loading={isLoadingMCPServers} className="mb-2" allowClear + showSearch optionLabelProp="label" disabled={!MCP_SUPPORTED_ENDPOINTS.has(endpointType as EndpointType)} maxTagCount={endpointType === EndpointType.MCP ? 1 : "responsive"} + filterOption={(input, option) => { + if (option?.value === "__all__") { + return "All MCP Servers".toLowerCase().includes(input.toLowerCase()); + } + const server = mcpServers.find((s) => s.server_id === option?.value); + if (!server) return false; + const searchText = [ + server.server_name, + server.alias, + server.server_id, + server.description, + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return searchText.includes(input.toLowerCase()); + }} > {/* All MCP Servers option - hidden for MCP direct mode */} {endpointType !== EndpointType.MCP && ( diff --git a/ui/litellm-dashboard/tsconfig.json b/ui/litellm-dashboard/tsconfig.json index 5b0352feb98..d24bdd340f7 100644 --- a/ui/litellm-dashboard/tsconfig.json +++ b/ui/litellm-dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { From e3810b4009f00b30cb1a002977d6924d3d68f081 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 17:19:07 -0800 Subject: [PATCH 154/171] feat: make model selection optional when creating virtual keys (#22826) - Remove required validation for models field in create_key_button.tsx - Update help text to clarify models are optional - If no models selected, key will have access to all models - This allows users to create keys for MCP-only access without selecting LLM models - Fixes LIT-1791: Cannot create virtual key without LLM provider if user only has MCP access Backend already supports empty models list (defaults to all models). This change only updates the UI source to match backend behavior. Co-authored-by: Cursor Agent Co-authored-by: Ishaan Jaff --- .../src/components/organisms/create_key_button.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 19a07c49a3d..c54725bd5a5 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -772,21 +772,17 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => { label={ Models{" "} - + } name="models" - rules={ - keyType === "management" || keyType === "read_only" - ? [] - : [{ required: true, message: "Please select a model" }] - } + rules={[]} help={ keyType === "management" || keyType === "read_only" ? "Models field is disabled for this key type" - : "required" + : "optional - leave empty to allow access to all models" } className="mt-4" > From e34960b3f0d485e940e10a119561527ab3248cc7 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 17:26:11 -0800 Subject: [PATCH 155/171] [Test] UI - Projects: add Vitest unit tests for all Projects components Adds 77 tests across 8 files covering ProjectsPage, ProjectDetailsPage, ProjectKeysSection, ProjectKeysTable, CreateProjectModal, EditProjectModal, ProjectBaseForm, and projectFormUtils. Co-Authored-By: Claude Sonnet 4.6 --- .../Projects/ProjectDetailsPage.test.tsx | 180 ++++++++++++++++++ .../Projects/ProjectKeysSection.test.tsx | 72 +++++++ .../Projects/ProjectKeysTable.test.tsx | 128 +++++++++++++ .../ProjectModals/CreateProjectModal.test.tsx | 54 ++++++ .../ProjectModals/EditProjectModal.test.tsx | 87 +++++++++ .../ProjectModals/ProjectBaseForm.test.tsx | 89 +++++++++ .../ProjectModals/projectFormUtils.test.ts | 101 ++++++++++ .../components/Projects/ProjectsPage.test.tsx | 162 ++++++++++++++++ 8 files changed, 873 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectDetailsPage.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectKeysSection.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectKeysTable.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectModals/CreateProjectModal.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectModals/EditProjectModal.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectModals/ProjectBaseForm.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectModals/projectFormUtils.test.ts create mode 100644 ui/litellm-dashboard/src/components/Projects/ProjectsPage.test.tsx diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectDetailsPage.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectDetailsPage.test.tsx new file mode 100644 index 00000000000..f9614fdabb9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectDetailsPage.test.tsx @@ -0,0 +1,180 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { ProjectDetail } from "./ProjectDetailsPage"; +import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects"; + +const mockUseProjectDetails = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/projects/useProjectDetails", () => ({ + useProjectDetails: (id: string) => mockUseProjectDetails(id), +})); + +const mockUseTeam = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + useTeam: (id?: string) => mockUseTeam(id), +})); + +vi.mock("./ProjectModals/EditProjectModal", () => ({ + EditProjectModal: ({ isOpen }: { isOpen: boolean }) => + isOpen ?

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx index 25c20dd0746..87f14021f46 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { Card, Title, Text } from "@tremor/react"; -import { ToolOutlined, CheckCircleOutlined, SearchOutlined } from "@ant-design/icons"; +import { ToolOutlined, CheckCircleOutlined, SearchOutlined, EditOutlined } from "@ant-design/icons"; import { Badge, Spin, Checkbox, Input } from "antd"; import { useTestMCPConnection } from "../../hooks/useTestMCPConnection"; @@ -11,6 +11,10 @@ interface MCPToolConfigurationProps { allowedTools: string[]; existingAllowedTools: string[] | null; onAllowedToolsChange: (tools: string[]) => void; + toolNameToDisplayName: Record; + toolNameToDescription: Record; + onToolNameToDisplayNameChange: (map: Record) => void; + onToolNameToDescriptionChange: (map: Record) => void; } const MCPToolConfiguration: React.FC = ({ @@ -20,10 +24,15 @@ const MCPToolConfiguration: React.FC = ({ allowedTools, existingAllowedTools, onAllowedToolsChange, + toolNameToDisplayName, + toolNameToDescription, + onToolNameToDisplayNameChange, + onToolNameToDescriptionChange, }) => { const previousToolsRef = useRef([]); const [toolSearchTerm, setToolSearchTerm] = useState(""); const hasInitializedRef = useRef(false); + const [expandedTools, setExpandedTools] = useState>(new Set()); const { tools, isLoadingTools, toolsError, canFetchTools } = useTestMCPConnection({ accessToken, @@ -96,6 +105,39 @@ const MCPToolConfiguration: React.FC = ({ onAllowedToolsChange([]); }; + const handleToggleEditExpanded = (toolName: string, e: React.MouseEvent) => { + e.stopPropagation(); + setExpandedTools((prev) => { + const next = new Set(prev); + if (next.has(toolName)) { + next.delete(toolName); + } else { + next.add(toolName); + } + return next; + }); + }; + + const handleDisplayNameChange = (toolName: string, value: string) => { + const next = { ...toolNameToDisplayName }; + if (value) { + next[toolName] = value; + } else { + delete next[toolName]; + } + onToolNameToDisplayNameChange(next); + }; + + const handleDescriptionChange = (toolName: string, value: string) => { + const next = { ...toolNameToDescription }; + if (value) { + next[toolName] = value; + } else { + delete next[toolName]; + } + onToolNameToDescriptionChange(next); + }; + // Don't show anything if required fields aren't filled if (!canFetchTools && !formValues.url && !formValues.spec_path) { return null; @@ -213,39 +255,106 @@ const MCPToolConfiguration: React.FC = ({ No tools found matching "{toolSearchTerm}"
: null, +})); + +vi.mock("@/components/common_components/DefaultProxyAdminTag", () => ({ + default: ({ userId }: { userId: string }) => {userId}, +})); + +const mockProject: ProjectResponse = { + project_id: "proj-1", + project_alias: "My Project", + description: "A sample project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4"], + spend: 12.5, + model_spend: { "gpt-4": 12.5 }, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-15T08:00:00Z", + created_by: "user-1", + updated_at: "2024-02-01T12:00:00Z", + updated_by: "user-2", + litellm_budget_table: null, +}; + +describe("ProjectDetail", () => { + const onBack = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + mockUseTeam.mockReturnValue({ data: undefined, isLoading: false }); + }); + + describe("when loading", () => { + it("should show a loading spinner", () => { + mockUseProjectDetails.mockReturnValue({ data: undefined, isLoading: true }); + renderWithProviders(); + expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument(); + }); + }); + + describe("when the project is not found", () => { + it("should display 'Project not found'", () => { + mockUseProjectDetails.mockReturnValue({ data: undefined, isLoading: false }); + renderWithProviders(); + expect(screen.getByText("Project not found")).toBeInTheDocument(); + }); + + it("should call onBack when the back button is clicked in the not-found state", async () => { + const user = userEvent.setup(); + mockUseProjectDetails.mockReturnValue({ data: undefined, isLoading: false }); + renderWithProviders(); + await user.click(screen.getByRole("button")); + expect(onBack).toHaveBeenCalledOnce(); + }); + }); + + describe("when the project loads successfully", () => { + beforeEach(() => { + mockUseProjectDetails.mockReturnValue({ data: mockProject, isLoading: false }); + }); + + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("My Project")).toBeInTheDocument(); + }); + + it("should display the project alias as the page title", () => { + renderWithProviders(); + expect(screen.getByRole("heading", { name: "My Project" })).toBeInTheDocument(); + }); + + it("should display 'Active' for a non-blocked project", () => { + renderWithProviders(); + expect(screen.getByText("Active")).toBeInTheDocument(); + }); + + it("should display 'Blocked' for a blocked project", () => { + mockUseProjectDetails.mockReturnValue({ + data: { ...mockProject, blocked: true }, + isLoading: false, + }); + renderWithProviders(); + expect(screen.getByText("Blocked")).toBeInTheDocument(); + }); + + it("should call onBack when the back button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await user.click(screen.getByRole("button", { name: "" })); + expect(onBack).toHaveBeenCalledOnce(); + }); + + it("should show the current spend amount", () => { + renderWithProviders(); + expect(screen.getByText("$12.50")).toBeInTheDocument(); + }); + + it("should show 'No budget limit' when no max budget is set", () => { + renderWithProviders(); + expect(screen.getByText("No budget limit")).toBeInTheDocument(); + }); + + it("should show the budget limit when one is set", () => { + mockUseProjectDetails.mockReturnValue({ + data: { + ...mockProject, + litellm_budget_table: { max_budget: 100 }, + }, + isLoading: false, + }); + renderWithProviders(); + expect(screen.getByText("of $100.00 budget")).toBeInTheDocument(); + }); + + it("should show the project description", () => { + renderWithProviders(); + expect(screen.getByText("A sample project")).toBeInTheDocument(); + }); + + it("should show an 'Edit Project' button", () => { + renderWithProviders(); + expect(screen.getByRole("button", { name: /edit project/i })).toBeInTheDocument(); + }); + + it("should open the edit modal when 'Edit Project' is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /edit project/i })); + expect(screen.getByTestId("edit-modal")).toBeInTheDocument(); + }); + + it("should show 'No team assigned' when the project has no team", () => { + mockUseProjectDetails.mockReturnValue({ + data: { ...mockProject, team_id: null }, + isLoading: false, + }); + renderWithProviders(); + expect(screen.getByText("No team assigned")).toBeInTheDocument(); + }); + + it("should show team information when team data is available", () => { + mockUseTeam.mockReturnValue({ + data: { + team_info: { + team_id: "team-1", + team_alias: "Engineering", + models: ["gpt-4"], + spend: 50, + members_with_roles: [], + }, + }, + isLoading: false, + }); + renderWithProviders(); + expect(screen.getByText("Engineering")).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectKeysSection.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectKeysSection.test.tsx new file mode 100644 index 00000000000..f9a2722da94 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectKeysSection.test.tsx @@ -0,0 +1,72 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { ProjectKeysSection } from "./ProjectKeysSection"; + +const mockUseKeys = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({ + useKeys: (...args: unknown[]) => mockUseKeys(...args), +})); + +vi.mock("@/components/common_components/DefaultProxyAdminTag", () => ({ + default: ({ userId }: { userId: string }) => {userId}, +})); + +const emptyKeysResponse = { + data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 }, + isLoading: false, +}; + +describe("ProjectKeysSection", () => { + it("should render", () => { + mockUseKeys.mockReturnValue(emptyKeysResponse); + renderWithProviders(); + expect(screen.getByRole("table")).toBeInTheDocument(); + }); + + it("should show the Keys card title", () => { + mockUseKeys.mockReturnValue(emptyKeysResponse); + renderWithProviders(); + expect(screen.getByText("Keys")).toBeInTheDocument(); + }); + + it("should display the total key count from the API response", () => { + mockUseKeys.mockReturnValue({ + data: { keys: [], total_count: 42, current_page: 1, total_pages: 9 }, + isLoading: false, + }); + renderWithProviders(); + expect(screen.getByText("42 keys")).toBeInTheDocument(); + }); + + it("should show 'No keys found' when the project has no keys", () => { + mockUseKeys.mockReturnValue(emptyKeysResponse); + renderWithProviders(); + expect(screen.getByText("No keys found")).toBeInTheDocument(); + }); + + it("should render a search input for filtering by key name", () => { + mockUseKeys.mockReturnValue(emptyKeysResponse); + renderWithProviders(); + expect(screen.getByPlaceholderText("Filter by key name...")).toBeInTheDocument(); + }); + + it("should call useKeys with the projectId", () => { + mockUseKeys.mockReturnValue(emptyKeysResponse); + renderWithProviders(); + expect(mockUseKeys).toHaveBeenCalledWith( + expect.any(Number), + expect.any(Number), + expect.objectContaining({ projectID: "proj-abc" }) + ); + }); + + it("should pass null for selectedKeyAlias when the filter input is empty", () => { + mockUseKeys.mockReturnValue(emptyKeysResponse); + renderWithProviders(); + expect(mockUseKeys).toHaveBeenCalledWith( + expect.any(Number), + expect.any(Number), + expect.objectContaining({ selectedKeyAlias: null }) + ); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectKeysTable.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectKeysTable.test.tsx new file mode 100644 index 00000000000..4d077a6fb3a --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectKeysTable.test.tsx @@ -0,0 +1,128 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { ProjectKeysTable } from "./ProjectKeysTable"; +import { KeyResponse } from "@/components/key_team_helpers/key_list"; + +vi.mock("@/components/common_components/DefaultProxyAdminTag", () => ({ + default: ({ userId }: { userId: string }) => {userId}, +})); + +function makeKey(overrides: Partial = {}): KeyResponse { + return { + token: "tok-abc123", + token_id: "tid-abc123", + key_name: "sk-...abc", + key_alias: "Test Key", + spend: 0, + max_budget: 0, + expires: "", + models: [], + aliases: {}, + config: {}, + user_id: null as any, + team_id: null, + project_id: null, + max_parallel_requests: 0, + metadata: {}, + tpm_limit: 0, + rpm_limit: 0, + duration: "", + budget_duration: "", + budget_reset_at: "", + allowed_cache_controls: [], + allowed_routes: [], + permissions: {}, + model_spend: {}, + model_max_budget: {}, + soft_budget_cooldown: false, + blocked: false, + litellm_budget_table: {}, + organization_id: null, + created_at: "2024-03-01T00:00:00Z", + updated_at: "2024-03-01T00:00:00Z", + last_active: null, + team_spend: 0, + team_alias: "", + team_tpm_limit: 0, + team_rpm_limit: 0, + team_max_budget: 0, + team_models: [], + team_blocked: false, + soft_budget: 0, + team_model_aliases: {}, + team_member_spend: 0, + team_metadata: {}, + end_user_id: "", + end_user_tpm_limit: 0, + end_user_rpm_limit: 0, + end_user_max_budget: 0, + last_refreshed_at: 0, + api_key: "", + user_role: "user", + rpm_limit_per_model: {}, + tpm_limit_per_model: {}, + user_tpm_limit: 0, + user_rpm_limit: 0, + user_email: "", + ...overrides, + } as KeyResponse; +} + +describe("ProjectKeysTable", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByRole("table")).toBeInTheDocument(); + }); + + it("should display 'No keys found' when the keys list is empty", () => { + renderWithProviders(); + expect(screen.getByText("No keys found")).toBeInTheDocument(); + }); + + it("should display the key alias when provided", () => { + renderWithProviders(); + expect(screen.getByText("My API Key")).toBeInTheDocument(); + }); + + it("should display '—' when the key alias is null", () => { + // Provide a user_id so only the alias column shows "—" (not the owner column too) + renderWithProviders( + + ); + expect(screen.getByText("—")).toBeInTheDocument(); + }); + + it("should display the owner using user.user_email when available", () => { + const key = makeKey({ user: { user_id: "u1", user_email: "alice@example.com" } }); + renderWithProviders(); + expect(screen.getByTestId("owner-tag")).toHaveTextContent("alice@example.com"); + }); + + it("should fall back to user_id when user.user_email is absent", () => { + const key = makeKey({ user_id: "user-99" }); + renderWithProviders(); + expect(screen.getByTestId("owner-tag")).toHaveTextContent("user-99"); + }); + + it("should display 'Never' in the Last Active column when last_active is null", () => { + renderWithProviders(); + expect(screen.getByText("Never")).toBeInTheDocument(); + }); + + it("should display a formatted date in the Last Active column when last_active is provided", () => { + renderWithProviders( + + ); + expect(screen.queryByText("Never")).not.toBeInTheDocument(); + }); + + it("should render multiple keys as separate rows", () => { + const keys = [ + makeKey({ token: "tok-1", key_alias: "Key One" }), + makeKey({ token: "tok-2", key_alias: "Key Two" }), + ]; + renderWithProviders(); + expect(screen.getByText("Key One")).toBeInTheDocument(); + expect(screen.getByText("Key Two")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectModals/CreateProjectModal.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectModals/CreateProjectModal.test.tsx new file mode 100644 index 00000000000..12fd80179b0 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectModals/CreateProjectModal.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders, screen } from "../../../../tests/test-utils"; +import { CreateProjectModal } from "./CreateProjectModal"; + +const mockMutate = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/projects/useCreateProject", () => ({ + useCreateProject: () => ({ mutate: mockMutate, isPending: false }), +})); + +// Mock the form to keep tests focused on modal behavior +vi.mock("./ProjectBaseForm", () => ({ + ProjectBaseForm: () => diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx index cf2eafd43f5..00060658ea1 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx @@ -38,6 +38,8 @@ const MCPServerEdit: React.FC = ({ const [searchValue, setSearchValue] = useState(""); const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false); const [allowedTools, setAllowedTools] = useState([]); + const [toolNameToDisplayName, setToolNameToDisplayName] = useState>({}); + const [toolNameToDescription, setToolNameToDescription] = useState>({}); const [pendingRestoredValues, setPendingRestoredValues] = useState | null>(null); const authType = Form.useWatch("auth_type", form) as string | undefined; const transportType = Form.useWatch("transport", form) as string | undefined; @@ -195,11 +197,13 @@ const MCPServerEdit: React.FC = ({ } }, [mcpServer]); - // Initialize allowed tools from existing server data + // Initialize allowed tools and tool overrides from existing server data useEffect(() => { if (mcpServer.allowed_tools) { setAllowedTools(mcpServer.allowed_tools); } + setToolNameToDisplayName(mcpServer.tool_name_to_display_name ?? {}); + setToolNameToDescription(mcpServer.tool_name_to_description ?? {}); }, [mcpServer]); useEffect(() => { @@ -541,6 +545,8 @@ const MCPServerEdit: React.FC = ({ // Include permission management fields extra_headers: restValues.extra_headers || [], allowed_tools: allowedTools.length > 0 ? allowedTools : null, + tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null, + tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null, disallowed_tools: restValues.disallowed_tools || [], static_headers: staticHeaders, allow_all_keys: Boolean(allowAllKeysRaw ?? mcpServer.allow_all_keys), @@ -906,6 +912,10 @@ const MCPServerEdit: React.FC = ({ allowedTools={allowedTools} existingAllowedTools={mcpServer.allowed_tools || null} onAllowedToolsChange={setAllowedTools} + toolNameToDisplayName={toolNameToDisplayName} + toolNameToDescription={toolNameToDescription} + onToolNameToDisplayNameChange={setToolNameToDisplayName} + onToolNameToDescriptionChange={setToolNameToDescription} />
, +})); + +describe("CreateProjectModal", () => { + const onClose = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should not render modal content when closed", () => { + renderWithProviders(); + expect(screen.queryByText("Create New Project")).not.toBeInTheDocument(); + }); + + it("should render the modal when open", () => { + renderWithProviders(); + expect(screen.getByText("Create New Project")).toBeInTheDocument(); + }); + + it("should show a 'Create Project' submit button", () => { + renderWithProviders(); + expect(screen.getByRole("button", { name: /create project/i })).toBeInTheDocument(); + }); + + it("should show a 'Cancel' button", () => { + renderWithProviders(); + expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument(); + }); + + it("should call onClose when the Cancel button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /cancel/i })); + expect(onClose).toHaveBeenCalledOnce(); + }); + + it("should render the project form inside the modal", () => { + renderWithProviders(); + expect(screen.getByTestId("project-base-form")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectModals/EditProjectModal.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectModals/EditProjectModal.test.tsx new file mode 100644 index 00000000000..905dbcd985e --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectModals/EditProjectModal.test.tsx @@ -0,0 +1,87 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders, screen } from "../../../../tests/test-utils"; +import { EditProjectModal } from "./EditProjectModal"; +import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects"; + +const mockMutate = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/projects/useUpdateProject", () => ({ + useUpdateProject: () => ({ mutate: mockMutate, isPending: false }), +})); + +vi.mock("./ProjectBaseForm", () => ({ + ProjectBaseForm: () =>
, +})); + +const mockProject: ProjectResponse = { + project_id: "proj-1", + project_alias: "My Project", + description: "A test project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4"], + spend: 10.0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-02T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, +}; + +describe("EditProjectModal", () => { + const onClose = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should not render modal content when closed", () => { + renderWithProviders( + + ); + expect(screen.queryByText("Edit Project")).not.toBeInTheDocument(); + }); + + it("should render the modal when open", () => { + renderWithProviders( + + ); + expect(screen.getByText("Edit Project")).toBeInTheDocument(); + }); + + it("should show a 'Save Changes' submit button", () => { + renderWithProviders( + + ); + expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument(); + }); + + it("should show a 'Cancel' button", () => { + renderWithProviders( + + ); + expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument(); + }); + + it("should call onClose when the Cancel button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + await user.click(screen.getByRole("button", { name: /cancel/i })); + expect(onClose).toHaveBeenCalledOnce(); + }); + + it("should render the project form inside the modal", () => { + renderWithProviders( + + ); + expect(screen.getByTestId("project-base-form")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectModals/ProjectBaseForm.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectModals/ProjectBaseForm.test.tsx new file mode 100644 index 00000000000..04e3ed64f47 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectModals/ProjectBaseForm.test.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders, screen, waitFor } from "../../../../tests/test-utils"; +import { Form } from "antd"; +import { ProjectBaseForm, ProjectFormValues } from "./ProjectBaseForm"; + +const mockUseTeams = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + useTeams: () => mockUseTeams(), +})); + +vi.mock("@/components/organisms/create_key_button", () => ({ + fetchTeamModels: vi.fn().mockResolvedValue([]), +})); + +vi.mock("@/components/key_team_helpers/fetch_available_models_team_key", () => ({ + getModelDisplayName: (model: string) => model, +})); + +function FormWrapper() { + const [form] = Form.useForm(); + return ; +} + +describe("ProjectBaseForm", () => { + beforeEach(() => { + mockUseTeams.mockReturnValue({ data: [], isLoading: false }); + }); + + it("should render", () => { + renderWithProviders(); + expect(screen.getByLabelText("Project Name")).toBeInTheDocument(); + }); + + it("should show a 'Basic Information' section heading", () => { + renderWithProviders(); + expect(screen.getByText("Basic Information")).toBeInTheDocument(); + }); + + it("should show a Project Name input", () => { + renderWithProviders(); + expect(screen.getByPlaceholderText("e.g. Customer Support Bot")).toBeInTheDocument(); + }); + + it("should show a Team select", () => { + renderWithProviders(); + expect(screen.getByText("Team")).toBeInTheDocument(); + }); + + it("should show a Description textarea", () => { + renderWithProviders(); + expect(screen.getByPlaceholderText("Describe the purpose of this project")).toBeInTheDocument(); + }); + + it("should show the models select as disabled when no team is selected", () => { + renderWithProviders(); + // The models select should be disabled — its placeholder indicates no team yet + expect(screen.getByText("Select a team first")).toBeInTheDocument(); + }); + + it("should show available team options when the Team dropdown is opened", async () => { + const user = userEvent.setup(); + mockUseTeams.mockReturnValue({ + data: [ + { team_id: "team-1", team_alias: "Engineering", models: [] }, + { team_id: "team-2", team_alias: "Sales", models: [] }, + ], + isLoading: false, + }); + renderWithProviders(); + // The form label "Team" is associated with the combobox input inside the Select + await user.click(screen.getByLabelText("Team")); + await waitFor(() => { + expect(screen.getByText("Engineering")).toBeInTheDocument(); + }); + expect(screen.getByText("Sales")).toBeInTheDocument(); + }); + + it("should show the Max Budget field", () => { + renderWithProviders(); + expect(screen.getByPlaceholderText("0.00")).toBeInTheDocument(); + }); + + it("should show the Advanced Settings collapse panel", () => { + renderWithProviders(); + expect(screen.getByText("Advanced Settings")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectModals/projectFormUtils.test.ts b/ui/litellm-dashboard/src/components/Projects/ProjectModals/projectFormUtils.test.ts new file mode 100644 index 00000000000..f910fd7f819 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectModals/projectFormUtils.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from "vitest"; +import { buildProjectApiParams } from "./projectFormUtils"; +import { ProjectFormValues } from "./ProjectBaseForm"; + +const baseValues: ProjectFormValues = { + project_alias: "My Project", + team_id: "team-1", + models: [], + isBlocked: false, +}; + +describe("buildProjectApiParams", () => { + it("should map basic fields to the API shape", () => { + const result = buildProjectApiParams(baseValues); + expect(result.project_alias).toBe("My Project"); + expect(result.blocked).toBe(false); + expect(result.models).toEqual([]); + }); + + it("should set blocked=true when isBlocked is true", () => { + const result = buildProjectApiParams({ ...baseValues, isBlocked: true }); + expect(result.blocked).toBe(true); + }); + + it("should pass through description when provided", () => { + const result = buildProjectApiParams({ ...baseValues, description: "A description" }); + expect(result.description).toBe("A description"); + }); + + it("should pass through max_budget when provided", () => { + const result = buildProjectApiParams({ ...baseValues, max_budget: 50.0 }); + expect(result.max_budget).toBe(50.0); + }); + + it("should build model_rpm_limit from modelLimits entries", () => { + const result = buildProjectApiParams({ + ...baseValues, + modelLimits: [{ model: "gpt-4", rpm: 100, tpm: 200 }], + }); + expect(result.model_rpm_limit).toEqual({ "gpt-4": 100 }); + }); + + it("should build model_tpm_limit from modelLimits entries", () => { + const result = buildProjectApiParams({ + ...baseValues, + modelLimits: [{ model: "gpt-4", rpm: 100, tpm: 200 }], + }); + expect(result.model_tpm_limit).toEqual({ "gpt-4": 200 }); + }); + + it("should omit model_rpm_limit when no modelLimits are provided", () => { + const result = buildProjectApiParams(baseValues); + expect(result).not.toHaveProperty("model_rpm_limit"); + }); + + it("should omit model_tpm_limit when no modelLimits are provided", () => { + const result = buildProjectApiParams(baseValues); + expect(result).not.toHaveProperty("model_tpm_limit"); + }); + + it("should skip a modelLimits entry that has no model name", () => { + const result = buildProjectApiParams({ + ...baseValues, + modelLimits: [{ model: "", rpm: 100 }], + }); + expect(result).not.toHaveProperty("model_rpm_limit"); + }); + + it("should handle multiple model limit entries", () => { + const result = buildProjectApiParams({ + ...baseValues, + modelLimits: [ + { model: "gpt-4", rpm: 100 }, + { model: "gpt-3.5-turbo", tpm: 5000 }, + ], + }); + expect(result.model_rpm_limit).toEqual({ "gpt-4": 100 }); + expect(result.model_tpm_limit).toEqual({ "gpt-3.5-turbo": 5000 }); + }); + + it("should build metadata from key-value entries", () => { + const result = buildProjectApiParams({ + ...baseValues, + metadata: [{ key: "env", value: "production" }], + }); + expect(result.metadata).toEqual({ env: "production" }); + }); + + it("should omit metadata when no entries are provided", () => { + const result = buildProjectApiParams(baseValues); + expect(result).not.toHaveProperty("metadata"); + }); + + it("should skip metadata entries with no key", () => { + const result = buildProjectApiParams({ + ...baseValues, + metadata: [{ key: "", value: "something" }], + }); + expect(result).not.toHaveProperty("metadata"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectsPage.test.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectsPage.test.tsx new file mode 100644 index 00000000000..f804b9b3ee6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Projects/ProjectsPage.test.tsx @@ -0,0 +1,162 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils"; +import { ProjectsPage } from "./ProjectsPage"; +import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects"; + +const mockUseProjects = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/projects/useProjects", () => ({ + useProjects: () => mockUseProjects(), +})); + +const mockUseTeams = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + useTeams: () => mockUseTeams(), +})); + +// Stub modals and the detail page to keep tests focused on the list page +vi.mock("./ProjectModals/CreateProjectModal", () => ({ + CreateProjectModal: ({ isOpen }: { isOpen: boolean }) => + isOpen ?
: null, +})); + +vi.mock("./ProjectDetailsPage", () => ({ + ProjectDetail: ({ projectId }: { projectId: string }) => ( +
{projectId}
+ ), +})); + +const mockProjects: ProjectResponse[] = [ + { + project_id: "proj-1", + project_alias: "Alpha Project", + description: "First project", + team_id: "team-1", + budget_id: null, + metadata: null, + models: ["gpt-4", "claude-3"], + spend: 5.0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: false, + object_permission_id: null, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, + }, + { + project_id: "proj-2", + project_alias: "Beta Project", + description: "Second project", + team_id: "team-2", + budget_id: null, + metadata: null, + models: [], + spend: 0, + model_spend: null, + model_rpm_limit: null, + model_tpm_limit: null, + blocked: true, + object_permission_id: null, + created_at: "2024-02-01T00:00:00Z", + created_by: "user-2", + updated_at: "2024-02-01T00:00:00Z", + updated_by: "user-2", + litellm_budget_table: null, + }, +]; + +describe("ProjectsPage", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseTeams.mockReturnValue({ data: [], isLoading: false }); + }); + + it("should render the Projects heading", () => { + mockUseProjects.mockReturnValue({ data: [], isLoading: false }); + renderWithProviders(); + expect(screen.getByRole("heading", { name: /projects/i })).toBeInTheDocument(); + }); + + it("should show a 'Create Project' button", () => { + mockUseProjects.mockReturnValue({ data: [], isLoading: false }); + renderWithProviders(); + expect(screen.getByRole("button", { name: /create project/i })).toBeInTheDocument(); + }); + + it("should render the projects table", () => { + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(); + expect(screen.getByText("Alpha Project")).toBeInTheDocument(); + expect(screen.getByText("Beta Project")).toBeInTheDocument(); + }); + + it("should show the model count for each project", () => { + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(); + // proj-1 has 2 models, proj-2 has 0 + expect(screen.getByText("2")).toBeInTheDocument(); + expect(screen.getByText("0")).toBeInTheDocument(); + }); + + it("should display 'Active' tag for non-blocked projects", () => { + mockUseProjects.mockReturnValue({ data: [mockProjects[0]], isLoading: false }); + renderWithProviders(); + expect(screen.getByText("Active")).toBeInTheDocument(); + }); + + it("should display 'Blocked' tag for blocked projects", () => { + mockUseProjects.mockReturnValue({ data: [mockProjects[1]], isLoading: false }); + renderWithProviders(); + expect(screen.getByText("Blocked")).toBeInTheDocument(); + }); + + it("should open the create modal when 'Create Project' is clicked", async () => { + const user = userEvent.setup(); + mockUseProjects.mockReturnValue({ data: [], isLoading: false }); + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /create project/i })); + expect(screen.getByTestId("create-modal")).toBeInTheDocument(); + }); + + it("should show the project detail view when a project ID is clicked", async () => { + const user = userEvent.setup(); + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(); + await user.click(screen.getByText("proj-1")); + expect(screen.getByTestId("project-detail")).toHaveTextContent("proj-1"); + }); + + it("should filter displayed projects when the search input has a value", async () => { + const user = userEvent.setup(); + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(); + await user.type( + screen.getByPlaceholderText(/search projects/i), + "Alpha" + ); + await waitFor(() => { + expect(screen.getByText("Alpha Project")).toBeInTheDocument(); + expect(screen.queryByText("Beta Project")).not.toBeInTheDocument(); + }); + }); + + it("should show the total project count in the pagination", () => { + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(); + expect(screen.getByText("2 projects")).toBeInTheDocument(); + }); + + it("should resolve team alias from the teams list in the Team column", () => { + mockUseTeams.mockReturnValue({ + data: [{ team_id: "team-1", team_alias: "Engineering", models: [] }], + isLoading: false, + }); + mockUseProjects.mockReturnValue({ data: [mockProjects[0]], isLoading: false }); + renderWithProviders(); + expect(screen.getByText("Engineering")).toBeInTheDocument(); + }); +}); From 06d1616b9f1a85fbb9fe04363418762d544aeef7 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 17:26:18 -0800 Subject: [PATCH 156/171] remove unused exports from networking.tsx Co-Authored-By: Claude Haiku 4.5 --- .../src/components/networking.tsx | 982 +----------------- .../playground/chat_ui/ChatUI.test.tsx | 1 - .../templates/key_edit_view.test.tsx | 3 - 3 files changed, 22 insertions(+), 964 deletions(-) diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 9df2a3401e0..30c1d3c5b81 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -78,7 +78,7 @@ import { jsonFields } from "./common_components/check_openapi_schema"; import NotificationsManager from "./molecules/notifications_manager"; const isLocal = process.env.NODE_ENV === "development"; -export const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : null; +const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : null; const defaultServerRootPath = "/"; export let serverRootPath = defaultServerRootPath; export let proxyBaseUrl = defaultProxyBaseUrl; @@ -161,7 +161,7 @@ export interface PromptTemplateBase { metadata?: Record | null; } -export interface PromptInfoResponse { +interface PromptInfoResponse { prompt_spec: PromptSpec; raw_prompt_template: PromptTemplateBase | null; } @@ -246,7 +246,7 @@ export interface AgentCreateInfo { use_a2a_form_fields?: boolean; } -export interface PublicModelHubInfo { +interface PublicModelHubInfo { docs_title: string; custom_docs_description: string | null; litellm_version: string; @@ -734,43 +734,6 @@ export const invitationCreateCall = async ( } }; -export const invitationClaimCall = async ( - accessToken: string, - formValues: Record, // Assuming formValues is an object -) => { - try { - console.log("Form Values in invitationCreateCall:", formValues); // Log the form values before making the API call - - console.log("Form Values after check:", formValues); - const url = proxyBaseUrl ? `${proxyBaseUrl}/invitation/claim` : `/invitation/claim`; - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...formValues, // Include formValues in the request body - }), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log("API Response:", data); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - export const alertingSettingsCall = async (accessToken: string) => { /** * Get all configurable params for setting a model @@ -1900,38 +1863,6 @@ export const agentDailyActivityCall = async ( }); }; -export const getTotalSpendCall = async (accessToken: string) => { - /** - * Get all models on proxy - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend` : `/global/spend`; - - //NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - export const getOnboardingCredentials = async (inviteUUID: string) => { /** * Get all models on proxy @@ -2415,33 +2346,6 @@ export const modelAvailableCall = async ( } }; -export const keySpendLogsCall = async (accessToken: string, token: string) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend/logs` : `/global/spend/logs`; - console.log("in keySpendLogsCall:", url); - const response = await fetch(`${url}?api_key=${token}`, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - return data; - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - export const teamSpendLogsCall = async (accessToken: string) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend/teams` : `/global/spend/teams`; @@ -2599,51 +2503,10 @@ export const userFilterUICall = async (accessToken: string, params: URLSearchPar } }; -export const userSpendLogsCall = async ( - accessToken: string, - token: string, - userRole: string, - userID: string, - startTime: string, - endTime: string, -) => { - try { - console.log(`user role in spend logs call: ${userRole}`); - let url = proxyBaseUrl ? `${proxyBaseUrl}/spend/logs` : `/spend/logs`; - if (userRole == "App Owner") { - url = `${url}?user_id=${userID}&start_date=${startTime}&end_date=${endTime}`; - } else { - url = `${url}?start_date=${startTime}&end_date=${endTime}`; - } - //NotificationsManager.info("Making spend logs request"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - //NotificationsManager.success("Spend Logs received"); - return data; - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - /** * Optional query params for /spend/logs/ui - matches backend spend_management_endpoints.py */ -export interface UiSpendLogsParams { +interface UiSpendLogsParams { api_key?: string; team_id?: string; request_id?: string; @@ -2663,7 +2526,7 @@ export interface UiSpendLogsParams { max_spend?: number; } -export interface UiSpendLogsCallOptions { +interface UiSpendLogsCallOptions { accessToken: string; start_date: string; end_date: string; @@ -2990,92 +2853,6 @@ export const adminGlobalActivityPerModel = async ( } }; -export const adminGlobalActivityExceptions = async ( - accessToken: string, - startTime: string | undefined, - endTime: string | undefined, - modelGroup: string, -) => { - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/global/activity/exceptions` : `/global/activity/exceptions`; - - if (startTime && endTime) { - url += `?start_date=${startTime}&end_date=${endTime}`; - } - - if (modelGroup) { - url += `&model_group=${modelGroup}`; - } - - const requestOptions = { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - }, - }; - - const response = await fetch(url, requestOptions); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - return data; - } catch (error) { - console.error("Failed to fetch spend data:", error); - throw error; - } -}; - -export const adminGlobalActivityExceptionsPerDeployment = async ( - accessToken: string, - startTime: string | undefined, - endTime: string | undefined, - modelGroup: string, -) => { - try { - let url = proxyBaseUrl - ? `${proxyBaseUrl}/global/activity/exceptions/deployment` - : `/global/activity/exceptions/deployment`; - - if (startTime && endTime) { - url += `?start_date=${startTime}&end_date=${endTime}`; - } - - if (modelGroup) { - url += `&model_group=${modelGroup}`; - } - - const requestOptions = { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - }, - }; - - const response = await fetch(url, requestOptions); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - return data; - } catch (error) { - console.error("Failed to fetch spend data:", error); - throw error; - } -}; - export const adminTopModelsCall = async (accessToken: string) => { try { let url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend/models?limit=5` : `/global/spend/models?limit=5`; @@ -3381,109 +3158,6 @@ export const keyAliasesCall = async ( } }; -export const spendUsersCall = async (accessToken: string, userID: string) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/users` : `/spend/users`; - console.log("in spendUsersCall:", url); - const response = await fetch(`${url}?user_id=${userID}`, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - return data; - } catch (error) { - console.error("Failed to get spend for user", error); - throw error; - } -}; - -export const userRequestModelCall = async ( - accessToken: string, - model: string, - UserID: string, - justification: string, -) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/user/request_model` : `/user/request_model`; - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - models: [model], - user_id: UserID, - justification: justification, - }), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - //NotificationsManager.success(""); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - -export const userGetRequesedtModelsCall = async (accessToken: string) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/user/get_requests` : `/user/get_requests`; - console.log("in userGetRequesedtModelsCall:", url); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - //NotificationsManager.success(""); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to get requested models:", error); - throw error; - } -}; - -export interface User { - user_role: string; - user_id: string; - user_email: string; - [key: string]: string; // Include any other potential keys in the dictionary -} - export const userDailyActivityAggregatedCall = async (accessToken: string, startTime: Date, endTime: Date, userId: string | null = null) => { /** * Get aggregated daily user activity (no pagination) @@ -3533,36 +3207,6 @@ export const userDailyActivityAggregatedCall = async (accessToken: string, start } }; -export const userGetAllUsersCall = async (accessToken: string, role: string) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/user/get_users?role=${role}` : `/user/get_users?role=${role}`; - console.log("in userGetAllUsersCall:", url); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - //NotificationsManager.success("Got all users"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to get requested models:", error); - throw error; - } -}; - export const getPossibleUserRoles = async (accessToken: string) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/user/available_roles` : `/user/available_roles`; @@ -3958,41 +3602,6 @@ export const modelPatchUpdateCall = async ( } }; -export const modelUpdateCall = async ( - accessToken: string, - formValues: Record, // Assuming formValues is an object -) => { - try { - console.log("Form Values in modelUpateCall:", formValues); // Log the form values before making the API call - - const url = proxyBaseUrl ? `${proxyBaseUrl}/model/update` : `/model/update`; - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...formValues, // Include formValues in the request body - }), - }); - - if (!response.ok) { - const errorData = await response.text(); - handleError(errorData); - console.error("Error update from the server:", errorData); - throw new Error("Network response was not ok"); - } - const data = await response.json(); - console.log("Update model Response:", data); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to update model:", error); - throw error; - } -}; - export interface Member { role: string; user_id: string | null; @@ -4446,77 +4055,6 @@ export const userBulkUpdateUserCall = async ( } }; -export const PredictedSpendLogsCall = async (accessToken: string, requestData: any) => { - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/global/predict/spend/logs` : `/global/predict/spend/logs`; - - //NotificationsManager.info("Predicting spend logs request"); - - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - data: requestData, - }), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log(data); - //NotificationsManager.success("Predicted Logs received"); - return data; - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - -export const slackBudgetAlertsHealthCheck = async (accessToken: string) => { - try { - let url = proxyBaseUrl - ? `${proxyBaseUrl}/health/services?service=slack_budget_alerts` - : `/health/services?service=slack_budget_alerts`; - - console.log("Checking Slack Budget Alerts service health"); - //NotificationsManager.info("Sending Test Slack alert..."); - - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.text(); - handleError(errorData); - // throw error with message - throw new Error(errorData); - } - - const data = await response.json(); - NotificationsManager.success("Test Slack Alert worked - check your Slack!"); - console.log("Service Health Response:", data); - - // You can add additional logic here based on the response if needed - - return data; - } catch (error) { - console.error("Failed to perform health check:", error); - throw error; - } -}; - export const serviceHealthCheck = async (accessToken: string, service: string) => { try { let url = proxyBaseUrl @@ -4581,39 +4119,6 @@ export const getBudgetList = async (accessToken: string) => { throw error; } }; -export const getBudgetSettings = async (accessToken: string) => { - /** - * Get all configurable params for setting a budget - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/budget/settings` : `/budget/settings`; - - //NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - //NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to get callbacks:", error); - throw error; - } -}; - export const getCallbacksCall = async (accessToken: string, userID: string, userRole: string) => { /** * Get all the models user has access to @@ -4858,42 +4363,6 @@ export const getConfigFieldSetting = async (accessToken: string, fieldName: stri } }; -export const updatePassThroughFieldSetting = async (accessToken: string, fieldName: string, fieldValue: any) => { - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/config/pass_through_endpoint` : `/config/pass_through_endpoint`; - - let formData = { - field_name: fieldName, - field_value: fieldValue, - }; - //NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - //NotificationsManager.info("Received model data"); - NotificationsManager.success("Successfully updated value!"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to set callbacks:", error); - throw error; - } -}; - export const createPassThroughEndpoint = async (accessToken: string, formValues: Record) => { /** * Set callbacks on proxy @@ -5070,39 +4539,6 @@ export const setCallbacksCall = async (accessToken: string, formValues: Record { - /** - * Get all the models user has access to - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/health` : `/health`; - - //NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - //NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to call /health:", error); - throw error; - } -}; - export const individualModelHealthCheckCall = async (accessToken: string, modelId: string) => { /** * Run health check for a specific model using model ID (so each deployment is checked separately). @@ -5167,51 +4603,6 @@ export const cachingHealthCheckCall = async (accessToken: string) => { } }; -export const healthCheckHistoryCall = async ( - accessToken: string, - model?: string, - statusFilter?: string, - limit: number = 100, - offset: number = 0, -) => { - /** - * Get health check history for models - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/health/history` : `/health/history`; - - const params = new URLSearchParams(); - if (model) params.append("model", model); - if (statusFilter) params.append("status_filter", statusFilter); - params.append("limit", limit.toString()); - params.append("offset", offset.toString()); - - if (params.toString()) { - url += `?${params.toString()}`; - } - - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.text(); - handleError(errorData); - throw new Error(errorData); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to call /health/history:", error); - throw error; - } -}; - export const latestHealthChecksCall = async (accessToken: string) => { /** * Get the latest health check status for all models @@ -5305,37 +4696,6 @@ export const getUISettings = async (accessToken: string) => { } }; -export const updateUISettings = async (accessToken: string, settings: any) => { - /** - * Update UI-specific configuration flags in the database - * Only proxy admins can update these settings - */ - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/update/ui_settings` : `/update/ui_settings`; - const response = await fetch(url, { - method: "PATCH", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(settings), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to update UI settings:", error); - throw error; - } -}; - export const getMCPSemanticFilterSettings = async (accessToken: string) => { /** * Get MCP semantic filter configuration @@ -5519,14 +4879,14 @@ export interface GuardrailSubmissionItem { updated_at?: string | null; } -export interface GuardrailSubmissionSummary { +interface GuardrailSubmissionSummary { total: number; pending_review: number; active: number; rejected: number; } -export interface ListGuardrailSubmissionsResponse { +interface ListGuardrailSubmissionsResponse { submissions: GuardrailSubmissionItem[]; summary: GuardrailSubmissionSummary; } @@ -5558,29 +4918,6 @@ export const listGuardrailSubmissions = async ( return response.json(); }; -export const getGuardrailSubmission = async ( - accessToken: string, - guardrailId: string -): Promise => { - const url = proxyBaseUrl - ? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}` - : `/guardrails/submissions/${encodeURIComponent(guardrailId)}`; - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - return response.json(); -}; - export const approveGuardrailSubmission = async ( accessToken: string, guardrailId: string @@ -5720,35 +5057,6 @@ export const getGuardrailsUsageLogs = async ( } }; -export const getPoliciesUsageOverview = async ( - accessToken: string, - startDate?: string, - endDate?: string -) => { - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/policies/usage/overview` : `/policies/usage/overview`; - const params = new URLSearchParams(); - if (startDate) params.append("start_date", startDate); - if (endDate) params.append("end_date", endDate); - if (params.toString()) url += `?${params.toString()}`; - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(deriveErrorMessage(errorData)); - } - return response.json(); - } catch (error) { - console.error("Failed to get policies usage overview:", error); - throw error; - } -}; - // ───────────────────────────────────────────────────────────────────────────── // Policy CRUD API Calls // ───────────────────────────────────────────────────────────────────────────── @@ -5779,13 +5087,13 @@ export const getPoliciesList = async (accessToken: string) => { } }; -export interface GuardrailInputs { +interface GuardrailInputs { texts?: string[]; images?: string[]; [key: string]: unknown; } -export interface TestPoliciesAndGuardrailsRequest { +interface TestPoliciesAndGuardrailsRequest { policy_names?: string[] | null; guardrail_names?: string[] | null; /** Single input (legacy). Use inputs_list for per-input batch processing. */ @@ -5798,19 +5106,19 @@ export interface TestPoliciesAndGuardrailsRequest { agent_id?: string | null; } -export interface GuardrailErrorEntry { +interface GuardrailErrorEntry { guardrail_name: string; message: string; } -export interface TestPoliciesAndGuardrailsResultItem { +interface TestPoliciesAndGuardrailsResultItem { inputs: Record; guardrail_errors: GuardrailErrorEntry[]; /** Present when request included agent_id; serialized chat completion response. */ agent_response?: Record; } -export interface TestPoliciesAndGuardrailsResponse { +interface TestPoliciesAndGuardrailsResponse { inputs?: Record; guardrail_errors?: GuardrailErrorEntry[]; /** Present when inputs_list was used; one result per input. */ @@ -6779,34 +6087,6 @@ export const convertPromptFileToJson = async ( } }; -export const patchPromptCall = async (accessToken: string, promptId: string, promptData: any) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/${promptId}` : `/prompts/${promptId}`; - - const response = await fetch(url, { - method: "PATCH", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(promptData), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to patch prompt:", error); - throw error; - } -}; - export const createAgentCall = async (accessToken: string, agentData: any) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents` : `/v1/agents`; @@ -7232,35 +6512,6 @@ export const fetchSearchTools = async (accessToken: string) => { } }; -export const fetchSearchToolById = async (accessToken: string, searchToolId: string) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/search_tools/${searchToolId}` : `/search_tools/${searchToolId}`; - console.log("Fetching search tool by ID from:", url); - - const response = await fetch(url, { - method: HTTP_REQUEST.GET, - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log("Fetched search tool:", data); - return data; - } catch (error) { - console.error("Failed to fetch search tool:", error); - throw error; - } -}; - export const createSearchTool = async (accessToken: string, formValues: Record) => { try { console.log("Creating search tool with values:", formValues); @@ -7467,7 +6718,7 @@ export const listMCPTools = async ( } }; -export interface CallMCPToolOptions { +interface CallMCPToolOptions { guardrails?: string[]; customHeaders?: Record; } @@ -8145,33 +7396,6 @@ export const deleteAgentCall = async (accessToken: string, agentId: string) => { } }; -export const makeAgentPublicCall = async (accessToken: string, agentId: string) => { - try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/${agentId}/make_public` : `/v1/agents/${agentId}/make_public`; - - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.text(); - handleError(errorData); - throw new Error(errorData); - } - - const data = await response.json(); - console.log("Make agent public response:", data); - return data; - } catch (error) { - console.error("Failed to make agent public:", error); - throw error; - } -}; - export const makeAgentsPublicCall = async (accessToken: string, agentIds: string[]) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/make_public` : `/v1/agents/make_public`; @@ -8599,7 +7823,7 @@ export const applyGuardrail = async ( } }; -export interface TestCustomCodeGuardrailRequest { +interface TestCustomCodeGuardrailRequest { custom_code: string; test_input: { texts: string[]; @@ -8619,7 +7843,7 @@ export interface TestCustomCodeGuardrailRequest { }; } -export interface TestCustomCodeGuardrailResponse { +interface TestCustomCodeGuardrailResponse { success: boolean; result?: { action: "allow" | "block" | "modify"; @@ -8786,7 +8010,7 @@ export const updateSSOSettings = async (accessToken: string, settings: Record { - try { - let url = proxyBaseUrl - ? `${proxyBaseUrl}/config/pass_through_endpoint?endpoint_id=${encodeURIComponent(endpointPath)}` - : `/config/pass_through_endpoint?endpoint_id=${encodeURIComponent(endpointPath)}`; - - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - const endpoints = data["endpoints"]; - - if (!endpoints || endpoints.length === 0) { - throw new Error("Pass through endpoint not found"); - } - - return endpoints[0]; // Return the first (and should be only) endpoint - } catch (error) { - console.error("Failed to get pass through endpoint info:", error); - throw error; - } -}; - export const deleteCallback = async (accessToken: string, callbackName: string) => { /** * Delete specific callback from proxy using the /config/callback/delete API @@ -9031,72 +8220,6 @@ export const deleteCallback = async (accessToken: string, callbackName: string) } }; -export const mcpToolsCall = async (accessToken: string) => { - const proxyBaseUrl = getProxyBaseUrl(); - const response = await fetch(`${proxyBaseUrl}/v1/mcp/tools`, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json(); -}; - -export const testMCPConnectionRequest = async (accessToken: string, mcpServerConfig: Record) => { - try { - console.log("Testing MCP connection with config:", JSON.stringify(mcpServerConfig)); - - // Construct the URL for POST request - const url = proxyBaseUrl ? `${proxyBaseUrl}/mcp-rest/test/connection` : `/mcp-rest/test/connection`; - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - }, - body: JSON.stringify(mcpServerConfig), - }); - - // Check for non-JSON responses first - const contentType = response.headers.get("content-type"); - if (!contentType || !contentType.includes("application/json")) { - const text = await response.text(); - console.error("Received non-JSON response:", text); - throw new Error( - `Received non-JSON response (${response.status}: ${response.statusText}). Check network tab for details.`, - ); - } - - const data = await response.json(); - - if (!response.ok || data.status === "error") { - // Return the error response instead of throwing an error - // This allows the caller to handle the error format properly - if (data.status === "error") { - return data; // Return the full error response - } else { - return { - status: "error", - message: data.error?.message || `MCP connection test failed: ${response.status} ${response.statusText}`, - }; - } - } - - return data; - } catch (error) { - console.error("MCP connection test error:", error); - // For network errors or other exceptions, still throw - throw error; - } -}; - export const testMCPToolsListRequest = async ( accessToken: string | null, mcpServerConfig: Record, @@ -9364,67 +8487,6 @@ export const searchToolQueryCall = async ( } }; -export const userAgentAnalyticsCall = async ( - accessToken: string, - startTime: Date, - endTime: Date, - page: number = 1, - pageSize: number = 50, - userAgentFilter?: string, -) => { - /** - * Get user agent analytics data including DAU, WAU, MAU, successful requests, and completed tokens - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/tag/user-agent/analytics` : `/tag/user-agent/analytics`; - - const queryParams = new URLSearchParams(); - - // Format dates as YYYY-MM-DD for the API - const formatDate = (date: Date) => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; - }; - - queryParams.append("start_date", formatDate(startTime)); - queryParams.append("end_date", formatDate(endTime)); - queryParams.append("page", page.toString()); - queryParams.append("page_size", pageSize.toString()); - - if (userAgentFilter) { - queryParams.append("user_agent_filter", userAgentFilter); - } - - const queryString = queryParams.toString(); - if (queryString) { - url += `?${queryString}`; - } - - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to fetch user agent analytics:", error); - throw error; - } -}; - // New endpoint functions for DAU, WAU, MAU export const tagDauCall = async (accessToken: string, endDate: Date, tagFilter?: string, tagFilters?: string[]) => { /** @@ -9751,7 +8813,7 @@ export interface LoginRequest { password: string; } -export interface LoginResponse { +interface LoginResponse { redirect_url: string; } @@ -10166,7 +9228,7 @@ export interface ToolPolicyOption { description: string; } -export interface ToolPolicyOptionsResponse { +interface ToolPolicyOptionsResponse { input_policies: ToolPolicyOption[]; output_policies: ToolPolicyOption[]; } @@ -10219,12 +9281,12 @@ export interface ToolPolicyOverrideRow { updated_at?: string; } -export interface ToolDetailResponse { +interface ToolDetailResponse { tool: ToolRow; overrides: ToolPolicyOverrideRow[]; } -export interface ToolUsageLogEntry { +interface ToolUsageLogEntry { id: string; timestamp: string; model?: string | null; @@ -10233,7 +9295,7 @@ export interface ToolUsageLogEntry { input_snippet?: string | null; } -export interface ToolUsageLogsResponse { +interface ToolUsageLogsResponse { logs: ToolUsageLogEntry[]; total: number; page: number; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx index e00f27c39f4..b652085a098 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx @@ -13,7 +13,6 @@ vi.mock("../networking", () => ({ tagListCall: vi.fn().mockResolvedValue({ data: [] }), vectorStoreListCall: vi.fn().mockResolvedValue({ data: [] }), getGuardrailsList: vi.fn().mockResolvedValue({ data: [] }), - mcpToolsCall: vi.fn().mockResolvedValue({ data: [] }), modelHubCall: vi.fn().mockResolvedValue({ data: [] }), })); diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx index f090df7c361..b00a8d1e3f8 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx @@ -31,9 +31,6 @@ vi.mock("../networking", async () => { vectorStoreListCall: vi.fn().mockResolvedValue({ data: [], }), - mcpToolsCall: vi.fn().mockResolvedValue({ - data: [], - }), agentListCall: vi.fn().mockResolvedValue({ data: [], }), From 9501a161e7d0b900fc2799b911fbfe9891d58451 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 17:35:01 -0800 Subject: [PATCH 157/171] [Fix] Docs build broken due to mismatched @docusaurus package versions Co-Authored-By: Claude Sonnet 4.6 --- docs/my-website/package-lock.json | 5305 ++--------------------------- docs/my-website/package.json | 6 +- 2 files changed, 254 insertions(+), 5057 deletions(-) diff --git a/docs/my-website/package-lock.json b/docs/my-website/package-lock.json index 5113cd3e381..1ffc267557c 100644 --- a/docs/my-website/package-lock.json +++ b/docs/my-website/package-lock.json @@ -9,10 +9,10 @@ "version": "0.0.0", "dependencies": { "@docusaurus/core": "3.8.1", - "@docusaurus/plugin-google-gtag": "^3.5.2", + "@docusaurus/plugin-google-gtag": "3.8.1", "@docusaurus/plugin-ideal-image": "3.8.1", - "@docusaurus/preset-classic": "^3.5.2", - "@docusaurus/theme-mermaid": "^3.5.2", + "@docusaurus/preset-classic": "3.8.1", + "@docusaurus/theme-mermaid": "3.8.1", "@inkeep/cxkit-docusaurus": "^0.5.89", "@mdx-js/react": "^3.0.0", "clsx": "^1.2.1", @@ -91,30 +91,6 @@ "algoliasearch": ">= 4.9.1 < 6" } }, - "node_modules/@algolia/cache-browser-local-storage": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.27.0.tgz", - "integrity": "sha512-YGog2s57sO20lvpa+hv5XLAAmiTI1kHsCMRtPVfiaOdIQnvRla21lfH08onqEbZihOPVI8GULwt79zQB2ymKzg==", - "license": "MIT", - "dependencies": { - "@algolia/cache-common": "4.27.0" - } - }, - "node_modules/@algolia/cache-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.27.0.tgz", - "integrity": "sha512-Sr8zjNXj82p6lO4W9CdzfF0m0/9h/H6VAdSHOTtimm/cTzXIYXRI2IZq7+Nt2ljJ7Ukx+7dIFcxQjE57eQSPsw==", - "license": "MIT" - }, - "node_modules/@algolia/cache-in-memory": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.27.0.tgz", - "integrity": "sha512-abgMRTcVD0IllNvMM9JFhxtyLn1v6Ey7mQ7+BGS3JCzvkCX7KZqlS0BIuVUDgx9sPIfOeNsG/awGzMmP50TwZw==", - "license": "MIT", - "dependencies": { - "@algolia/cache-common": "4.27.0" - } - }, "node_modules/@algolia/client-abtesting": { "version": "5.49.1", "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.49.1.tgz", @@ -130,69 +106,19 @@ "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-account": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.27.0.tgz", - "integrity": "sha512-sSHxwrKTKJrwfoR/LcQJZfmiWJcM5d9Rp7afMChxOcdGdkSdIwrNBC8SCcHRenA3GsZ6mg+j6px7KWYxJ34btA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/client-search": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/client-account/node_modules/@algolia/client-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.27.0.tgz", - "integrity": "sha512-ZrT6l/YPQgyIUuBCxcYPeXol2VBLUMuNb1rKXrm6z1f/iTiwqtnEEb16/6CC11+Re0ZGXrdcMVrgDRrzveQ1aQ==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/client-account/node_modules/@algolia/client-search": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.27.0.tgz", - "integrity": "sha512-qmX/f67ay0eZ4V5Io8fWWOcUVo/gqre2yei1PnmEhQU2Gul6ushg25QnNrfu4BODiRrw1rwYveZaLCiHvcUxrQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, "node_modules/@algolia/client-analytics": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.27.0.tgz", - "integrity": "sha512-MqIDyxODljn9ZC4oqjQD0kez2a4zjIJ9ywA/b7cIiUiK/tDjZNTVjYd9WXMKQlXnWUwfrfXJZClVVqN1iCXS+Q==", + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.49.1.tgz", + "integrity": "sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g==", "license": "MIT", "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/client-search": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/client-analytics/node_modules/@algolia/client-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.27.0.tgz", - "integrity": "sha512-ZrT6l/YPQgyIUuBCxcYPeXol2VBLUMuNb1rKXrm6z1f/iTiwqtnEEb16/6CC11+Re0ZGXrdcMVrgDRrzveQ1aQ==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/client-analytics/node_modules/@algolia/client-search": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.27.0.tgz", - "integrity": "sha512-qmX/f67ay0eZ4V5Io8fWWOcUVo/gqre2yei1PnmEhQU2Gul6ushg25QnNrfu4BODiRrw1rwYveZaLCiHvcUxrQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" + "@algolia/client-common": "5.49.1", + "@algolia/requester-browser-xhr": "5.49.1", + "@algolia/requester-fetch": "5.49.1", + "@algolia/requester-node-http": "5.49.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { @@ -220,24 +146,18 @@ } }, "node_modules/@algolia/client-personalization": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.27.0.tgz", - "integrity": "sha512-OZqaFFVm+10hAlmxpiTWi/o2n+YKBESbSqSy2yXAumPH/kaK4moJHFblbh8IkV3KZR0lLm4hzPtn8Q2nWNiDUA==", + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.49.1.tgz", + "integrity": "sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w==", "license": "MIT", "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/client-personalization/node_modules/@algolia/client-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.27.0.tgz", - "integrity": "sha512-ZrT6l/YPQgyIUuBCxcYPeXol2VBLUMuNb1rKXrm6z1f/iTiwqtnEEb16/6CC11+Re0ZGXrdcMVrgDRrzveQ1aQ==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" + "@algolia/client-common": "5.49.1", + "@algolia/requester-browser-xhr": "5.49.1", + "@algolia/requester-fetch": "5.49.1", + "@algolia/requester-node-http": "5.49.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { @@ -291,21 +211,6 @@ "node": ">= 14.0.0" } }, - "node_modules/@algolia/logger-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.27.0.tgz", - "integrity": "sha512-pIrmQRXtDV+zTMVXKtKCosC2rWhn0F0TdUeb9etA6RiAz6jY6bY6f0+JX7YekDK09SnmZMLIyUa7Jci+Ied9bw==", - "license": "MIT" - }, - "node_modules/@algolia/logger-console": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.27.0.tgz", - "integrity": "sha512-UWvta8BxsR/u5z9eI088mOSLQaGtmoCtXeN3DYJurlxAdJwPuKtEb5+433kxA6/E9f2/JgoW89KZ1vNP9pcHBQ==", - "license": "MIT", - "dependencies": { - "@algolia/logger-common": "4.27.0" - } - }, "node_modules/@algolia/monitoring": { "version": "1.49.1", "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.49.1.tgz", @@ -322,61 +227,18 @@ } }, "node_modules/@algolia/recommend": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.27.0.tgz", - "integrity": "sha512-CFy54xDjrsazPi3KN04yPmLRDT72AKokc3RLOdWQvG0/uEUjj7dhWqe9qenxpL4ydsjO7S1eY5YqmX+uMGonlg==", + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.49.1.tgz", + "integrity": "sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw==", "license": "MIT", "dependencies": { - "@algolia/cache-browser-local-storage": "4.27.0", - "@algolia/cache-common": "4.27.0", - "@algolia/cache-in-memory": "4.27.0", - "@algolia/client-common": "4.27.0", - "@algolia/client-search": "4.27.0", - "@algolia/logger-common": "4.27.0", - "@algolia/logger-console": "4.27.0", - "@algolia/requester-browser-xhr": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/requester-node-http": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/client-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.27.0.tgz", - "integrity": "sha512-ZrT6l/YPQgyIUuBCxcYPeXol2VBLUMuNb1rKXrm6z1f/iTiwqtnEEb16/6CC11+Re0ZGXrdcMVrgDRrzveQ1aQ==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/client-search": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.27.0.tgz", - "integrity": "sha512-qmX/f67ay0eZ4V5Io8fWWOcUVo/gqre2yei1PnmEhQU2Gul6ushg25QnNrfu4BODiRrw1rwYveZaLCiHvcUxrQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/requester-browser-xhr": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.27.0.tgz", - "integrity": "sha512-dTenMBIIpyp5o3C2ZnfbsuSlD/lL9jPkk6T+2+qm38fyw2nf49ANbcHFE79NgiGrnmw7QrYveCs9NIP3Wk4v6g==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0" - } - }, - "node_modules/@algolia/recommend/node_modules/@algolia/requester-node-http": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.27.0.tgz", - "integrity": "sha512-y8nUqaUQeSOQ5oaNo0b2QPznyBFW9LoIwljyUphJ+gUZpU6O/j2/C8ovoqDpIe6J0etqHg5RCcBizrCFZuLpyw==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0" + "@algolia/client-common": "5.49.1", + "@algolia/requester-browser-xhr": "5.49.1", + "@algolia/requester-fetch": "5.49.1", + "@algolia/requester-node-http": "5.49.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { @@ -391,12 +253,6 @@ "node": ">= 14.0.0" } }, - "node_modules/@algolia/requester-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.27.0.tgz", - "integrity": "sha512-VC3prAQVgWTubMStb3mJz6i61Hqbtagi2LeIbgNtoFJFff3XZDcAaO1D5r0GYl2+DrB2VzUHnQXbkiuI+HHYyg==", - "license": "MIT" - }, "node_modules/@algolia/requester-fetch": { "version": "5.49.1", "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.49.1.tgz", @@ -421,17 +277,6 @@ "node": ">= 14.0.0" } }, - "node_modules/@algolia/transporter": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.27.0.tgz", - "integrity": "sha512-PvSbELU4VjN3xSX79ki+zIdOGhTxyJXWvRDzkUjfTx2iNfPWDdTjzKbP1o+268coJztxrkuBwJz90Urek7o1Kw==", - "license": "MIT", - "dependencies": { - "@algolia/cache-common": "4.27.0", - "@algolia/logger-common": "4.27.0", - "@algolia/requester-common": "4.27.0" - } - }, "node_modules/@antfu/install-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", @@ -3572,76 +3417,6 @@ } } }, - "node_modules/@docsearch/react/node_modules/@algolia/client-analytics": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.49.1.tgz", - "integrity": "sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.49.1", - "@algolia/requester-browser-xhr": "5.49.1", - "@algolia/requester-fetch": "5.49.1", - "@algolia/requester-node-http": "5.49.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@docsearch/react/node_modules/@algolia/client-personalization": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.49.1.tgz", - "integrity": "sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.49.1", - "@algolia/requester-browser-xhr": "5.49.1", - "@algolia/requester-fetch": "5.49.1", - "@algolia/requester-node-http": "5.49.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@docsearch/react/node_modules/@algolia/recommend": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.49.1.tgz", - "integrity": "sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.49.1", - "@algolia/requester-browser-xhr": "5.49.1", - "@algolia/requester-fetch": "5.49.1", - "@algolia/requester-node-http": "5.49.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@docsearch/react/node_modules/algoliasearch": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.1.tgz", - "integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.15.1", - "@algolia/client-abtesting": "5.49.1", - "@algolia/client-analytics": "5.49.1", - "@algolia/client-common": "5.49.1", - "@algolia/client-insights": "5.49.1", - "@algolia/client-personalization": "5.49.1", - "@algolia/client-query-suggestions": "5.49.1", - "@algolia/client-search": "5.49.1", - "@algolia/ingestion": "1.49.1", - "@algolia/monitoring": "1.49.1", - "@algolia/recommend": "5.49.1", - "@algolia/requester-browser-xhr": "5.49.1", - "@algolia/requester-fetch": "5.49.1", - "@algolia/requester-node-http": "5.49.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/@docusaurus/babel": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.8.1.tgz", @@ -3859,7 +3634,6 @@ "version": "3.8.1", "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.1.tgz", "integrity": "sha512-6xhvAJiXzsaq3JdosS7wbRt/PwEPWHr9eM4YNYqVlbgG1hSK3uQDXTVvQktasp3VO6BmfYWPozueLWuj4gB+vg==", - "dev": true, "license": "MIT", "dependencies": { "@docusaurus/types": "3.8.1", @@ -3876,24 +3650,24 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz", - "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.1.tgz", + "integrity": "sha512-vNTpMmlvNP9n3hGEcgPaXyvTljanAKIUkuG9URQ1DeuDup0OR7Ltvoc8yrmH+iMZJbcQGhUJF+WjHLwuk8HSdw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/theme-common": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "cheerio": "1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -3905,336 +3679,31 @@ }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz", - "integrity": "sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", + "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/module-type-aliases": "3.5.2", - "@docusaurus/theme-common": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" @@ -4243,346 +3712,21 @@ "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/module-type-aliases": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz", - "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.5.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "*", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz", - "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.1.tgz", + "integrity": "sha512-a+V6MS2cIu37E/m7nDJn3dcxpvXb6TvgdNI22vJX8iUTp8eoMoPa0VArEbWvCxMY/xdC26WzNv4wZ6y0iIni/w==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" @@ -4591,977 +3735,75 @@ "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.1.tgz", + "integrity": "sha512-VQ47xRxfNKjHS5ItzaVXpxeTm7/wJLFMOPo1BkmoMG4Cuz4nuI+Hs62+RMk1OqVog68Swz66xVPK8g9XTrBKRw==", "license": "MIT", "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "tslib": "^2.6.0" }, "engines": { "node": ">=18.0" } }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" - } - }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.5.2.tgz", - "integrity": "sha512-kBK6GlN0itCkrmHuCS6aX1wmoWc5wpd5KJlqQ1FyrF0cLDnvsYSnh7+ftdwzt7G6lGBho8lrVwkkL9/iQvaSOA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.8.1.tgz", + "integrity": "sha512-nT3lN7TV5bi5hKMB7FK8gCffFTBSsBsAfV84/v293qAmnHOyg1nr9okEw8AiwcO3bl9vije5nsUvP0aRl2lpaw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "engines": { "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-debug/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.5.2.tgz", - "integrity": "sha512-rjEkJH/tJ8OXRE9bwhV2mb/WP93V441rD6XnM6MIluu7rk8qg38iSxS43ga2V2Q/2ib53PcqbDEJDG/yWQRJhQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.1.tgz", + "integrity": "sha512-Hrb/PurOJsmwHAsfMDH6oVpahkEGsx7F8CWMjyP/dw1qjqmdS9rcV1nYCGlM8nOtD3Wk/eaThzUB5TSZsGz+7Q==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "tslib": "^2.6.0" }, "engines": { "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.5.2.tgz", - "integrity": "sha512-lm8XL3xLkTPHFKKjLjEEAHUrW0SZBSHBE1I+i/tmYMBsjCcUB5UJ52geS5PSiOCFVR74tbPGcPHEV/gaaxFeSA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.1.tgz", + "integrity": "sha512-tKE8j1cEZCh8KZa4aa80zpSTxsC2/ZYqjx6AAfd8uA8VHZVw79+7OTEP2PoWi0uL5/1Is0LF5Vwxd+1fz5HlKg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -5569,639 +3811,27 @@ "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.5.2.tgz", - "integrity": "sha512-QkpX68PMOMu10Mvgvr5CfZAzZQFx8WLlOiUQ/Qmmcl6mjGK6H21WLT5x7xDmcpCoKA/3CegsqIqBR+nA137lQg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.1.tgz", + "integrity": "sha512-iqe3XKITBquZq+6UAXdb1vI0fPY5iIOitVjPQ581R1ZKpHr0qe+V6gVOrrcOHixPDD/BUKdYwkxFjpNiEN+vBw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "tslib": "^2.6.0" }, "engines": { "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/plugin-ideal-image": { @@ -6235,17 +3865,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.5.2.tgz", - "integrity": "sha512-DnlqYyRAdQ4NHY28TfHuVk414ft2uruP4QWCH//jzpHjqvKyXjj2fmDtI8RPUBh9K8iZKFMHRnLtzJKySPWvFA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.1.tgz", + "integrity": "sha512-+9YV/7VLbGTq8qNkjiugIelmfUEVkTyLe6X8bWq7K5qPvGXAjno27QAfFq63mYfFFbJc7z+pudL63acprbqGzw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -6254,648 +3884,61 @@ "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.1.tgz", + "integrity": "sha512-rW0LWMDsdlsgowVwqiMb/7tANDodpy1wWPwCcamvhY7OECReN3feoFwLjd/U4tKjNY3encj0AJSTxJA+Fpe+Gw==", "license": "MIT", "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", + "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { "node": ">=18.0" }, "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.5.2.tgz", - "integrity": "sha512-3ihfXQ95aOHiLB5uCu+9PRy2gZCeSZoDcqpnDvf3B+sTrMvMTr8qRUzBvWkoIqc82yG5prCboRjk1SVILKx6sg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.8.1.tgz", + "integrity": "sha512-yJSjYNHXD8POMGc2mKQuj3ApPrN+eG0rO1UPgSx7jySpYU+n4WjBikbrA2ue5ad9A7aouEtMWUoiSRXTH/g7KQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/plugin-content-blog": "3.5.2", - "@docusaurus/plugin-content-docs": "3.5.2", - "@docusaurus/plugin-content-pages": "3.5.2", - "@docusaurus/plugin-debug": "3.5.2", - "@docusaurus/plugin-google-analytics": "3.5.2", - "@docusaurus/plugin-google-gtag": "3.5.2", - "@docusaurus/plugin-google-tag-manager": "3.5.2", - "@docusaurus/plugin-sitemap": "3.5.2", - "@docusaurus/theme-classic": "3.5.2", - "@docusaurus/theme-common": "3.5.2", - "@docusaurus/theme-search-algolia": "3.5.2", - "@docusaurus/types": "3.5.2" + "@docusaurus/core": "3.8.1", + "@docusaurus/plugin-content-blog": "3.8.1", + "@docusaurus/plugin-content-docs": "3.8.1", + "@docusaurus/plugin-content-pages": "3.8.1", + "@docusaurus/plugin-css-cascade-layers": "3.8.1", + "@docusaurus/plugin-debug": "3.8.1", + "@docusaurus/plugin-google-analytics": "3.8.1", + "@docusaurus/plugin-google-gtag": "3.8.1", + "@docusaurus/plugin-google-tag-manager": "3.8.1", + "@docusaurus/plugin-sitemap": "3.8.1", + "@docusaurus/plugin-svgr": "3.8.1", + "@docusaurus/theme-classic": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/theme-search-algolia": "3.8.1", + "@docusaurus/types": "3.8.1" }, "engines": { "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/preset-classic/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/responsive-loader": { @@ -6923,30 +3966,31 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.5.2.tgz", - "integrity": "sha512-XRpinSix3NBv95Rk7xeMF9k4safMkwnpSgThn0UNQNumKvmcIYjfkwfh2BhwYh/BxMXQHJ/PdmNh22TQFpIaYg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.8.1.tgz", + "integrity": "sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/module-type-aliases": "3.5.2", - "@docusaurus/plugin-content-blog": "3.5.2", - "@docusaurus/plugin-content-docs": "3.5.2", - "@docusaurus/plugin-content-pages": "3.5.2", - "@docusaurus/theme-common": "3.5.2", - "@docusaurus/theme-translations": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/plugin-content-blog": "3.8.1", + "@docusaurus/plugin-content-docs": "3.8.1", + "@docusaurus/plugin-content-pages": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/theme-translations": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", - "infima": "0.2.0-alpha.44", + "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", - "postcss": "^8.4.26", + "postcss": "^8.5.4", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react-router-dom": "^5.3.4", @@ -6958,293 +4002,8 @@ "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/module-type-aliases": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz", - "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.5.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "*", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/theme-translations": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz", - "integrity": "sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw==", - "license": "MIT", - "dependencies": { - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-classic/node_modules/clsx": { @@ -7256,27 +4015,6 @@ "node": ">=6" } }, - "node_modules/@docusaurus/theme-classic/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/theme-classic/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/@docusaurus/theme-classic/node_modules/prism-react-renderer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", @@ -7290,48 +4028,16 @@ "react": ">=16.0.0" } }, - "node_modules/@docusaurus/theme-classic/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" - } - }, "node_modules/@docusaurus/theme-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.5.2.tgz", - "integrity": "sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.8.1.tgz", + "integrity": "sha512-UswMOyTnPEVRvN5Qzbo+l8k4xrd5fTFu2VPPfD6FcW/6qUtVLmJTQCktbAL3KJ0BVXGm5aJXz/ZrzqFuZERGPw==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/module-type-aliases": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", + "@docusaurus/mdx-loader": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-common": "3.8.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -7346,178 +4052,8 @@ }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/module-type-aliases": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz", - "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.5.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "*", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-common/node_modules/clsx": { @@ -7529,21 +4065,6 @@ "node": ">=6" } }, - "node_modules/@docusaurus/theme-common/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/@docusaurus/theme-common/node_modules/prism-react-renderer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", @@ -7557,383 +4078,44 @@ "react": ">=16.0.0" } }, - "node_modules/@docusaurus/theme-common/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@docusaurus/theme-mermaid": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.5.2.tgz", - "integrity": "sha512-7vWCnIe/KoyTN1Dc55FIyqO5hJ3YaV08Mr63Zej0L0mX1iGzt+qKSmeVUAJ9/aOalUhF0typV0RmNUSy5FAmCg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.8.1.tgz", + "integrity": "sha512-IWYqjyTPjkNnHsFFu9+4YkeXS7PD1xI3Bn2shOhBq+f95mgDfWInkpfBN4aYvx4fTT67Am6cPtohRdwh4Tidtg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.5.2", - "@docusaurus/module-type-aliases": "3.5.2", - "@docusaurus/theme-common": "3.5.2", - "@docusaurus/types": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "mermaid": "^10.4.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/types": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", + "mermaid": ">=11.6.0", "tslib": "^2.6.0" }, "engines": { "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/module-type-aliases": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz", - "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.5.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "*", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/types": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", - "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.5.2.tgz", - "integrity": "sha512-qW53kp3VzMnEqZGjakaV90sst3iN1o32PH+nawv1uepROO8aEGxptcq2R5rsv7aBShSRbZwIobdvSYKsZ5pqvA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.1.tgz", + "integrity": "sha512-NBFH5rZVQRAQM087aYSRKQ9yGEK9eHd+xOxQjqNpxMiV85OhJDD4ZGz6YJIod26Fbooy54UWVdzNU0TFeUUUzQ==", "license": "MIT", "dependencies": { - "@docsearch/react": "^3.5.2", - "@docusaurus/core": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/plugin-content-docs": "3.5.2", - "@docusaurus/theme-common": "3.5.2", - "@docusaurus/theme-translations": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "algoliasearch": "^4.18.0", - "algoliasearch-helper": "^3.13.3", + "@docsearch/react": "^3.9.0", + "@docusaurus/core": "3.8.1", + "@docusaurus/logger": "3.8.1", + "@docusaurus/plugin-content-docs": "3.8.1", + "@docusaurus/theme-common": "3.8.1", + "@docusaurus/theme-translations": "3.8.1", + "@docusaurus/utils": "3.8.1", + "@docusaurus/utils-validation": "3.8.1", + "algoliasearch": "^5.17.1", + "algoliasearch-helper": "^3.22.6", "clsx": "^2.0.0", "eta": "^2.2.0", "fs-extra": "^11.1.1", @@ -7945,253 +4127,8 @@ "node": ">=18.0" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/core": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", - "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/preset-react": "^7.22.5", - "@babel/preset-typescript": "^7.22.5", - "@babel/runtime": "^7.22.6", - "@babel/runtime-corejs3": "^7.22.6", - "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.5.2", - "@docusaurus/logger": "3.5.2", - "@docusaurus/mdx-loader": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.1.3", - "babel-plugin-dynamic-import-node": "^2.3.3", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "clean-css": "^5.3.2", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "copy-webpack-plugin": "^11.0.0", - "core-js": "^3.31.1", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "html-minifier-terser": "^7.2.0", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.5.3", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "mini-css-extract-plugin": "^2.7.6", - "p-map": "^4.0.0", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", - "react-helmet-async": "^1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "rtl-detect": "^1.0.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.5", - "shelljs": "^0.8.5", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "url-loader": "^4.1.1", - "webpack": "^5.88.1", - "webpack-bundle-analyzer": "^4.9.0", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.9.0", - "webpackbar": "^5.0.2" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/cssnano-preset": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", - "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.4.38", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/logger": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", - "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/mdx-loader": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", - "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-validation": "3.5.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^1.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/theme-translations": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz", - "integrity": "sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw==", - "license": "MIT", - "dependencies": { - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/utils": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", - "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "@svgr/webpack": "^8.1.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/utils-common": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", - "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/@docusaurus/utils-validation": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", - "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.5.2", - "@docusaurus/utils": "3.5.2", - "@docusaurus/utils-common": "3.5.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@docusaurus/theme-search-algolia/node_modules/clsx": { @@ -8203,59 +4140,6 @@ "node": ">=6" } }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT" - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia/node_modules/webpackbar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz", - "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.3", - "pretty-time": "^1.1.0", - "std-env": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" - } - }, "node_modules/@docusaurus/theme-translations": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.8.1.tgz", @@ -12063,12 +7947,6 @@ "form-data": "^4.0.4" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", @@ -12636,26 +8514,28 @@ } }, "node_modules/algoliasearch": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.27.0.tgz", - "integrity": "sha512-C88C5grLa5VOCp9eYZJt+q99ik7yNdm92l7Q9+4XK0Md8kL05Lg8l2v9ZVX0uMW3mH9pAFxMMXlLOvqNumA4lw==", + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.1.tgz", + "integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==", "license": "MIT", "dependencies": { - "@algolia/cache-browser-local-storage": "4.27.0", - "@algolia/cache-common": "4.27.0", - "@algolia/cache-in-memory": "4.27.0", - "@algolia/client-account": "4.27.0", - "@algolia/client-analytics": "4.27.0", - "@algolia/client-common": "4.27.0", - "@algolia/client-personalization": "4.27.0", - "@algolia/client-search": "4.27.0", - "@algolia/logger-common": "4.27.0", - "@algolia/logger-console": "4.27.0", - "@algolia/recommend": "4.27.0", - "@algolia/requester-browser-xhr": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/requester-node-http": "4.27.0", - "@algolia/transporter": "4.27.0" + "@algolia/abtesting": "1.15.1", + "@algolia/client-abtesting": "5.49.1", + "@algolia/client-analytics": "5.49.1", + "@algolia/client-common": "5.49.1", + "@algolia/client-insights": "5.49.1", + "@algolia/client-personalization": "5.49.1", + "@algolia/client-query-suggestions": "5.49.1", + "@algolia/client-search": "5.49.1", + "@algolia/ingestion": "1.49.1", + "@algolia/monitoring": "1.49.1", + "@algolia/recommend": "5.49.1", + "@algolia/requester-browser-xhr": "5.49.1", + "@algolia/requester-fetch": "5.49.1", + "@algolia/requester-node-http": "5.49.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/algoliasearch-helper": { @@ -12670,45 +8550,6 @@ "algoliasearch": ">= 3.1 < 6" } }, - "node_modules/algoliasearch/node_modules/@algolia/client-common": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.27.0.tgz", - "integrity": "sha512-ZrT6l/YPQgyIUuBCxcYPeXol2VBLUMuNb1rKXrm6z1f/iTiwqtnEEb16/6CC11+Re0ZGXrdcMVrgDRrzveQ1aQ==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/algoliasearch/node_modules/@algolia/client-search": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.27.0.tgz", - "integrity": "sha512-qmX/f67ay0eZ4V5Io8fWWOcUVo/gqre2yei1PnmEhQU2Gul6ushg25QnNrfu4BODiRrw1rwYveZaLCiHvcUxrQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "4.27.0", - "@algolia/requester-common": "4.27.0", - "@algolia/transporter": "4.27.0" - } - }, - "node_modules/algoliasearch/node_modules/@algolia/requester-browser-xhr": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.27.0.tgz", - "integrity": "sha512-dTenMBIIpyp5o3C2ZnfbsuSlD/lL9jPkk6T+2+qm38fyw2nf49ANbcHFE79NgiGrnmw7QrYveCs9NIP3Wk4v6g==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0" - } - }, - "node_modules/algoliasearch/node_modules/@algolia/requester-node-http": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.27.0.tgz", - "integrity": "sha512-y8nUqaUQeSOQ5oaNo0b2QPznyBFW9LoIwljyUphJ+gUZpU6O/j2/C8ovoqDpIe6J0etqHg5RCcBizrCFZuLpyw==", - "license": "MIT", - "dependencies": { - "@algolia/requester-common": "4.27.0" - } - }, "node_modules/altcha-lib": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/altcha-lib/-/altcha-lib-1.4.1.tgz", @@ -12888,15 +8729,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.27", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", @@ -15359,28 +11191,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "license": "MIT", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -15465,38 +11275,6 @@ "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -16458,15 +12236,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -16573,134 +12342,6 @@ } } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -16903,23 +12544,6 @@ "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", "license": "ISC" }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -16962,44 +12586,6 @@ "node": ">=10" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -17799,16 +13385,6 @@ "node": ">=16.x" } }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -17853,9 +13429,9 @@ } }, "node_modules/infima": { - "version": "0.2.0-alpha.44", - "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.44.tgz", - "integrity": "sha512-tuRkUSO/lB3rEhLJk25atwAjgLuzq070+pOW8XcvpHky/YbENnRRdPd85IBkyeTgttmOy5ah+yHYsK1HhUd4lQ==", + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", "license": "MIT", "engines": { "node": ">=12" @@ -17888,15 +13464,6 @@ "node": ">=12" } }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -18149,15 +13716,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -18200,15 +13758,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -21183,15 +16732,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -21779,15 +17319,6 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/package-json": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", @@ -21985,31 +17516,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-to-regexp": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", @@ -22078,79 +17584,6 @@ "pathe": "^2.0.3" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/pkijs": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", @@ -23876,15 +19309,6 @@ ], "license": "MIT" }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -23998,132 +19422,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -24146,12 +19444,6 @@ "react": "^18.0.0 || ^19.0.0" } }, - "node_modules/react-error-overlay": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", - "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", - "license": "MIT" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -24199,15 +19491,15 @@ "license": "MIT" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-loadable": { @@ -24444,23 +19736,6 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" - }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/recma-build-jsx": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", @@ -24528,18 +19803,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -25000,22 +20263,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -25034,12 +20281,6 @@ "points-on-path": "^0.2.1" } }, - "node_modules/rtl-detect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", - "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", - "license": "BSD-3-Clause" - }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -25126,10 +20367,13 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", - "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "license": "BlueOak-1.0.0" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.23.2", @@ -25140,6 +20384,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -25159,13 +20409,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/search-insights": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", - "license": "MIT", - "peer": true - }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -25580,23 +20823,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "license": "BSD-3-Clause", - "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -26332,12 +21558,6 @@ "b4a": "^1.6.4" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -26509,20 +21729,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/ufo": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", @@ -27729,15 +22935,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", diff --git a/docs/my-website/package.json b/docs/my-website/package.json index a864301d77f..acdd7463a36 100644 --- a/docs/my-website/package.json +++ b/docs/my-website/package.json @@ -15,10 +15,10 @@ }, "dependencies": { "@docusaurus/core": "3.8.1", - "@docusaurus/plugin-google-gtag": "^3.5.2", + "@docusaurus/plugin-google-gtag": "3.8.1", "@docusaurus/plugin-ideal-image": "3.8.1", - "@docusaurus/preset-classic": "^3.5.2", - "@docusaurus/theme-mermaid": "^3.5.2", + "@docusaurus/preset-classic": "3.8.1", + "@docusaurus/theme-mermaid": "3.8.1", "@inkeep/cxkit-docusaurus": "^0.5.89", "@mdx-js/react": "^3.0.0", "clsx": "^1.2.1", From dd183a7fcb5dc734ddc770bad1663da961945ece Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 17:47:01 -0800 Subject: [PATCH 158/171] [Feat] UI - Allow sorting MCPs by created_at, Display name date (#22825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add column sorting to MCP servers table - Added sorting state management to DataTable component - Enabled getSortedRowModel for tanstack/react-table - Made column headers clickable with sort indicators (↑↓⇅) - Added enableSorting: true to sortable columns in mcp_server_columns - Columns now support ascending/descending sort by clicking headers - Updated package-lock.json and tsconfig.json from build process Co-authored-by: Ishaan Jaff * Make table sorting opt-in to avoid conflicts with existing consumers Address Greptile feedback (score 2/5): - Added enableSorting prop to DataTable (defaults to false) - Only enable sorting features when explicitly requested - Pass enableSorting=true from MCP servers component - This prevents unintended sorting on other DataTable consumers: * view_logs (has server-side sorting) * pass_through_settings * UsagePage - Sorting UI (indicators, click handlers) only shown when enabled Co-authored-by: Ishaan Jaff --------- Co-authored-by: Cursor Agent Co-authored-by: Ishaan Jaff --- ui/litellm-dashboard/package-lock.json | 15 -------- .../mcp_tools/mcp_server_columns.tsx | 7 ++++ .../src/components/mcp_tools/mcp_servers.tsx | 1 + .../src/components/view_logs/table.tsx | 36 ++++++++++++++++--- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 697ec68e0bb..200182cf551 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -12984,21 +12984,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", - "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx index c42a8593cf1..b30caa16d6b 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx @@ -15,6 +15,7 @@ export const mcpServerColumns = ( { accessorKey: "server_id", header: "Server ID", + enableSorting: true, cell: ({ row }) => (
diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index 77cb273d4fe..38c00490b78 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -1,5 +1,5 @@ -import { Fragment } from "react"; -import { ColumnDef, flexRender, getCoreRowModel, getExpandedRowModel, Row, useReactTable } from "@tanstack/react-table"; +import { Fragment, useState } from "react"; +import { ColumnDef, flexRender, getCoreRowModel, getExpandedRowModel, Row, useReactTable, getSortedRowModel, SortingState } from "@tanstack/react-table"; import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react"; @@ -15,6 +15,8 @@ interface DataTableProps { isLoading?: boolean; loadingMessage?: string; noDataMessage?: string; + /** Enable client-side column sorting (defaults to false to avoid conflicts with server-side sorting) */ + enableSorting?: boolean; } export function DataTable({ @@ -27,18 +29,28 @@ export function DataTable({ isLoading = false, loadingMessage = "🚅 Loading logs...", noDataMessage = "No logs found", + enableSorting = false, }: DataTableProps) { const supportsExpansion = !!(renderSubComponent || renderChildRows) && !!getRowCanExpand; + const [sorting, setSorting] = useState([]); const table = useReactTable({ data, columns, + ...(enableSorting && { + state: { + sorting, + }, + onSortingChange: setSorting, + enableSortingRemoval: false, + }), ...(supportsExpansion && { getRowCanExpand }), getRowId: (row: TData, index: number) => { const _row: any = row as any; return _row?.request_id ?? String(index); }, getCoreRowModel: getCoreRowModel(), + ...(enableSorting && { getSortedRowModel: getSortedRowModel() }), ...(supportsExpansion && { getExpandedRowModel: getExpandedRowModel() }), }); @@ -49,9 +61,25 @@ export function DataTable({ {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { + const canSort = enableSorting && header.column.getCanSort(); + const isSorted = header.column.getIsSorted(); + return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + {header.isPlaceholder ? null : ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {canSort && ( + + {isSorted === 'asc' ? '↑' : isSorted === 'desc' ? '↓' : '⇅'} + + )} +
+ )}
); })} From 9897df50898b864ba74a80725885f19f0f27f5df Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 17:58:05 -0800 Subject: [PATCH 159/171] feat(mcp): allow admins to override tool name and description per MCP server (#22828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mcp): add tool_name_to_display_name and tool_name_to_description overrides for MCP servers * docs(mcp): add mcp_openapi.md with OpenAPI→MCP guide and tool override section * docs(mcp): add sequential UI screenshots to mcp_openapi.md * fix(mcp): apply tool overrides after permission filtering; reverse-map display names in tools/call --- docs/my-website/docs/mcp.md | 170 +------------ docs/my-website/docs/mcp_openapi.md | 226 ++++++++++++++++++ .../img/mcp_openapi_custom_name_badge.png | Bin 0 -> 147052 bytes .../img/mcp_openapi_tool_edit_panel.png | Bin 0 -> 154571 bytes .../img/mcp_openapi_tools_loaded.png | Bin 0 -> 105089 bytes docs/my-website/sidebars.js | 1 + litellm/proxy/_experimental/mcp_server/db.py | 6 + .../mcp_server/mcp_server_manager.py | 6 + .../proxy/_experimental/mcp_server/server.py | 53 ++++ litellm/proxy/_types.py | 6 + litellm/proxy/schema.prisma | 2 + .../types/mcp_server/mcp_server_manager.py | 2 + .../mcp_server/test_mcp_server.py | 74 ++++++ .../mcp_tools/create_mcp_server.tsx | 8 + .../components/mcp_tools/mcp_server_edit.tsx | 12 +- .../mcp_tools/mcp_tool_configuration.tsx | 167 ++++++++++--- .../src/components/mcp_tools/types.tsx | 2 + 17 files changed, 537 insertions(+), 198 deletions(-) create mode 100644 docs/my-website/docs/mcp_openapi.md create mode 100644 docs/my-website/img/mcp_openapi_custom_name_badge.png create mode 100644 docs/my-website/img/mcp_openapi_tool_edit_panel.png create mode 100644 docs/my-website/img/mcp_openapi_tools_loaded.png diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index fcbb31c07d3..822b3f5fabc 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -336,175 +336,9 @@ litellm_settings: ## Converting OpenAPI Specs to MCP Servers -LiteLLM can automatically convert OpenAPI specifications into MCP servers, allowing you to expose any REST API as MCP tools. This is useful when you have existing APIs with OpenAPI/Swagger documentation and want to make them available as MCP tools. +LiteLLM can convert OpenAPI specifications into MCP servers, exposing any REST API as MCP tools without writing custom server code. -**Benefits:** - -- **Rapid Integration**: Convert existing APIs to MCP tools without writing custom MCP server code -- **Automatic Tool Generation**: LiteLLM automatically generates MCP tools from your OpenAPI spec -- **Unified Interface**: Use the same MCP interface for both native MCP servers and OpenAPI-based APIs -- **Easy Testing**: Test and iterate on API integrations quickly - -**Configuration:** - -Add your OpenAPI-based MCP server to your `config.yaml`: - -```yaml title="config.yaml - OpenAPI to MCP" showLineNumbers -model_list: - - model_name: gpt-4o - litellm_params: - model: openai/gpt-4o - api_key: sk-xxxxxxx - -mcp_servers: - # OpenAPI Spec Example - Petstore API - petstore_mcp: - url: "https://petstore.swagger.io/v2" - spec_path: "/path/to/openapi.json" - auth_type: "none" - - # OpenAPI Spec with API Key Authentication - my_api_mcp: - url: "http://0.0.0.0:8090" - spec_path: "/path/to/openapi.json" - auth_type: "api_key" - auth_value: "your-api-key-here" - - # OpenAPI Spec with Bearer Token - secured_api_mcp: - url: "https://api.example.com" - spec_path: "/path/to/openapi.json" - auth_type: "bearer_token" - auth_value: "your-bearer-token" -``` - -**Configuration Parameters:** - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `url` | Yes | The base URL of your API endpoint | -| `spec_path` | Yes | Path or URL to your OpenAPI specification file (JSON or YAML) | -| `auth_type` | No | Authentication type: `none`, `api_key`, `bearer_token`, `basic`, `authorization` | -| `auth_value` | No | Authentication value (required if `auth_type` is set) | -| `authorization_url` | No | For `auth_type: oauth2`. Optional override; if omitted LiteLLM auto-discovers it. | -| `token_url` | No | For `auth_type: oauth2`. Optional override; if omitted LiteLLM auto-discovers it. | -| `registration_url` | No | For `auth_type: oauth2`. Optional override; if omitted LiteLLM auto-discovers it. | -| `scopes` | No | For `auth_type: oauth2`. Optional override; if omitted LiteLLM uses the scopes advertised by the server. | -| `description` | No | Optional description for the MCP server | -| `allowed_tools` | No | List of specific tools to allow (see [MCP Tool Filtering](#mcp-tool-filtering)) | -| `disallowed_tools` | No | List of specific tools to block (see [MCP Tool Filtering](#mcp-tool-filtering)) | - -### Usage Example - -Once configured, you can use the OpenAPI-based MCP server just like any other MCP server: - - - - -```python title="Using OpenAPI-based MCP Server" showLineNumbers -from fastmcp import Client -import asyncio - -# Standard MCP configuration -config = { - "mcpServers": { - "petstore": { - "url": "http://localhost:4000/petstore_mcp/mcp", - "headers": { - "x-litellm-api-key": "Bearer sk-1234" - } - } - } -} - -# Create a client that connects to the server -client = Client(config) - -async def main(): - async with client: - # List available tools generated from OpenAPI spec - tools = await client.list_tools() - print(f"Available tools: {[tool.name for tool in tools]}") - - # Example: Get a pet by ID (from Petstore API) - response = await client.call_tool( - name="getpetbyid", - arguments={"petId": "1"} - ) - print(f"Response:\n{response}\n") - - # Example: Find pets by status - response = await client.call_tool( - name="findpetsbystatus", - arguments={"status": "available"} - ) - print(f"Response:\n{response}\n") - -if __name__ == "__main__": - asyncio.run(main()) -``` - - - - - -```json title="Cursor MCP Configuration for OpenAPI Server" showLineNumbers -{ - "mcpServers": { - "Petstore": { - "url": "http://localhost:4000/petstore_mcp/mcp", - "headers": { - "x-litellm-api-key": "Bearer $LITELLM_API_KEY" - } - } - } -} -``` - - - - - -```bash title="Using OpenAPI MCP Server with OpenAI" showLineNumbers -curl --location 'https://api.openai.com/v1/responses' \ ---header 'Content-Type: application/json' \ ---header "Authorization: Bearer $OPENAI_API_KEY" \ ---data '{ - "model": "gpt-4o", - "tools": [ - { - "type": "mcp", - "server_label": "petstore", - "server_url": "http://localhost:4000/petstore_mcp/mcp", - "require_approval": "never", - "headers": { - "x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY" - } - } - ], - "input": "Find all available pets in the petstore", - "tool_choice": "required" -}' -``` - - - - -**How It Works** - -1. **Spec Loading**: LiteLLM loads your OpenAPI specification from the provided `spec_path` -2. **Tool Generation**: Each API endpoint in the spec becomes an MCP tool -3. **Parameter Mapping**: OpenAPI parameters are automatically mapped to MCP tool parameters -4. **Request Handling**: When a tool is called, LiteLLM converts the MCP request to the appropriate HTTP request -5. **Response Translation**: API responses are converted back to MCP format - -**OpenAPI Spec Requirements** - -Your OpenAPI specification should follow standard OpenAPI/Swagger conventions: -- **Supported versions**: OpenAPI 3.0.x, OpenAPI 3.1.x, Swagger 2.0 -- **Required fields**: `paths`, `info` sections should be properly defined -- **Operation IDs**: Each operation should have a unique `operationId` (this becomes the tool name) -- **Parameters**: Request parameters should be properly documented with types and descriptions +See the **[MCP from OpenAPI Specs guide](./mcp_openapi.md)** for full setup, usage examples, and how to override tool names and descriptions. ## MCP OAuth diff --git a/docs/my-website/docs/mcp_openapi.md b/docs/my-website/docs/mcp_openapi.md new file mode 100644 index 00000000000..0f18ecc127a --- /dev/null +++ b/docs/my-website/docs/mcp_openapi.md @@ -0,0 +1,226 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import Image from '@theme/IdealImage'; + +# MCP from OpenAPI Specs + +LiteLLM can convert any OpenAPI/Swagger spec into an MCP server — no custom MCP server code required. + +## Step 1 — Add the MCP Server + +Add your OpenAPI-based server in `config.yaml`: + +```yaml title="config.yaml" showLineNumbers +mcp_servers: + petstore_mcp: + url: "https://petstore.swagger.io/v2" + spec_path: "/path/to/openapi.json" + auth_type: "none" + + my_api_mcp: + url: "http://0.0.0.0:8090" + spec_path: "/path/to/openapi.json" + auth_type: "api_key" + auth_value: "your-api-key-here" + + secured_api_mcp: + url: "https://api.example.com" + spec_path: "/path/to/openapi.json" + auth_type: "bearer_token" + auth_value: "your-bearer-token" +``` + +Or from the UI: go to **MCP Servers → Add New MCP Server**, fill in the URL and spec path, and LiteLLM will fetch the spec and load all endpoints as tools. + +**Configuration parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `url` | Yes | Base URL of your API | +| `spec_path` | Yes | Path or URL to your OpenAPI spec (JSON or YAML) | +| `auth_type` | No | `none`, `api_key`, `bearer_token`, `basic`, `authorization`, `oauth2` | +| `auth_value` | No | Auth value (required if `auth_type` is set) | +| `description` | No | Optional description | +| `allowed_tools` | No | Allowlist of specific tools | +| `disallowed_tools` | No | Blocklist of specific tools | + +**Supported spec versions:** OpenAPI 3.0.x, 3.1.x, Swagger 2.0. Each operation's `operationId` becomes the tool name — make sure they're unique. + +Once tools are loaded, you'll see them in the Tool Configuration section: + + + +
+ +## Step 2 — Optionally Override Tool Names and Descriptions + +By default, tool names and descriptions come from the `operationId` and description fields in your spec. You can rename or rewrite them so MCP clients see something cleaner — without touching the upstream spec. + +### From the UI + +Each tool card has a pencil icon. Click it to open the inline editor: + + + +
+ +- **Display Name** — overrides the name MCP clients see +- **Description** — overrides the description MCP clients see +- Leave a field blank to keep the original from the spec + +After setting overrides, a purple **Custom name** badge appears on the tool card: + + + +
+ +### From the API + +Pass `tool_name_to_display_name` and `tool_name_to_description` in the create or update request: + +```bash title="Create server with tool name overrides" showLineNumbers +curl -X POST http://localhost:4000/v1/mcp/server \ + -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "petstore_mcp", + "url": "https://petstore.swagger.io/v2", + "spec_path": "/path/to/openapi.json", + "tool_name_to_display_name": { + "getPetById": "Get Pet", + "findPetsByStatus": "List Available Pets" + }, + "tool_name_to_description": { + "getPetById": "Look up a pet by its ID", + "findPetsByStatus": "Returns all pets matching a given status (available, pending, sold)" + } + }' +``` + +```bash title="Update overrides on an existing server" showLineNumbers +curl -X PUT http://localhost:4000/v1/mcp/server/{server_id} \ + -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name_to_display_name": { + "getPetById": "Get Pet" + }, + "tool_name_to_description": { + "getPetById": "Look up a pet by its ID" + } + }' +``` + +The map key is the **original `operationId`** from the spec — not the prefixed tool name. LiteLLM strips the server prefix before doing the lookup. + +For example, if your server is `petstore_mcp`, the tool is exposed as `petstore_mcp-getPetById`. The map key is still `getPetById`. + +**Before and after:** + +``` +# Without overrides +Tool: "petstore_mcp-getPetById" +Description: "Returns a single pet" + +Tool: "petstore_mcp-findPetsByStatus" +Description: "Finds Pets by status" + +# After overrides +Tool: "Get Pet" +Description: "Look up a pet by its ID" + +Tool: "List Available Pets" +Description: "Returns all pets matching a given status (available, pending, sold)" +``` + +## Using the Server + + + + +```python title="Using OpenAPI-based MCP Server" showLineNumbers +from fastmcp import Client +import asyncio + +config = { + "mcpServers": { + "petstore": { + "url": "http://localhost:4000/petstore_mcp/mcp", + "headers": { + "x-litellm-api-key": "Bearer sk-1234" + } + } + } +} + +client = Client(config) + +async def main(): + async with client: + tools = await client.list_tools() + print(f"Available tools: {[tool.name for tool in tools]}") + + response = await client.call_tool( + name="Get Pet", # overridden name + arguments={"petId": "1"} + ) + print(f"Response: {response}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +```json title="Cursor MCP Configuration" showLineNumbers +{ + "mcpServers": { + "Petstore": { + "url": "http://localhost:4000/petstore_mcp/mcp", + "headers": { + "x-litellm-api-key": "Bearer $LITELLM_API_KEY" + } + } + } +} +``` + + + + + +```bash title="Using OpenAPI MCP Server with OpenAI" showLineNumbers +curl --location 'https://api.openai.com/v1/responses' \ +--header 'Content-Type: application/json' \ +--header "Authorization: Bearer $OPENAI_API_KEY" \ +--data '{ + "model": "gpt-4o", + "tools": [ + { + "type": "mcp", + "server_label": "petstore", + "server_url": "http://localhost:4000/petstore_mcp/mcp", + "require_approval": "never", + "headers": { + "x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY" + } + } + ], + "input": "Find all available pets", + "tool_choice": "required" +}' +``` + + + diff --git a/docs/my-website/img/mcp_openapi_custom_name_badge.png b/docs/my-website/img/mcp_openapi_custom_name_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..11f94c1e68c707c5ea0e8f9d9fcc960c322812ef GIT binary patch literal 147052 zcmb@tWl$YK*Dgv7*8ssGK#-sTf?I-1aQ6^`yTjf{2<{NvVdL)Z5Zv9}ZR2itAn#lE z)K}-$JwML;fU23E-aWlmFL|D|{AHy@Q4sMFVPIfT#KnZ2|O59(7`;Mtk0((@wv-oCKUVZkZeoW)#qM*+{sUSMG^$QERYau!7Wmv_? zkAgFT7mMj;h>@e>my713hvM3P#_?Xv0hz9DUr6|IL{`UD^lpM0x^DQBL ztXHJ}hT(PmpAK_EyS~ca>4p9GUTzxt&p&t%2=;`EOlZ&3%k8uwrZu_P!$(ehu~(9f zD()S?>EZ0nDB$_}{G*BipV0At5gPeWQwtd+(@;ucCxu5urLQxj|9jjEJpPlC62rJ! z7FjJ{W*bSPmmKn!R@ux^EQ?WV!lV87Ns7Sh50WB$ znKX~e>e7lJ3%924uph{P3nCxnQ39jXR1=j<5EULXME!4DGKm z|B}|r@%<|US|#>B<+gZm{(iufWA^=hgKf`Or6#{Y#m)GdIrt-n=$)*U*_>A5WKum1flw)^s>X(fhNhBHM}L{l7BxKnASc>Z!4n>x z&unduP|jRVyW`)YX%`B&SyiAv1BuRbdQTH2k>>O#M0TOnR7cjTdmTZKj;_BW{o?Nc z%JY=7vz4YJ=INx}89xxbqG;;;so(@sCH$`X6J6G(2r957gdKdS51Qs;J@`MXo7kk9 z$$)!N-GvF;A6|)89nRkl4Z2nfv~k&+jb-Z9Z&8KPhWMQy{rRJSWndsJ9U_1^EfVBP z9K0!o&uTg=%R|Qg`#1mNuCGG&kgmn&0-b7wt}4q0zKO}ntfSYl(J|TIBxS10Dk{p$ zv7f|f$5`BWF(W_VO=j;{*EuG}`S1-M0F%c!JnQw**d8uqOc) z%ij-mswbQ7MaSLYLhkC8X$`5UT3@CQ=btoKst;ylIa#F+#S7oVbY-yRz{vs=h3v<}8Wt-IVmi|sR`ANhO zGNXmM%FxiEJf(aAK|xnnS1Gi_;u*Y8kH^rrHD)u+c3nYudi&bn9BwZTgqdhNzc_Jo zDI4W^8zu5O_Dp}rO|=$uKTZ*N*RS+$*OcN3M)j{zr7J>0##?ffRvd^&L<~Ov3+BuGzAif4C zMLttr|417%RNzKnorQ%3dnbj6f7gw~=*N%m$A9?4VVA&rl~X!#zY}(L@YX0BgKA4% zyB4$I&apdJykirV^h^RF)Wz{n`i*rui5}W^zBh$N%o7_Hw!FJLo2!sbqm&0fplz`2 ze0`$-*<`Z7|AG2bXJ;p*V!B9+JCa)NJ0Z72L8&zv;$R#LoE~-J%`nS{4}VpaT`B_h zYAC&03b`*tggd$u#9TWcczEVJ+@+$5AI4&0Qr_anM8!gl3@y#|Gv#T7KT`DlqC6pn zw!xfC3+O2qX@^2h8Tr}mtlTrcR?s>tY98Hh8PVO?$H}u?q6aKKZkJCI-PVF{9S?Zz z7TNhp@49`_2<|l-DU(o1cxP^N=nS_rml!H2Cr1N9J( z79Zbk!a%0fq{u`F_h4S7#=i?3#*stG}=l|39GFjbFigz{|P!1GopHXX@Gn;*0wa#Id8gH6?0L`poRKZ zi)7$hevt=shh;#nZU1J7>y>W^zx#37lQPmow6(SU6_k{aP%k%-sx+DKK_v?KQuCuD z0PqKAOO4=5e@v=)cFV;qsf1s@ewoizA)I&K^B^|^M-dhUfBqC+M5yT%9P0bqoeLp+ zp&?WvF262_>PymH4>8Z#gfdv8{X>5K<>CHiY>E?Mc_Ej(gN%lT#PRXJUNwFhHCA#U z(i3bxcVvP0CZ0yiMbVXX`q(3}EH7||iTHm0vKZQRQ!3Ws0vQd7MCRGp*oM2fT{&Aj zI9!Ca-nX@r^Iw}iXdJrI`645Q1~m||p2TfoP!dcQ6A<9=-$oP{X`!`g#l@-B?Dbvq zKh}a=PYr^Hw}!(p{lrf^y2HdkaHvG>9Qn6OU$rczw|m5r1uz$%tQ2;wHZRxhRIYc> zRF*<>Y69t?wDiGTRV3QsbQ5+mYVe#KlheD}lor79eHtvBbF4NUgNUc!i2Zow6BKdW zfA)9VWW0p9&a&Z_-<&vKdBSkDCLA}r6=Ax>_%;J43+|0?5FuMhlY5fuZK&NLH=%!N zS&ZR5$o|u>1k*Rj`+Mn&z23DrcBj!7wd;x<9aO2o!*hfjYo#o6TZ=dTOfL@o*87^4lkN_Mx7hulW0O)t2Q316ETl z9+UC`g#UxBQwc)VRIAjw5Cf@mr?4B%1hRe8jX*M{|rTkcR4RAfT(9D&+TD z*F@e0WMT)2QVU87c6P`U-bqrTzQ>mK-1dK+v*54z4k0+0l$&a+fD&D~5Yp`K;C-_3 zQ*Z8K!S5%10{hI>)Y}Zc@zq&-x7})fWj+=IBTtb1#;0Pbgx~n&_}u|HBxHXk3aqyj zewymp8Xbywk@1wMAH0J%KHYLPGc`Tk4Z5^G^kHpgw@@Srb~JRo8H4aYs5j%O!A}UB zsWrOjRx|7#&@Mfm%@xd~Fly9ZcsgbSV=X6UQBYAY`1?~dt~MGjI^pWjG_UrUUr#-{ z-kg_p@W&?OE`&Gm+RmVp1fJ=xWl%fZOp%H({1!5w~~+MwZVH4?$-J#q>sn79%&g}Ct) zU&+n<`mOC$Q zN|dqSy%4>rx3uK+qod;Eu?Qm2lj2>uKWaqEpCDqE(WBvD#h;`uN@sKhA2P=$*lsb{ z+NMiJ&hMRcd|cZ=&14vqJ}GHD-FjR|lLK2NefvQroiwpHQ7&rum*%=fnrNcqvSsgU zw_cg-yOc1$3V(h>obwse!NkVc*hm-J5BU0C1n*9uLKlyYeJy)`Z!PUr2Z0^bw~_^a{8&ZiJ6!f~HEcYsOHXgl9oup^ zRD{~A&*a-KKKCsv~5(ir|0^KHbpZ{JqrYU}GE-+Y_bBO_4~s@xwoLSsvc zo9>;VU5D7jSECy(;*8&7xU=|zni}gGk8j3Fa2^=YKK|7>JsB4-ZszgE5deZ%NLjj4 zzOsRVf$HPe_nLt5=Pl-0ffh+Ki0Pm2PDHpeGcyA&BJ6m05U?WExv(a`n(t1Kd?F1Q z3W0QDB5(IxBuWC%x83FCUoZZ;w&kF|+z3ezJpgv|$2^*R!r3@g=+20Eq^6&T0hH~X zu5m4B?|ia~O}Q$0H@VjNSU2;!pD+iG46*m3EN&2Xh)M5;?0g{pv(S4Ur>m=+70(LZ z^0G3%t0f#7rHAZCaJ7$)lH!rYQiz3JHHgreTiqz6028gZtDd<|%=FRhM&TmdGBr5Z zUb?8x2+xjXy{T&_o!C&-DWgh8JNd0KdQVBD- z-ut?%HQYrEZK|A~Gr5{5JjHhMx%PXLy1KfeA%sk>ZaCvG`AYd!#$%N0b-huv*%=x8 z3-wdIy%Ii+K(uPH*x)>z`Zb5EmshwihB4Gvu~?hv{`PEVZF}44?&|n-YpD6*W^dJ_ z+Y`Y%OP!jS@g0g+-4`RueLC!rcL8l%F-!_exqND}A!@lvD&20>FeWl&0n6DazJGX< zhtI@7c6)wM$>Jtg77oRWuWYv_55I+*>1Do2RF!q5OX&@M@_OP{N)UA-m zgUeoxXiRva^_Kj{*=>dNoq5J4%=qyS zpQ*0Zg=g9 z50q?0u5;1li_Z54abv?Q)K}5}!j~`vG$Jm$p;lNlV$E#nBzlc1I@OZz8@*AgkM5gs zhKZkX#)>qm8B9F)CJS#b<}4i#W+%!GWz^&dIjp{CN<=-}oel{HzVr6+f%F=hn%=hH z+?y1W64(F8MPEGB^>{q6hi5&Ey>YcISIXEVE5Mq+l(Fp_?W|Kz=YmB30sHR0LpbmL zowl=wj*34_wS1R(=yq4BmO$(ZvM*fSvt1p>`$;pv5IFSX7awmu*{%6fsbfo+wN%1q zp-S}NUnmOy#R4#LbPk~?1ml=!D{G;P@yzxHOCEYY$WTj5!`o9v$*?kIMirg19~T92 z)b%Jttj_Bt9(dg;w2uNq53Una9GoaMBeyJ6%^a>r ze*W#rZivEz*!h!b*^8N2Qi3ZM^Rc@8cMUaC*=|l30Z=FoqmEXyoGayO_RTpn$BPGh zQ&R7cq$~NpO{rm>1idu3#@X25Z5HcZQ|cCnz0oxLJ_Ey73_-+?tHZ7LYpa-S<}+zT z99&!%Nopx{clJ9O2|+ZTtq*~t>$zS&t> zm6Tpzx_G>M_YU4;q26(5a8P7@?4r7=ikSJQpL}lrz`z9R;mXP}wEi%FO0@KK8_QNo z#usW*^tw59;fzdun&6+H(@_@>OFVeM4v!(Z<)DOIlhx|Ct|0_}4Ck&+Q9Bi_qw)ImfocfOGc_tM>i!hMFPr&Ss@X)T>E@1no#DDgMLQk~h}rtJ zX1`EZZ}afxSSnZDcr@RazGY8fHNVszB8H}x_j)yl(rjm&+<^yA7hCXcMk6H_GNgPG7A6vEi|Ec9v-nXEHLKI ze`N6fiud3RPiHV*a%3VikdYj^Ud6eL7B|NRzh-#e@H`B15v=1?OjRO#dwW)g7z_My z<|%X;x^m~!O@H~;Wi?(XXY7SRQ(dk^x1zRGZ?9agW&FlcBl}}W62B{@1k5*tD#5>h zDV#wy0iqJxY1;F=13^56+q|Zp-U}fCA^yR|YK`%WQMezzxoB$suCL#FH8qAJAuc{J z(8uC5i~IV6cU)XVgx*ShnXOG2^d})b9VbOc+kD&N!-IAcLxY;`O1QQ4u~PmGKF@18 z%a?B>{tRFvdPW4uZLF`Ws;L=oX&4y1)2=M#!~+745iZjU+vj7i94==KEn?^v4t02Q z=rNGYtC6fIVpMh2mrFArb(U4@$C&2TFzVa2hrXywi4HTMX@*5y6;Og4CF{>DX27rb zW`k2WdtpzAxl{2Z$B$p{ay0?+gDFvc^1I(3&BS0IGE-Vw-TC(Hk1h&VPT=e9M2(5F z*RdRyC?ekSX%-~w=APjiG`$~{o}iqZAiei=Uv#sM94dN-h61`}^G*(ve~7RJDE^{D zL4Qd?2ltF)jYN1G;@!u@ZAL=gA+nHA5ioLl3mfwpggpt$&(EU--JG)Ez)Uy0n;kQL z4ZiQ2^S>v(s8HrS87PT~g@N+m&CBfrc)OfHHB+3ThvhC;}Ec(QH2 zPfo69k(q*;4K@)C2~0&JveTb2EHcpQC-lXLf_L#BokH*c z{{EcesVB`5N7l#Op2wvf6Lo>?X604_gU;l?+e9+& zkf=n#ax_>>B|9it;PNko{Zp_4_JWz>UcaB`kl+!8Dl*C5Q*cM<7 zpW#)xW4-qo+-&A^ygQlIT<63ynM>QXI`@fjQ53}W6=y+OU^J=^ln4VKwGG`b7J4fa zgg%x%4j-?qWe+@BTie+sw!T2`&K|h?w0x)k40bBs)$~#49!%R>4dU>h?a#W+=VYhr zt-9Um&lH9orEIy=5f@L_TwOj~ZiMO#Fz5_+^!J-h94^hw%>>A=U-iJG^d{%WxG?_P z3>a7e`KeXew`L2F9=OAXykSk*OabSZYW};`-OAKi)_XuE$_0z)Ru2*sXw}*a+*M|i z@UL7R&VR?V=J|LDYuAF(wGs?o-PqaM*}>OXId|*^zq)aF1g|;3f$Kf)L7y$Q89%F7 z-uZExSI<=HP;Bj$?*&YDhI1_w(yS)brmpb>NWD+Ya<&v9Eo(DPX)?F12XflTHG?{jmF3pG9yKiKg* zyCwjT(!snfH496=^BAn2C~{d@Sx}(Nm3a?-f4f8!O$h$%QnU1cPE5ff_(;`3dXV67 z$o0Gf!AS2)e9dQVCHoaa(T(Y8BANmGlm0-u{OW`Ld|B@4?%p3uk649nuyQ*ZxmtDY z0-Lt6xzYG=(Iv+1r^{LET-?BPyB>FnyL^=k^Rv5Qz5#zC-=+C`Pp5hP2nz$lamxxO z|9x3OllwvH@-U7zm?Rwe-u=P8BWCG-v_j4mNFXc+Z;(9@d7Cv#))PqF1eee9cTca` zksj|%Syq3vgY2zT3U?<=u9U)u-V(m`=?o2w)M=$xN4Dvgx^KRUtrTF;;{duL?Y>kxr7A44V6P%(Rn^0}C=rmTfaOi$lS(@w{ znv{GDrP|O+)c9=5@tD2jexuI`BIdO=xR|$0_kiK>@LExrSl-$6+m=O4x!($s0zk95f+;y%45_krz&Heb~|GVI%QFZ|&HQCR5_cW{n66wt60rRjHAFzdMqAMfj&NYwxWmD}U|87ce;g^ev=(fP#&bH`Ik6f2c%2m0)09)#c{znpvD zkWi76k=XOiKI%PIOL>xy+w&c?QusIN-`EeXt;v(2*=|LC)Il#QE2B)=q>1@gXAFJP z3=8omn9IvFH4{O0=G9I!@qeKvfdgQusd;{kT(MSg=*~#&r6>F`H&mf&JkXSO#JJ>sygJT@pI5udu5mesg$+9Z*_*BosmdtAkC@N7=3JCg zLv?TDmJ)7m=(xG(Ld-S3#c}j@%oyKD4!K?57dOJmVFSjycv+v%>r9` zBW?`4f^U#`>nfUAeyt8_jW9V5R^VY{#v~;jfZ267aT0k{rcSmymm1#+YP!6vV;Bs9 zO_9c7UZ2(H_wf=Tez0!-eVZ=Qm;0$(IKy{EHxgfSUFK5_$}DpPM)6Ce2XCA%{Do?UAtTz z^#X?>1)+w+WR=!*$`W%ocFPOe^s1{u9C^Kk|Gw^S-kTOMObj=A*uQB<$9Yq3f-kq4 zPDgkMGzd5c-bL>Ykg(q8L8ooe*Jj+euNGg4ZiF4Ev(MFTp>M)yZ}wkqHeEI|4g{U; zPd9Dw*P|u#q&F$PFI!qSw#-+|%cPwOPGBY{C9PG9xB;jlHC0uzFR0+nO#bM{jSaO@ z=e3(%!|Iap1Rl#`33dyg<&O;7O;tCC^v_EAes;7Y@C61Y>2OXR3;MmgJ)TZoA<@@1JDaQ+N@xaa1o8{WtCrN*c5y}N2_YYZ3%~il`ipmsAU@y6w#tWov1BUNd;YF;l zfGy9u8Oi--dDHhU&ve^*NQ*s%~e~x!zcDX zrtQZP$o7+X;=Itl6p@u_WI!97Pla+*!Fh=gv@G!0ccW+&=lHPyN#}NudoziObkuNg z&(6*4EC8FTYigR0!h-gmn;WiN4*ETJ4pYn2ly)H*7g@`h6Ry#tbX;9cE#NjhKH5T0 zkA5hed_^BZRaUeZk`jO_@`jJF;}ac&p3+|S)WwgGCuI?twcAit5X5`Xmg2b z`mX*_Q5(jw+fF7QCNGlcSIk#S5j;0?INDhXt#ni?wBH!7;5+>~ z<`Gm7c-oUQrz2c}CksMGql9>PW@u*k<-*YWUvMgc-PkB-FCA)WQan;Nlk)!^vgRm` z@QQ?d)B6|7LgYL48Cv_K=aGh(pZ8rTQ^GARFu2^jU;yzSn;k{diTKM5$AaY_;eEd+ zcIM4E$QTnGha!^Bl-^4HRZ-$UWO%M|`uDj2lv%|B##_P=zQ>NT4KbfAJzbD?R-<>J zlfe9-A3u(JqsGSOJA=*Ny=?ypv6&9PKYG0QFBWiznDMo7*;kcAFzAnC8odG}Iixng0dq(2;K$>qoh^_diN*Z-Li65fVb+<~451nJBHAbeK zn_E*{93F7iPBNIEwoeFRg+DGJGfxrd3wo%jI`^N;Aw(XN&v zNe0Hb?biqTv%fLMWxeLs<$PiL= z%xl=)ETSx`%$gvQ@9S`sSvSrY_*e<|;B?JMMeDn@hrGWzLHLUHR7pqY%GPumLsYVU zWCDzuKdE%__rpODhlhiEN)_W2fde_`ows1h+U9NcJL1sVEA2IeQZ@0EBn=Stu8TR& zQp_%@@uo7trNO%X0O#%P;+$a~Cy3R$@T1>nRnq5-MxKOSwT2231BC$zw`t^7pPodm znViYZ^BS&+HA8jDr`R3y6Vm$+8t>1WJufc{=9xvKxQ|;B4|KaewOo6pPOv*#BJz%x z%C7~J`eqSdzvU%o`7!kO%^+7}j(x|?)E`07!~0Jxj0E?Fgpa60)4B4PjNnUS(I$0A zQ+`mB&DmZtLxGuQnt@gfP0DM!>-~9VD%#u0_}JOh2Aws;N*g8T`^UIi&a5y()KpYykz0zs(ylxvY$p9$&1PyF#{l#wl~ zGv@`-HAn=$`^UwvdyGncT3{m zlnO4-)1$Bx9EBjJm~9FOpw|s(PTZP6+@^B`g43CBHngx0ddX2Ba(y$RIWjWw6t^ml zML`xQhL&~_wquqiy&$omtFaK}3$qm{qa_(!fB?gX`7b!R@^f211j6XvR@oF&-KNUb zy-HgQw=r;(tnm&_)pP&PxlQBclhu05RzuGIztv*n@{bJ!dwfnqvLuB7vOv{MUlu*hy|Z3O!SHVBtxO8 z-ClcjLlfLxL5mL%frw%4LiW3sgI`l2gZy}4eZB59!;68YxVLJ}4LXvJ?tpkzLfl+w zE_P*99{6Ic(yJX!t@#P*%=c;(uOCikfzw}1;d`-oF-t4jR9Y>2DWtl9=?pUb+fzl5 z?UJ5LQa2MV=meM(Lh+eePzC-cL?>$xcBrMv zUVf7tlFe0|1!E(6do~h@B*aD0SNr`+Dx|+zldEE517Jg3;!q%5>@u>oJ!LLuCfYyP zQ24p5J2L~YT1KX@3jBZP=QX8e9HgrnlaihbFh->(VK)$j?Qe}%7!=_vyYNgJ7q;WZ z=Y#awAJi}^1aln8&NUaW1=pFdV5jU7PVdr&ZXZ~P*hR0;ohl!!tgDf8`t~hkyrIOd z@le3zq0QYdwh&oDwAyn|;G`%a9wVqr_lw=U<--=LO*e^~3d2Vw^!zQ=_da2sfLV$a zLuR>No@da|ZqssY&!&}U@R5MXoIoq-{xQq z$El(nm3o6pzwVp}!$BbB`*$-Zm`8TblTmps5Jlq-B!Q|DAO~|xJ-y&BHLg=H z;J5)wGd^t#dM|q1vp?RH*&kPf!)rJYPw@mHwmE#${FCjQ|GfF4FPtrh2dj~_1BRDb z_v|+U?+89(SgqwsLd`={r-jhq!GS|Wu(fvJ{UtUQF_h zy8`dEtuWH~+tmAHi$U}2{Q$sfmY0*~_dkUb`o0Y?jSaJ}^Sqf~kWs|@{T7m`TSfF@ z7i`lvo*W!xqvrT4^j-*WcvOHFuhberG$iF~duwYGc&o}t6AjVY!KR|LRD}EUU6qa! z3FZ($CesmI=eeYz=fG(@S|Jt-bfBSJ)!KMDgL8HCRSm=nh6>4a-JE zSco0ol2eIEi3Np)`v?0YBEcitwst@MnxDrxFlyB@z=>#{O?~*Hp>eKeZf0IkN!3RB z6s1C_0%x>-ccCvzmTEeX@sT1du+b-+9n91wWKH5Y&uwQ)=PBm~eReuIyB@FjtavQD zeQKo=gDKcXHgYhQjwv!);zqQ7+uu)zDT9v_^ZwDs_SEZ!=q=O&8=Z!iCa=o}0EM&a z=aR}x<`EGb?d)2*!H$QjBMbB&=pZdM51ju_`-Qjn(z{S$H@AnPM&JLUv)KH~@Z$7LOxIgW$|6FGoOlJr)m!Tb zV6z1*GT9qb>_I4#x^+>s_%rsf{ft|3Hd>I3C7pfX+m1c{w#rM;M+<3pf zPgYp-9@m&nH}s8;Ld3}*3BHoLTh5QVTd@Ex77{$}A+c1>+`Xh6uleoy?NoA${=f`YKW-R69zps0)~X6oXAuSXm@ z7U9L&uVm1{sQIJ@V_ePD$jHNK=u$;x>ATR5i9L3Ddb;%5}Nq5i<>z3=vE!vnr>`D5d3@c9~beR4zfJcc?DEbh>4i`n>@Qh3HcSO9nSCJ zWGaj&KCm&g4h}lpZMWKZ0XRc59^p-=>CnfhJl4IcWL|f*OvUS|LQWeoejv3(KDvNA zovk)^+wFhDa~PA-Ktzle6(7Br*K8_uAbuj>?RJc$-Szz1ab!su$m&d3D?w%sm%9K!CD8Re01lUtvB} zifPDn(uuP7L)-Y4Ao05MrjI>SYtJB|3Od-Qu*w=jd=Xh&dwomd&I_9U1$#cuTFuy` zwhmR+P25bHZ9LotfPlk(C*6*CsT}kl-xtKW+?lMdcTqf3c4x}e+GU!qa@`LZKvDA7 zo5}T7thBUqU0t^GIjtLgoJ(<4%~h|p(K`1FRjN0SvTu78;MSxL$I~bl&$eT)XqvP! zMfe+S4mR_rEPhl0rJY1WWTJsMO6i~|GE_?|{PkOXD?Gp$LYQA_TnzO0Pt!}pbfNzN zh+@Cb+?T|Q<&Q=tc{j1V>x6$ zOeRWTD^h8UXH=fIJw5T-3}X>0$Wv+<(Z1NaA@3S;uvp47tiCXO$v{ffV<>djo|AW@ zOw3k;2I*eSbiY5j1*F*GEjVd8JCmg!0TfuR?!3EiLUf(2GE;9K!Wj$7kgIk;X-wd( ztp$(=N#z2;PC;u%1_s@S&}PG*1}h$tDn&?5*lZ5#E!l7v0<+hqYy8{JEAq?n_H!2` ze55H1vI@z0>nmAv(Q87zyEBEaY`W9Pv-$2%wzw+qj)DNx0)V@*;{!F^t`{s@NpiW(v$&(*6063>Yts~7FF5Uw# zT^C_@Ao7VgFz2H!4smQ$q5IE|qpXPKPL4O{(6^YFk2?)1N2oveT)eN*@gE`$_z3yO zB^8MOiv`$AUrH@i*vqW>Jak91d$#jkt!re;`(p)AH`#4~8mMcJ08g5$)mQA-PLK$T z*#`rdl_-qPB9>N*6$Voged2Mz7 z?DnL2Y?PUhaNnxjIpU_L6Hy@1YC_b~@xV_I@#CCQ}90yo#Ma|B3Wu{x1gW zU0x~>Rru2Ukhh`HPDl1qDT3wXnkF~$_7T2u0t3)-2<~Y05f2(F!lPR&bCA{dW@sBc z&&Rh9sSgXKiAI~YmFEeLom3x0=0iGNoD4USl8JZoJ|?4%{Za|`ZylJVKg43O^!CxI zu`}Pb)jQ5g|8c$H*eoL4_2Um&`nb}}ENfnmAro&yf~m9T*n1LFD?Ick->r!LiZqwB zu<>zIv0y-_!R}x=TtVYu#uuJxElRu_R-hayhj4#&!*sh>A{AgWzIiV1uWu{Vj_$DF zV8F|yH2Re~E9E&!mD_GU2zG6xhb0Z8poA7^)N|pxDY;e*X%|bQ=wT{%RF|2(?<3-H zdCN*x`}$#Oc() zsCh97sroBe0vkY=*Li0gY;3;#`nskp#$n*+&@6;CS#kSCxoBV4@e)4)<<2yX(!Yhy zF$sG&i8xA3mfB+xuQr(VZ81-EUTK`=s+7m>MBaQ70xL#m8@UBJI+WXqP$qyh+_;_0 zLZ=SY-q!X~AtXL;u|RN$vH2c57~o#eJH1V$xzrD#E*B>S9Ot!6MudD0u#8e!U(CdUjAA{&#Jx3piKi+4! zlXk2W0~W1M#heGUO4loE)5W9a|J&!>l$(pI+jAbv;0+?kT%A?;PB!;MuEKb!it}cD zJ&z8aq&s5bWEHw%9{>MAObzlG9$rl8Cm&(VI9 zN%OPh5QlEYal1zQJ#vE^rblnzm|(FMR;(NtjZjkTTQrq2LKC*z`OU!M0yn%A6UvBi zX9EGd;HS%dGxG|xin21H`(2UYLLgg%m*PW42>jBagqw0rxro)j-kpQ1mKp&bf|U4o zukRhyJ`N*bA(&n%ecW2D(CuT;&`78gK`dW#zixHpXH+Y*an&!DAjU>XIT#aVku26i zvsjRCtTwlim`x+6nya$#F&3Qelv3kGt;4-va0j~rwB+8Q+VB-59-WS(->6-YaQtMC6NNJ`R}a{Itu}X zv~9Y=kZWBQGczmL8$Z!#c8)QmaKsCc#TBB|th3=2!Nq@feOgS*e{)vfPxor;LbdG2 z!j=vrmaq?k$Tf6$WUL#;+Y)gg*C-@~%ke>-}f^wi-e7VvF9mRm;`< z7{OtR?Q4*5y{EUhKV8uO*`xx**eO=Vu{8|_J>fO z(qd!1-P@@;r)%{InkWoZUYfcz9zQq}$Gq{(VZ|R*p(GZjrol1f`Qhq7dgy%X>FhU& zit2zn2b4p|BsKc=TYjtV4ZE`smVH^qo03HZDL~r$oZ*nrX zH{QSuf4$K==EZZVgm;O6*siH$zl%Nd@4t-AOPr)1+D0W6IuQrNzXBZVLv^NU^ZgE5 z8XsZbxwm*alqV}}k{(|N0oi0OFSiO!XeTP@(QmNnIMax}SrZ=l8Hl(Pb{@y;gGk)2Jj;17nKr&-zllL88jl z*6HI^);-<*0x*(N3RFFN?1CV>M%y}^g{rukOGQ%O1~&VbtMiUX(TD@i7L*}GWQTc5 z=_Vjh=M+EPyZCg96yjmYLua^)Jz}5`ijJPdPxozvg4c1J<(bq-|N0HU#}-E3XY;X( z42}Tgti@uJ3b@j79QMfpAVjbu9qtD>@tgOu*fy*G;(6S!Nt=D>>B-Hh(l;IC;Ico# zZKyr@nFpZmy!`K&CPrIVqi{Nnw_YLoFn+_Q4}f90-et8{QU(`n^7^u~q1eFuQ*(?R zrUZ7&WNPg7xU)k?AhRngoQgszP=+v>PSVM}{4NnGq5p*ZbdfhXvCB(nYQF51sN%Z) zETJ~TxjlE5?Qu0hn1Ml>x@N_h zzL1XIJ_I{*D8JFaHZmf?RK;izX^lmv_%Zqbd`ZOf(2-dEiS{H?!ZRJ@1d0e2X5#K@ z`KdQc*FO+w1N)wSTn=E(wcU*&H8P;FUjq3%`oDmzl(@D#hr>|?2f!O|wbE%RNnFn` zvV@8%VvXSi!IBc7G0nd*vQM~^o_E-Tw~~pTFPK*qD>4j`kZ%7Q@_YWMRlXgcx;A!f zL>5o+FW@$rB+`i`EDy(C1t77z)i)Et18dXwQ|KM{OmhUEcNLVpjfOaFOR+Uw9+;tg zyLd(SY0BcYRjU}@3heA9&(G5Y5zfkJ zPk*Vt594wK0KfBPwVS5#T&;!H74ZRniA+S}lG@|b_;(Ah7@zFFkUkJ^M9~aNhn&nO zUrK_%Bx&E0vYKkj@Gwl|IhhV&-u`7`SK*l^P?`Q;UI2nlAe;SPnm&)Yyq+c#Qq03` zaDl0Z{SSkLA(#7OzXG!V4$Az$X&w#zN5w$oy+vV9{>$^FJN=5}Gg>thM2WVOD+ z2dap_)BP)MyFud(`SR3>C)Db=B^wf1avuQ6=`xR z9-yF~`fa&`_C`2WASO3liXhS&fF5S33evvLrI=tX2fteNaY1 zf`aaLvQq%y2uY*HYGtL}cWWR42Oa&WkFgm5NcZP!PcN6;apGw<78}7E?cr~a(c`c^ zJw4^XiQJA*h;X1RF4nUe2XjyGNii|L%Txr`*4Cw^rR1U^Lc-SFVWiU1(uEq;#Gj4Op##IiA@mkP(q8qE;5-yK(Lc57TrhJY*<>L{L(BR`37K)Psz9I`e1CZ2$F(S<$n)02`GfR;pW z19v%hcXtAUgf`Ffow2UIJ|w}qni^GARSw|d<|ZJZLJTc#IFU$TGe;-do5=U&J3c<9 z%+j4I()tHc0s$73Sq`l5yv2Z-J2v@rXbi#>pbB z+tEx(KsneraG8^-nWH2s-5(!V3-X_P9ux_eBaJ;`PSX8$QN01Tx=OjY6BkEnyU z|HT4y<|c_A+~z_}cpjY-4Dnp$KVlh3WXeCosQ1|=@&QT;Uu7s^+o=B3Rhe2OCneE7 z%jrQu^A9JVj4VTa{b#L(bTI%mCi&^lKLT}Dez2V_ zH$qHe2`OuCeJD8K%RO=~FRhZNi9!(1l@6hS18e-7yC#(_N8EE#^&`FUMr)hl`1nCt zvxWQ9>RpgZDLMxyVKwwdM_HIz$`AGd)V&aJ%KxdV?N*?X!v*Sal=C7(F4)-ECMgPe z#4f!(j>IYWZ8kSIfwE8_h5(=>Kkttpcdnm>lvDGaNKx0qK;L-9NY7GNxApT?hTeOZ z-^P1h(b3UjJ`$PCj|_5FL*Ji{48%7L9PZfC3ve&dEuZ~Zb-!T)MHI~Fd^2|ZaaZ3E zH@`d|Ea|$v6v*<35ryb~MeMR_%|+@iMpOCFu#V$uH(ZQbI_+ z23n-~IKq(u2vo;4tjdlaoA^;*cBNA3w-;1AsQy zlFG^sm~5F8R76C4HZ$Ct!`aF`z@$b2z2-X2d^dm<L#Ilx@&<;bOXR+>�Y6@2y(X6~!pX%|q+UtG%d3?skMN{o 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≫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%0D%J$3+!5i?-;d+vGo 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
  • 8dRux`m0KEPnTO%+B2L=U`%eTV9a>Q_Pp41Eki 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

    |bi|WbzB#Sd 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

      XLTR%e$WMYg=0$sr!>m&VijFel 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 RtbpI3JW*0ttbmvY{~w@k1l#}s literal 0 HcmV?d00001 diff --git a/docs/my-website/img/mcp_openapi_tools_loaded.png b/docs/my-website/img/mcp_openapi_tools_loaded.png new file mode 100644 index 0000000000000000000000000000000000000000..bb9f6be27193d29e3dba048e46317922319c185d GIT binary patch literal 105089 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 Pe;z3-sLR(t%!B?9o List[MCPTool]: + """Apply admin-configured display name/description overrides to tools. + + Overrides are keyed by the unprefixed tool name, same convention as + allowed_tools configuration. + """ + display_name_map = mcp_server.tool_name_to_display_name or {} + description_map = mcp_server.tool_name_to_description or {} + if not display_name_map and not description_map: + return tools + + for tool in tools: + unprefixed, _ = split_server_prefix_from_name(tool.name) + lookup_key = unprefixed or tool.name + if lookup_key in display_name_map: + tool.name = display_name_map[lookup_key] + if lookup_key in description_map: + tool.description = description_map[lookup_key] + return tools + def _get_client_ip_from_context() -> Optional[str]: """ Extract client_ip from auth context. @@ -991,6 +1016,10 @@ if MCP_AVAILABLE: user_api_key_auth=user_api_key_auth, ) + # Apply display-name/description overrides last so that + # permission filtering always works against original names. + filtered_tools = apply_tool_overrides(filtered_tools, server) + verbose_logger.debug( f"Successfully fetched {len(tools)} tools from server {server.name}, {len(filtered_tools)} after filtering" ) @@ -1449,6 +1478,26 @@ if MCP_AVAILABLE: return managed_resource_templates + def _resolve_display_name_to_original( + name: str, + allowed_mcp_servers: List[MCPServer], + ) -> str: + """Translate a display-name override back to the original prefixed tool name. + + When a client received a customised display name from tools/list (e.g. + "Get Pet") it will call tools/call with that same string. We need to + reverse-map it to the original prefixed name (e.g. + "petstore_mcp-getPetById") before any routing or permission logic runs. + """ + for server in allowed_mcp_servers: + display_map = server.tool_name_to_display_name or {} + for unprefixed_name, display_name in display_map.items(): + if display_name == name: + return add_server_prefix_to_name( + unprefixed_name, get_server_prefix(server) + ) + return name + async def execute_mcp_tool( name: str, arguments: Dict[str, Any], @@ -1485,6 +1534,10 @@ if MCP_AVAILABLE: # Track resolved MCP server for both permission checks and dispatch mcp_server: Optional[MCPServer] = None + # If the client called with a display-name override (e.g. "Get Pet"), + # translate it back to the original prefixed name before any routing. + name = _resolve_display_name_to_original(name, allowed_mcp_servers) + # Remove prefix from tool name for logging and processing original_tool_name, server_name = split_server_prefix_from_name(name) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 9acc80b0ddd..9b07d44deb6 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1095,6 +1095,8 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): mcp_info: Optional[MCPInfo] = None mcp_access_groups: List[str] = Field(default_factory=list) allowed_tools: Optional[List[str]] = None + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None extra_headers: Optional[List[str]] = None static_headers: Optional[Dict[str, str]] = None # Stdio-specific fields @@ -1149,6 +1151,8 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): mcp_info: Optional[MCPInfo] = None mcp_access_groups: List[str] = Field(default_factory=list) allowed_tools: Optional[List[str]] = None + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None extra_headers: Optional[List[str]] = None static_headers: Optional[Dict[str, str]] = None # Stdio-specific fields @@ -1198,6 +1202,8 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): teams: List[Dict[str, Optional[str]]] = Field(default_factory=list) mcp_access_groups: List[str] = Field(default_factory=list) allowed_tools: List[str] = Field(default_factory=list) + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None extra_headers: List[str] = Field(default_factory=list) mcp_info: Optional[MCPInfo] = None static_headers: Optional[Dict[str, str]] = None diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 9e847993efc..5e1ba479298 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -288,6 +288,8 @@ model LiteLLM_MCPServerTable { mcp_info Json? @default("{}") mcp_access_groups String[] allowed_tools String[] @default([]) + tool_name_to_display_name Json? @default("{}") + tool_name_to_description Json? @default("{}") extra_headers String[] @default([]) static_headers Json? @default("{}") // Health check status diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index cabac6b9d51..7f6a8b3ea24 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -33,6 +33,8 @@ class MCPServer(BaseModel): ] = None # allow admin to specify which headers to forward from client to the MCP server allowed_tools: Optional[List[str]] = None disallowed_tools: Optional[List[str]] = None + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None allowed_params: Optional[ Dict[str, List[str]] ] = None # map of tool names to allowed parameter lists diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index 1fcdeb627d0..de2ec13b4a3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -28,6 +28,7 @@ def cleanup_mcp_global_state(): from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( global_mcp_server_manager, ) + # Clear before test global_mcp_server_manager.registry.clear() global_mcp_server_manager.tool_name_to_mcp_server_name_mapping.clear() @@ -1788,6 +1789,79 @@ def test_filter_tools_by_allowed_tools(): assert filtered_tools[1].name == "my_api_mcp-findpetsbystatus" +def test_apply_tool_overrides(): + """Test that apply_tool_overrides applies custom display names and descriptions.""" + from mcp.types import Tool + + from litellm.proxy._experimental.mcp_server.server import apply_tool_overrides + from litellm.types.mcp import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + mcp_server = MCPServer( + server_id="my_api_mcp", + name="my_api_mcp", + transport=MCPTransport.http, + tool_name_to_display_name={"getpetbyid": "Get Pet"}, + tool_name_to_description={"getpetbyid": "Custom description for get pet"}, + ) + tools = [ + Tool( + name="my_api_mcp-getpetbyid", + title=None, + description="Original description", + inputSchema={"type": "object", "properties": {}}, + outputSchema=None, + annotations=None, + ), + Tool( + name="my_api_mcp-findpetsbystatus", + title=None, + description="Finds Pets by status", + inputSchema={"type": "object", "properties": {}}, + outputSchema=None, + annotations=None, + ), + ] + + result = apply_tool_overrides(tools, mcp_server) + + # First tool should have overridden name and description + assert result[0].name == "Get Pet" + assert result[0].description == "Custom description for get pet" + # Second tool should be unchanged + assert result[1].name == "my_api_mcp-findpetsbystatus" + assert result[1].description == "Finds Pets by status" + + +def test_apply_tool_overrides_no_overrides(): + """Test that apply_tool_overrides returns tools unchanged when no overrides are set.""" + from mcp.types import Tool + + from litellm.proxy._experimental.mcp_server.server import apply_tool_overrides + from litellm.types.mcp import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + mcp_server = MCPServer( + server_id="my_api_mcp", + name="my_api_mcp", + transport=MCPTransport.http, + ) + tools = [ + Tool( + name="my_api_mcp-getpetbyid", + title=None, + description="Original description", + inputSchema={"type": "object", "properties": {}}, + outputSchema=None, + annotations=None, + ), + ] + + result = apply_tool_overrides(tools, mcp_server) + assert result[0].name == "my_api_mcp-getpetbyid" + assert result[0].description == "Original description" + + def _make_db_mcp_server(server_id: str, updated_at: datetime) -> LiteLLM_MCPServerTable: return LiteLLM_MCPServerTable( server_id=server_id, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 0062ca5db4e..6dbb18887da 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -54,6 +54,8 @@ const CreateMCPServer: React.FC = ({ const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false); const [tools, setTools] = useState([]); const [allowedTools, setAllowedTools] = useState([]); + const [toolNameToDisplayName, setToolNameToDisplayName] = useState>({}); + const [toolNameToDescription, setToolNameToDescription] = useState>({}); const [transportType, setTransportType] = useState(""); const [searchValue, setSearchValue] = useState(""); const [oauthAccessToken, setOauthAccessToken] = useState(null); @@ -353,6 +355,8 @@ const CreateMCPServer: React.FC = ({ mcp_access_groups: accessGroups, alias: restValues.alias, allowed_tools: allowedTools.length > 0 ? allowedTools : null, + tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null, + tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null, allow_all_keys: Boolean(allowAllKeysRaw), available_on_public_internet: Boolean(availableOnPublicInternetRaw), static_headers: staticHeaders, @@ -712,6 +716,10 @@ const CreateMCPServer: React.FC = ({ allowedTools={allowedTools} existingAllowedTools={null} onAllowedToolsChange={setAllowedTools} + toolNameToDisplayName={toolNameToDisplayName} + toolNameToDescription={toolNameToDescription} + onToolNameToDisplayNameChange={setToolNameToDisplayName} + onToolNameToDescriptionChange={setToolNameToDescription} />
  • ) : ( - filteredTools.map((tool, index) => ( -
    handleToolToggle(tool.name)} - > -
    - handleToolToggle(tool.name)} /> -
    -
    - {tool.name} - { + const isEnabled = allowedTools.includes(tool.name); + const isEditExpanded = expandedTools.has(tool.name); + return ( +
    + {/* Main tool row */} +
    handleToolToggle(tool.name)} + > +
    + handleToolToggle(tool.name)} /> +
    +
    + + {toolNameToDisplayName[tool.name] || tool.name} + + + {isEnabled ? "Enabled" : "Disabled"} + + {toolNameToDisplayName[tool.name] && ( + + Custom name + + )} +
    + {(toolNameToDescription[tool.name] || tool.description) && ( + + {toolNameToDescription[tool.name] || tool.description} + + )} + + {isEnabled ? "✓ Users can call this tool" : "✗ Users cannot call this tool"} + +
    + {/* Edit toggle button */} +
    - {tool.description && {tool.description}} - - {allowedTools.includes(tool.name) - ? "✓ Users can call this tool" - : "✗ Users cannot call this tool"} -
    + + {/* Inline edit section */} + {isEditExpanded && ( +
    e.stopPropagation()} + > +
    + + Display Name + + handleDisplayNameChange(tool.name, e.target.value)} + /> + + Override how this tool's name appears to users. Leave blank to use original. + +
    +
    + + Description + + handleDescriptionChange(tool.name, e.target.value)} + rows={2} + /> + + Override the tool description shown to users. Leave blank to use original. + +
    +
    + )}
    -
    - )) + ); + }) )}
    diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 1fa447c0e67..8a08f13e22a 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -169,6 +169,8 @@ export interface MCPServer { teams?: Team[]; mcp_access_groups?: string[]; allowed_tools?: string[]; + tool_name_to_display_name?: Record; + tool_name_to_description?: Record; allow_all_keys?: boolean; available_on_public_internet?: boolean; From b6c2028294945585fc0d2f26d71501a9e903c666 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 4 Mar 2026 18:03:54 -0800 Subject: [PATCH 160/171] chore for release notes --- docs/my-website/release_notes/v1.81.14.md | 2 +- docs/my-website/release_notes/v1.82.0.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/my-website/release_notes/v1.81.14.md b/docs/my-website/release_notes/v1.81.14.md index 3a133f092ae..7a6e79f1b77 100644 --- a/docs/my-website/release_notes/v1.81.14.md +++ b/docs/my-website/release_notes/v1.81.14.md @@ -1,5 +1,5 @@ --- -title: "[Preview] v1.81.14 - New Gateway Level Guardrails & Compliance Playground" +title: "v1.81.14 - New Gateway Level Guardrails & Compliance Playground" slug: "v1-81-14" date: 2026-02-21T00:00:00 authors: diff --git a/docs/my-website/release_notes/v1.82.0.md b/docs/my-website/release_notes/v1.82.0.md index beb2451dd5c..c1eb00709f2 100644 --- a/docs/my-website/release_notes/v1.82.0.md +++ b/docs/my-website/release_notes/v1.82.0.md @@ -1,5 +1,5 @@ --- -title: "v1.82.0 - Realtime Guardrails, Projects Management, and 10+ Performance Optimizations" +title: "[Preview] v1.82.0 - Realtime Guardrails, Projects Management, and 10+ Performance Optimizations" slug: "v1-82-0" date: 2026-02-28T00:00:00 authors: From c60ea1878d2ab1fa2fd45584da4d3d5051481aa6 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 4 Mar 2026 18:15:45 -0800 Subject: [PATCH 161/171] chore --- docs/my-website/release_notes/v1.81.14.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/release_notes/v1.81.14.md b/docs/my-website/release_notes/v1.81.14.md index 3a133f092ae..0129b4cab86 100644 --- a/docs/my-website/release_notes/v1.81.14.md +++ b/docs/my-website/release_notes/v1.81.14.md @@ -34,7 +34,7 @@ ghcr.io/berriai/litellm:main-v1.81.14.rc.1 ``` showLineNumbers title="pip install litellm" -pip install litellm==1.81.14 +pip install litellm==1.81.14-stable ``` From 1c46495c01cab449aab265ca663adff429524d17 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 4 Mar 2026 18:24:04 -0800 Subject: [PATCH 162/171] new update --- docs/my-website/release_notes/v1.81.14.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/release_notes/v1.81.14.md b/docs/my-website/release_notes/v1.81.14.md index 0129b4cab86..20836d73b19 100644 --- a/docs/my-website/release_notes/v1.81.14.md +++ b/docs/my-website/release_notes/v1.81.14.md @@ -27,7 +27,7 @@ import Image from '@theme/IdealImage'; docker run \ -e STORE_MODEL_IN_DB=True \ -p 4000:4000 \ -ghcr.io/berriai/litellm:main-v1.81.14.rc.1 +ghcr.io/berriai/litellm:main-v1.81.14-stable ``` From 5bd692e649a82038c36c4772eb4b13effa06f4f9 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 4 Mar 2026 18:27:40 -0800 Subject: [PATCH 163/171] doc change --- docs/my-website/release_notes/v1.81.14.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/release_notes/v1.81.14.md b/docs/my-website/release_notes/v1.81.14.md index 1dcfa071d04..c342bc47ee9 100644 --- a/docs/my-website/release_notes/v1.81.14.md +++ b/docs/my-website/release_notes/v1.81.14.md @@ -34,7 +34,7 @@ ghcr.io/berriai/litellm:main-v1.81.14-stable ``` showLineNumbers title="pip install litellm" -pip install litellm==1.81.14-stable +pip install litellm==1.81.14 ``` From 38ea5aba801e03af36e45d833177f4d74522352f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 18:51:02 -0800 Subject: [PATCH 164/171] Delete ttft-logs-screenshot.png --- ttft-logs-screenshot.png | Bin 222088 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ttft-logs-screenshot.png diff --git a/ttft-logs-screenshot.png b/ttft-logs-screenshot.png deleted file mode 100644 index f07ad3b030862ad957dbf72f2a942385f3969295..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222088 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

    41F^<&4%Cs2_o;_1RGX%I)nwe zkmfOqdIrwesZWs{m@*c~S8!ak1k=-BRkZk{_5Kyd>ZvwiLQ-V06u1#7fV@;(6iO

    4aZCI(QqRn)_ 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 z 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⪻9Zzf0!nE6Wq(;qFdNmWLHytqBC=1 zDigNO8#6?Gth@%k`=q=O8%yET=p4>BRY+y%vlpLgd_tXxu7-X*{O|0dyrLr5^m%L> z9oPc@(po|?>S;eK{`<_~#mAw$PfLaDw+CaPKzZ`POiBN3vZkPxMF)yr9Geh+;<7LvK?WWR-gS_HsdlmNriLXohC zeSJtsz6jqtQqlkDD^#lms>rur5awrRfwb0;MKf`(aMtB>sdl**7$*nGt2OVF z1}hc{8o-{u;cX!TQD&fPS6Q6YJ=yqP*>Xa#(&7h-LU;S#|Y}n{N85=u?ofU|hW+f-s`%H)-13{CB9cRVq}BZP_$UvaU|;>; zmk~IC>HZoIt|fR^=Nr<8>qmZ;lt@NI)X#BBq3hD&Suikk(ON7`??0VUe>>Dv@A&9n z)zIHcgih_H6_4D9s{ddvEmvs)XT_(dm*sWKVan=bdcQZOTohS7$G+ca z%bJ`(!e}t{p;hls#!$3_YP75>8O8j|b@&z4Sfd$7eVv~xsPdU*jhsJNNogziF zO-E~W?1JBD&^%7&s*wb<|1<=|hUA^SI(6|QvDyB{cY4zo{@EC);tonPE~I{LC@ z(_fyq!Kr7*;{Z~y@ToQ1%pis)evZ@gRb^)>p?30JkL&Xb$J$wSz?%KKDb2WFC_iTh za9ljiwh0<{=sKqyYktpgfB6K01-*w&L+19!bv_V2<2t_m5{gbAZC9~Mpsf0vx$er~8O+@5S?{~`VEbfoj@v`za>2e1RBU9=_d`>s7d zs#?RodiajtaI)L}&QE1pn>o!<*R4$DdR)rt((C5a9T^F6E%Yh1HgYt=8mY4{N7^Axr3c^c)r z3)9o_Q8mlgvVx1;1XACzy2peApHJm>x~zdGIu2DUmOYj9tWEcRP_w+0xn#RY|C-k| z5SR{|uY`+4NbPlQxcDQVmgw#Btb_BVQB{IauWeMEeP3>ZkNi;NKgS$!BXDccEB!OW zMU^UJX08d?=ehAa?%m%(>aLcu3yma!FhRUa=?LXx;~p4b-TsV_jyv0GEd+grhYDY2 zqgO`~+m=|a#Yg>xdRongsUrI3Z?9LoL3(bo-L>*b?YCk(P=tvBZ$?hL0-ZQBq-Ra4 zug^1m(fq27j_mr&RvTnUkLjcsdtS2H{st>74EO4&zYZboJX6@q=l-*I0*Nr) z)!akSyjm^&`Al6q>M9;P_I#CC1UTrYF7sJk_QKbApV)E&$RTMhs~x@YyWgiek{gAH z$RwcVCb3FoiLu?0B1RQBK>zP^uyC(>g$-A>c9kMYf~zc2@sSK#Xl@{jW^-%SSgFUh zt>dtELNjlwb$hE^_dYnri9uU_JHy_>+M>$MY^IO@!cE&n_5hCY@}TpL{yAtTk3KarCYCbn~d0yFX@EST5NTK_9W6HRtv-Bse*oK zppVC9Gn{eOt2TE(c7S-Hzw(~uzDBwo4zxhSa{5u+dlqbAQ`cO`o5MA3q!BgSXzzih zI25AOZLuxFLKOCKLij74-)lLu&qC(sr>gI>WnmSR9qD^3 z*?HrHT`iHCp>p$(tT#Td3(Opu^o~qrU2jE$$tV_)sb^9TSwY*K3nG&Mx#Q*vJlQ}DhInpG6 z)A*R@NAe-z?ANccpYEUZ0xAj%pDWyqKesU&T!4({QCjuR3aWY*L0&<%8nkO33kkm6 z{I!?y6SChPyf(MwWhv$M3*|6N*eUfr^KPMh_?57Of<8jA1BZc#&h?`5jkS-c8PL*E z&5`a3YS~!NDU}z%QcLq=8*6=e1O05#iAC1$v=ts}1g1NabgrLz1~7C;WbzCf)&j?7 zvd;=4qj_;AOK6?0Xn*}F6$iWRqDUKw`k)&-VC2GG@@U^X%=l%-`u9#iBev!g+-moo zL{%bH3{8EhH_#I5bmcoISKDm%_kb#~@p99Ie8KA4-g!ZrE$>5TpkpFq{?h`5GBizB z=J;3d%r!8Zu1Nyu02LSs!jXOej17P_xiM3z*o4A>)|VJ9)Hx&v)@omXc|b~oc%^wm7H)PszHudF*>uabo&RP9gK*+h7VwO^pCuTPZ5;=j!@|O1NyS~=+@4cYWGJf9{=VZq zXu)U&O4O(>t=0+2$yhizB??sMtcxQZ?VQ1V(r=Jo;xkleO3T%nWezQ@;_lrp2?Kg8 z%H5w<K?jhtYWU6EmJq(MlMK2HxLl1 z*`Wb@gJ8~7#HRzhuO{wqitEpPU&)n3XNzAsN>rqh#|wdf3fF>rpO|kA zK=7NK@2fU`JzlKNnpd%^7K=p}-}AV7&;wrfP<1>!TQ_$NZ`M&wrc{0>p;Bcb<1^ zG)DO(u@#kixcCw+AI%%n6zz#!ue8rR$q#{3@l>*53hpzy;KnDq?QL-XP8Vf zd8C++$j&=K>)4PGF*)3StqN3stqO_pP8h8LUY!$bUpGxq`%mphap>jXPi}jDvTGvd zHQ$IMMO`D*w!d(jB-gfHouD%N?x#|3#%`P-scQ`&LCeB!5oThj)j~;Yi!vAzbs;!Q=-DC9XBQtm!;Y5ij(7}w|0rn2YjWa9j( zK$5gb;94TW;<;It-ne+S#y!p|w`$tx{*L^{>iglRw;rpfzlT!kaN^a;WB;_&O8LcM ziLo{-5m`0DG08#`)GVEAS&!9T!yZpukEp4um8}95Ke&HONaX53S-VsU}jm~e$VioYP@ZSmdX^^qd*hvJ`7~+>tqGVTG_qKneaC~b^hJAT) zzS@&vZAiKOxBlM7Jp=v6rXJ9~vUlTm5=|tgjdr$CvmPh^t-y2Wof%XPkweGcDxp#k zn*{Xi6z0?H)k^A1_yF>x{=sRF%Aw1le%+u&U9|<2w3V1w>1$a`k!eOiV~?9k#*wI z|J=Au&o373Y#Q7Aj!4US1G<WCzQ- zHm4c3rm}b~UYuo>E~F`G8QKnMVkOwzc1@afl3eH(bdhN=91Up#MM&n0Rb|H@^{v#( z5{5z@QlDzmGIx4>`P9 zetqO^zZsNcTY~t)A0BuAs+JtNJf&z!34oLJ85% z24lGSU?x@|#*M6qY{G!0KX70iPf(XQ-Yw12X|aJ|oQ_YG1T)l-TTWt=vPNxQ-EL%A zSyLVD7>kM5TgzDj9AN53u;SKM_iLkyQk-&JE{g^ONlut7ka-DX^icSg2qE4xOg7ra z=fyAxSyD zw?vWTIdLOB5E_1lIg);Rf$r%Q`%Zv)Z{o^3vE-`B&plPC}KsB`UPr zg}FxaM#TIdcyG<_YLSYC>S&fR>5tDQpgh62$8}`j$Eo9I!0-gr|DvQmZ%FSju?)+r z=0%q&T{@v`8Ii~*WzkU6M@NPN=IrYVay>f@s$bpRB#Qx)eujoQfs|pTnK63Es6>j? zDZ<~{o&_?@m4;KRzSxmKZFUK`!v&Iw58!D<2-<3EE9jZjYi=Mw9^B_tn3cgGRw?uw z)oNwbIz^*@{^MZ6n6UB;Q!6*G@|CBlM|n5Mdqt{LZd@{pgM--gX+uUOYLwlRL$Wtc z+ANDO8&0Fqj&`IPVkZ#lDry{?*HWne*jTjr!8Fh$201iDON<7u8BNuTD~1RmOVe&W zO&0%mrxgE!1&J1f2_%+3v1Q=&Ojg#yDE=Uu4F#eVYr%j0;#mK4 zch0mG?8KTUtzKCo9dIm?PmS@-U9_qvr=(1mDHOE*bD^dbmZdSRqCx_Lm;?#oYJ7@1 z@ROux0xzFbG!uveS)GDyU4naiWTP2=m=t=#Qe60m-pb0}0eTeirE|J6Ml+o3KUbQ1 z7N`B4t)$`>xgDhP{4$c06SK6qeAO&9A#iN*hj_x=>cuiSDqV=x$g+_lAxX({X=_*P zO`av@PD%Skm7DV@N8^=LT_Rl>`%){rlkaGma}*UUT5FNYm)c}m+8dY3rWkAn%E?A8T0zEn=@?5 zCA?h0W1&9uZO5#}@%Z2;BQe;HGc#_Kfd26-l(yPL(0;G@$l@pnO2zbhS{E9u;ckx7 zu*S*pF8;u%iO(nMG$Do^bD9Q?rtdR8j^EXP;szShT)IVRYk;6$==1+6Pffo$w+=wI z``xue)#Q?Z0s=nNL}Nk!P}R~(1IV-AW(uJ|qugw8bFv(}qaUuA1~CPVLW1h>JcpAN_gmkuns7MVyqFy9fO`0hFZp-qT&3yDaF6ra zoBntyY+rhPyZG1&ON*18;7@h0F2(4%_s~l@d`>FGDtU)S z3G6xqB`NTjt+jVg*3V*n6qborOiEZ4lV87P+Af=X z?dLC4wd5HS@i?e^aM?}AA(28qb%+^f1gKE5<-cAhTbqv4h$P-B{m{z&C0C~q-Z zU8{gCF)~{=VZkN~x(wheyUyqgGs!Y>*!zWKk})#+1^+Oituwg73=hfyni&$s!igvN zTVjRPq`~emUeh9xhVQlljNP0l)P;IQ42u|Cu=vNm`tpuIxcPJ6l&NPYj9{sLqBjGW zaKh%ncd@KlKrvVx%rfUeEMRz~&umr2b8o?GMr-+XBY|bHl#Pb}pF3|tSI&w*U}(61 zdPt@Bl39$aacu2DK^ueX?%rha70i??)2wqJ>X+3{+WFkNIPvxvbIz1HYQ&*^E!(in z6TCV;jgn)75-MVZ%>azOCk|%TFs*-WY%y{PC*~*JN`paDtK5Nl6$J-przotM)uz(( zSqv4L1*niH(qMqLo(|X|A9tV8$h9i^!*TKdXHED2q8#N}d>&k!9mC#n_HPTj64FtG#2%yfbV4LWDZXMXp$pDUPw4*nhW`Iq(S@_^9@*1dw4^8p znLJv*Xe4RZTYGyLSxM<^LeX}n@p(L_+{gM@5jcpoE&d;nBY%7ri3oTsq45d<6Lanl z51i%+8qm*GWV+8QC~41ZxfJ@~~egoK!fv z;kJj5n&WTq=?1E&oKtcB6+Ht!UQJ6gy0mo7(B%DEG+J)qSJp@-A?GIUYX%RkeH5pW zQ-v}8`0+y=n(>H~bWLro@n{l!g>8#iak%p8hOGG-&oF4~x*>F@a&x65@K*#Z7qaUU_;l0;-IoFdTf%0NfwFTB6{`54JQ zh$)lg<+0J}Pq(Iw&fZoTJ;zbuM_@VtCMi$_<{)udP6=hYF2y?{+J>j^&);|FlZuhb z52u_6G+1%A7k5SRuY zbM;Ysgf}ayS%9SL5``8_oLA0daviw$gBX1{CckA38EseG!_PiGeQftXDuuOvyWUSF zgsCM2AoH01sHLU#{M>Svm-b(g0ucM7jbZqw(1fVGz{TGY$Q_TAmcF`5z$Ge0$>ex> z2l&h+ox~anMMfgr+}w!eOE-gPlAm1}&W@ZM?Gb`wKS6T&|w;j<4njkwnk8vZoR zHO4-Grag)tYU-%Z!dr=Z$QP5VTd1@A&E!8CJo+$lezZBSP&IUotb_!Sa_^>VI?u~~ z$>;qqV+TaA{`j~`HIwn6D~9tnYthXm$b6~-FRx}Xw7sABTJ**)$nBxww}c_JsIeRT zryfmNzWP%aoH1{nMO%~r(@yN$w+8@tB=>_a^m?{LS@#SO7rt)(k>gpt28zH)cby*5 z{#DwKV{nuysPl-&3S*ENr4#64iY;0`Vc$nDO z5|23eeHLt`Dm02ev2v<3=!#`?6k>dgX9{Jn_9pC-EgM%HbQX+|PYg6MDGOB^2p5bw z(oI=3Tgy(>3^(xur@7oU{;LH@K&jthxbYsObb38FJ|kw-`%m)n&jhcD7M`U|*4G=G zWtH`w-VU|Q`goJ9jbHpQN=X8GsBY+iIS3MvD>f`z-+&q73cLydqJ0q(LiuLnVnM_# zVkBmdFT6*cgM@lM20XToI~j7&f)=9(k^#LoR)$Bd=fPtAVh`B8;Yo zM@JV0gn~Z!xL>F9C4n6UdVRb(ipJ;m@7XPM`jM;y>3y5N3ncLYFT!~hv8~NZX=r6dU+vwj1Z=D>AhYYGjk9!8I`IBo3)iq zo(*$jrnEY0xh+DDhp%Z-BZS~c5+w4(`Vrp>G&Fbuu6OF8`(Bh1GAkn}kcP3pke=h_ zU)BEMmLB&4FncKl%~f5Ok8JGh6&lqf{QNIKKuVM#d%8UlrhFae zJ-dEsA%brFy0j^Z48rq$*h$90!I2Ob6Y~N1FpX;-w4dbBc2wDVczCFQK?9G3bf#vQF|k~4Ju^LRFU1u+ zSEl~QeJfHS;*akeFl+=EV*s9(<*A2)u#3sSaL&V92@uNHwF0S2p4Ck9XThghg7wg~ zl9ghd3n0A(ZIQs6OF8r}=pv-XfYz89t8U5^c)oU)yn9^EyEyM2@Ug1UqUZ~U1x`Vz zMo_Fg^b#UF70Gq87%l(jVEG&ckI>2BqcLR0u6Nf;A31Wa5dQSHN>aURJYn>G7&&SD z6D9Y&*6z8n_S?P(Ax|vQNYfR-LEKv~Vf#E^h&x|1wjQ&5?Php;IrAlUTxZBlj*gBN ze7zrvrzD2>*>Xsu@nz7D8VsxW!x<_R4JaW;r0M3-Iqu?S>bWK&?4kB^VQDCe{+EG#U5S2G{apw76hvz(dyF{xgRLjvoH z(?L;8hyMLaEW>p(2m!COtZbu{UZ)9p4~d?pj$2^UQ_$m@YPM$b@L(8^1g(z}j8@L~8x;x>-yREW@!tJMH$<-K`3=yYRiXl$e(`wpa{$Y)7L>Q~ zJjJp~6EV+`Dx(l^|2gN}L0L$5%W*w?lZ99oCU81Qz$a-<@z$Ms*E`wu$lmq$He9}V z@?`oj@9MTLf=6j^2K{iw8*;crOjszAi2*hUf0(RIb+MC?IWK`}(y#ixOgq#lpbg2Utau z^n00ZpC6AV1fI&PU{LpO7W9J^A$g(WwhoH2f>=ZZ65u(Sa~HB(qoXD0G;2PU0>Swg zg0zebnfH4-iYBW`5O8kwDYCG#>ZTK&biCYZOCUmXWS}MxM{pG@Sd;>EG}vTF_751& zQ=(Xg`qlk7dXfP)697LoZEC9ofIZY=kG}1rU0K2Lk?ePH*jf z_3JBsk&p#YBZ^GK#On?Xi?D(S(??0#wthUT)yBb{oq`?c+gmLP@XJVr_~^+`#OHlN zKP$EBVI$wML(kEOp!A3@ut}5IQ=EFTb8tXHK)_gbJ%C#Ku$?`a)-mE(yk^?X4-$T3H0Z(68?)SI7%y2Bm*X?+DaJ7G? z{fieE)uNQriBpyU1JJ2i2yCmDJfQvwPEQ7nrKD8bqqTg1g`mwL7KHX|` zpt8sGV?d@w1$jo+I0hblmFK;@`s{%!+Rr?()KiUcU6vSnF~E5TAsh^;K=lICE0ci7 z|Ksgr^_~>mdrGgRZGr`ZK^QcO_0=7rt~nGPijbI8R&z5`)2;xLkI-K93lefEij#`n zXk{50_+6o>9GP4W9|ZQ&<$NA?um&-NZko-?g_dAHKzhKyKvRZjcP*kfG_d}3<{|Ks z!#8?)yiqDaM;$Q@bb+h;d|V&(5c9igR;R43>A2o>Bxz@7r~T!2C1&S@kKa#jk9qSq z5P88t-A_R9TSwST#$SkXMTR9JcG_Hq(AMRVRfxu=yoVO0+0J{noWmIy=*43fvzckz zN+aV|i0GO&!yx;+Tu^?M=u8}+B$HCUBa0EDh3!rsa=TkU1p?LQKxJg~F4{k8TNZAQG!kn?+ zcTs$@W4?1XS z$~srvpaSW^UVR4Kll~cB1PA*1P>P5TaIX%mf@^A6i0Lg9C1f|qtgWpf)X^p&KC!P= zbT57q4SV{uOU!`((5(y!+lLlsChUpU9B{wMv+eXl(3SI~+Gk>KrsLQ8lwzNY-Y?z9 z`Y}U~nA_*rI$m=5UGf}w^780FQzg$~WgPI&< z_r54>b#=f51hcjAp#y8MN;nv?iyGsDV}4;__gb$yWKK$IDm5SA^bFiGLc1iEg@c3J zpg9A0g|o$q-L>!Bb6QmSOdy(19e3e0&1rt)CjQrtQRfUFzpeHC$>8RKTu8Wa}YO}a$Egl>Qukyw-asu zcG7_{3=Jf{6c*3PV0X@74$*V8D@5^F3hUs4y_Q@IRW6cyd6@9kt1*Ge7qztf3(u3z zxVrd<(WB!u#?Dlu&`(}KG?a}DkX<5yeFqbN=P?BMJ=gQS&%2-`D9OpkfPBH5maYp3 zT=4%Z7zOA0N1Qox=Dd0He*E!ANG3Q`kSLxigoc6hwq?r}$g}zL=R;ZH=MEYYAKe8Z z)EAER)mL8;r{%SD>C&FXlvgU17hQDG#*G`_d+$Bs98P)FRaXgB!G2t~ZXM19nUJr- zxJpR16)RQ&V9J~rW_T`sfq8?aq(2x+B?t%~w=k?cLFU%ptcXZ{IR|-xYiV3T~ z66f~<^-i3VEg?H`6d^+3nl)?CV&UQ8zx&N=B1Zj>atMpH6iJyEc9j5(1h_}ftijUnXcBJ zp=yvq84C1X5R%R`p=-F8u*oF||Nht$ghCmTRM#6~!nz$1&K;)>?tu4IQBl#|faB07 z?%nZ$2ksvT6=_b-g2)|3d ztWgQI<4cIYcEvnB8-(Ns1uwm>q5lKtW$DpQKlXmTnRE6>-@y`tm9$tntDV^UaZ>#mL=2ILiBdw;az-g7 zgNuV>ZG0OH$d&gL5fMoVeyh&W@EDA5@p~8#0edJE$`Ax%&lU4s7&|Jsf9Q+IC!Ln3;pQlmzUA}JKZhp7pSepJ61UZ)hC96)Qrx8`ByHuNtD{3Y$U@>Z0-=i` z*hA|5OhU$o5QScH&RJNF)VFYY6H&>CT*AkQmkfyFz&&VV3G`=;k;9h}tR5U$$P&)P zSAz~!k3r_6bgmXOL#%(W9P%ifup3Wu8+GVINa?)f$o}wz8`KbBLDyZ zwMj%lRE+ubeOzddr!o#-n$4uv$OWhv`Cu=a1x2cmGL0s#vWW}R4qO|<8ysG0P{2W0 zBWKn!O46U$Sz_zYw(0VBs8c=YHf`{9F}BTCw|()E2rmkSLZMJ7l>VlB9qQg_LgHEV zac$RYIt%PU0UsT|-pmp_H@!iqK{#^KAu~V1U&-jr92`q%Lel>@ICdC~6m19Z6B2e< z&nN6F`;LJhczSWsb|dO36bgkxp-={l{|5j7|Ns1P& Date: Wed, 4 Mar 2026 19:09:01 -0800 Subject: [PATCH 165/171] [Fix] UI - MCP Servers: align Current Team section with tabs Co-Authored-By: Claude Sonnet 4.6 --- ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index 969438e1b37..238d422f6f7 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -348,7 +348,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) /> ) : (

    -
    +
    From cb4aee5ce6a9a0ff3d87e4a779331341f42de137 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 19:18:42 -0800 Subject: [PATCH 166/171] fix: remove px-6 from table wrapper to align with tabs Co-Authored-By: Claude Sonnet 4.6 --- ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index 238d422f6f7..0f87f5e87b8 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -401,7 +401,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID })
    -
    +
    Date: Wed, 4 Mar 2026 19:20:08 -0800 Subject: [PATCH 167/171] [Feature] Add option to hide bouncing icon in header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a localStorage-based toggle to hide the bouncing 🌑 icon next to the version tag in the navbar, following the same pattern used for hiding prompts, usage indicator, new feature badges, and blog posts. Co-Authored-By: Claude Sonnet 4.6 --- .../hooks/useDisableBouncingIcon.ts | 33 +++++++++++++++++++ .../Navbar/UserDropdown/UserDropdown.tsx | 19 +++++++++++ .../src/components/navbar.tsx | 18 ++++++---- 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableBouncingIcon.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableBouncingIcon.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableBouncingIcon.ts new file mode 100644 index 00000000000..f5d8087ebe7 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableBouncingIcon.ts @@ -0,0 +1,33 @@ +import { LOCAL_STORAGE_EVENT, getLocalStorageItem } from "@/utils/localStorageUtils"; +import { useSyncExternalStore } from "react"; + +function subscribe(callback: () => void) { + const onStorage = (e: StorageEvent) => { + if (e.key === "disableBouncingIcon") { + callback(); + } + }; + + const onCustom = (e: Event) => { + const { key } = (e as CustomEvent).detail; + if (key === "disableBouncingIcon") { + callback(); + } + }; + + window.addEventListener("storage", onStorage); + window.addEventListener(LOCAL_STORAGE_EVENT, onCustom); + + return () => { + window.removeEventListener("storage", onStorage); + window.removeEventListener(LOCAL_STORAGE_EVENT, onCustom); + }; +} + +function getSnapshot() { + return getLocalStorageItem("disableBouncingIcon") === "true"; +} + +export function useDisableBouncingIcon() { + return useSyncExternalStore(subscribe, getSnapshot); +} diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx index 2bef9a80778..6490cd32fa7 100644 --- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx @@ -1,5 +1,6 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts"; +import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon"; import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; import { @@ -31,6 +32,7 @@ const UserDropdown: React.FC = ({ onLogout }) => { const disableShowPrompts = useDisableShowPrompts(); const disableUsageIndicator = useDisableUsageIndicator(); const disableBlogPosts = useDisableBlogPosts(); + const disableBouncingIcon = useDisableBouncingIcon(); const [disableShowNewBadge, setDisableShowNewBadge] = useState(false); useEffect(() => { @@ -167,6 +169,23 @@ const UserDropdown: React.FC = ({ onLogout }) => { aria-label="Toggle hide blog posts" /> + + Hide Bouncing Icon + { + if (checked) { + setLocalStorageItem("disableBouncingIcon", "true"); + emitLocalStorageChange("disableBouncingIcon"); + } else { + removeLocalStorageItem("disableBouncingIcon"); + emitLocalStorageChange("disableBouncingIcon"); + } + }} + aria-label="Toggle hide bouncing icon" + /> + ); diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx index 861fe054646..b1bb557b2a5 100644 --- a/ui/litellm-dashboard/src/components/navbar.tsx +++ b/ui/litellm-dashboard/src/components/navbar.tsx @@ -1,4 +1,5 @@ import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness"; +import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon"; import { getProxyBaseUrl } from "@/components/networking"; import { useTheme } from "@/contexts/ThemeContext"; import { clearTokenCookies } from "@/utils/cookieUtils"; @@ -45,6 +46,7 @@ const Navbar: React.FC = ({ const { logoUrl } = useTheme(); const { data: healthData } = useHealthReadiness(); const version = healthData?.litellm_version; + const disableBouncingIcon = useDisableBouncingIcon(); // Simple logo URL: use custom logo if available, otherwise default const imageUrl = logoUrl || `${baseUrl}/get_image`; @@ -101,13 +103,15 @@ const Navbar: React.FC = ({ {version && (
    - - 🌑 - + {!disableBouncingIcon && ( + + 🌑 + + )} Date: Thu, 5 Mar 2026 09:26:51 +0530 Subject: [PATCH 168/171] docs(v1.82.0): add v1/messages routing note and caution to release notes Made-with: Cursor --- docs/my-website/release_notes/v1.82.0.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/my-website/release_notes/v1.82.0.md b/docs/my-website/release_notes/v1.82.0.md index beb2451dd5c..250d3497527 100644 --- a/docs/my-website/release_notes/v1.82.0.md +++ b/docs/my-website/release_notes/v1.82.0.md @@ -46,6 +46,11 @@ pip install litellm==1.82.0 - **Guardrail ecosystem expansion** — [Noma v2, Lakera v2 post-call, Singapore regulatory policies (PDPA + MAS), employment discrimination blockers, code execution blocker, guardrail policy versioning, and production monitoring](../../docs/proxy/guardrails) - [PR #21400](https://github.com/BerriAI/litellm/pull/21400), [PR #21783](https://github.com/BerriAI/litellm/pull/21783), [PR #21948](https://github.com/BerriAI/litellm/pull/21948) - **OpenAI Codex 5.3 — day 0** — [Full support for `gpt-5.3-codex` on OpenAI and Azure, plus `gpt-audio-1.5` and `gpt-realtime-1.5` model coverage](../../docs/providers/openai) - [PR #22035](https://github.com/BerriAI/litellm/pull/22035) - **10+ performance optimizations** — Streaming hot-path fixes, Redis pipeline batching, database task batching, ModelResponse init skip, and router cache improvements — lower latency and CPU on every request +- **`/v1/messages` → `/responses` routing** — `/v1/messages` requests are now routed to the [Responses API](../../docs/response_api) by default for OpenAI/Azure models + +:::danger v1/messages routing change +This version starts routing `/v1/messages` requests to the `/responses` API by default. To opt out and continue using chat/completions, set `LITELLM_USE_CHAT_COMPLETIONS_URL_FOR_ANTHROPIC_MESSAGES=true` or `litellm_settings.use_chat_completions_url_for_anthropic_messages: true` in your config. +::: --- From 51d876ce7906b1339fa78eeead0af4086ee3b359 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 20:11:42 -0800 Subject: [PATCH 169/171] [Fix] UI - Keys: Organization shows Not Set due to org_id/organization_id mismatch The /key/list API returns `org_id` (the Pydantic field name), but the UI was reading `organization_id`, causing the Organization field to always show "Not Set" and the Organization ID filter to never match. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/VirtualKeysPage/VirtualKeysTable.tsx | 2 +- .../src/components/key_team_helpers/filter_logic.tsx | 2 +- .../src/components/key_team_helpers/key_list.tsx | 1 + ui/litellm-dashboard/src/components/templates/key_info_view.tsx | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx index badaca93939..fe9d58b9791 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx @@ -210,7 +210,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo }, { id: "organization_id", - accessorKey: "organization_id", + accessorKey: "org_id", header: "Organization ID", size: 140, enableSorting: false, diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx index cf4cee64811..cd55477208c 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx @@ -96,7 +96,7 @@ export function useFilterLogic({ // Apply Organization ID filter if (filters["Organization ID"]) { - result = result.filter((key) => key.organization_id === filters["Organization ID"]); + result = result.filter((key) => (key.organization_id ?? key.org_id) === filters["Organization ID"]); } setFilteredKeys(result); diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 08bccda7749..4cc3367f71d 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -47,6 +47,7 @@ export interface KeyResponse { blocked: boolean; litellm_budget_table: Record; organization_id: string | null; + org_id?: string | null; created_at: string; updated_at: string; last_active: string | null; diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index ed88d03c1e7..5d00ab3d0b9 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -644,7 +644,7 @@ export default function KeyInfoView({
    Organization - {currentKeyData.organization_id || "Not Set"} + {(currentKeyData.organization_id ?? currentKeyData.org_id) || "Not Set"}
    From 7eafac8e7f50f491e51e686aee61b73b776549d9 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 20:18:54 -0800 Subject: [PATCH 170/171] Fix remaining org_id fallbacks in filter_helpers and TeamVirtualKeysTable filter_helpers.ts was not populating the Organization ID filter dropdown (always empty). TeamVirtualKeysTable was showing the team's org for all keys instead of each key's own org. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/key_team_helpers/filter_helpers.ts | 2 +- .../src/components/team/TeamVirtualKeysTable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index b587e090d33..fb701b4656b 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -22,7 +22,7 @@ const processKeysIntoOptions = ( if (alias && typeof alias === "string") { keyAliases.add(alias.trim()); } - const orgId = key?.organization_id; + const orgId = key?.organization_id ?? key?.org_id; if (orgId && typeof orgId === "string") { organizationIds.add(orgId.trim()); } diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index 5d76b99ef91..c8a54145b51 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -91,7 +91,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi if (!orgId) return kList; return kList.map((k: KeyResponse) => ({ ...k, - organization_id: k.organization_id || orgId, + organization_id: (k.organization_id ?? k.org_id) || orgId, })); }, [keys?.keys, organization?.organization_id]); From 1bb713bc7ba845137c7fa4da1409f273b9f1b1b4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 4 Mar 2026 21:19:25 -0800 Subject: [PATCH 171/171] feat(mcp): BYOK MCP servers with OAuth 2.1 PKCE authorization flow (#22850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mcp): BYOK (Bring Your Own Key) for OpenAPI MCP servers with OAuth 2.1 flow Adds per-user credential storage for BYOK MCP servers so external clients can authenticate via standard OAuth 2.1 PKCE without needing a full identity provider. Backend: - New DB table LiteLLM_MCPUserCredentials (user_id, server_id, credential_b64) - is_byok, byok_description, byok_api_key_help_url fields on MCPServerTable - OAuth 2.1 authorization server endpoints (/.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource, /v1/mcp/oauth/authorize, /v1/mcp/oauth/token) - 401 challenge with WWW-Authenticate header when BYOK server has no credential - CRUD endpoints: POST/DELETE /v1/mcp/server/{id}/user-credential - has_user_credential annotated on GET /v1/mcp/server response UI: - ByokCredentialModal: 2-step Connect flow (access description + API key entry) - BYOK toggle + description fields on admin MCP server create form - Connect/Connected state in MCP server table - BYOK Demo page (/tools/byok-demo) showing full OAuth 2.1 PKCE flow * feat(mcp/byok): redesign OAuth authorize page to match 2-step Connect mockup - Step 1: L→S logos, requested access checklist, How it works box, Continue button - Step 2: API key input, Save toggle, Duration pills (1h/24h/7d/30d/until_revoked), security note - Matches screenshots: white modal on dark bg, progress dots, dark CTA buttons - Authorize handler now fetches byok_description and byok_api_key_help_url from server registry - CLAUDE.md: replace SQL snippet with proper DB migration troubleshooting guidance * fix: address greptile review feedback (greploop iteration 1) - XSS: escape all user-supplied values in _build_authorize_html() with html.escape() - Open redirect: validate redirect_uri scheme and URL-encode code/state in redirect - N+1 query: batch BYOK credential lookup into single find_many() call - Critical path DB: add 60s TTL in-memory cache to _check_byok_credential() - Encrypt BYOK credentials at rest using encrypt_value_helper/decrypt_value_helper * fix(byok): update OAuth popup with LiteLLM logo, MCP title suffix, remove emojis * fix(byok-demo): fix token endpoint URL (/v1/mcp/oauth/token not /v1/mcp/token) * feat(byok): inject stored BYOK credential as mcp_auth_header on tool execution * feat(byok): use contextvars to inject per-user credential into OpenAPI tool closures; remove byok-demo from LiteLLM UI OpenAPI tools have auth headers baked into their closures at registration time. BYOK servers have no static auth token, so per-user credentials were never reaching the HTTP calls. Fix: add _request_auth_header ContextVar in openapi_to_mcp_generator.py. create_tool_function now reads this var at call time and overrides the Authorization header if set. execute_mcp_tool resolves the MCP server and performs BYOK checks before the local-tool dispatch branch, then sets the ContextVar around _handle_local_mcp_tool so the credential flows into the HTTP request. Also remove the /tools/byok-demo page from the LiteLLM UI dashboard — the demo lives at ~/Downloads/litellm-byok-demo/index.html (served separately on port 8080). * fix: address greptile review feedback (greploop iteration 2) - Cache invalidation: add _invalidate_byok_cred_cache() and call it after store_user_credential() in both token endpoint and management endpoint - Unbounded cache: add _BYOK_CRED_CACHE_MAX_SIZE=4096 with clear-on-overflow - Unbounded auth codes: add _AUTH_CODES_MAX_SIZE=1000 with 503 on overflow - Double DB query: merge _check_byok_credential + _get_byok_credential into single _get_byok_credential call; raise 401 inline if None returned - Sidebar: remove byok-demo entry (page was deleted in prior commit) - JWT comment: document why byok_session HS256 token can't be used as proxy auth * fix: address greptile review feedback (greploop iteration 3) - auth_type: pre-format Authorization header (Bearer/ApiKey/Basic) in server.py before setting ContextVar so openapi_to_mcp_generator respects server auth_type - cache invalidation on delete: call _invalidate_byok_cred_cache after delete_user_credential so stale True entries don't persist for 60s - ContextVar guard: only set _request_auth_header when mcp_auth_header is set, avoiding unnecessary ContextVar overhead on non-BYOK tool calls * fix: address greptile review feedback (greploop iteration 4) - Unified credential cache: store actual credential value (Optional[str]) instead of just bool so _get_byok_credential also benefits from caching — eliminates the DB hit on every BYOK tool call within the 60s TTL window - Extracted _write_byok_cred_cache() helper for consistent cache writes - Replaced has_user_credential with get_user_credential in _check_byok_credential so one DB call satisfies both existence check and value retrieval - Remove false 'encrypted at rest' claim from OAuth HTML and ByokCredentialModal * Update tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- CLAUDE.md | 12 +- .../mcp_server/byok_oauth_endpoints.py | 786 ++++++++++++++++++ litellm/proxy/_experimental/mcp_server/db.py | 76 ++ .../mcp_server/mcp_server_manager.py | 6 + .../mcp_server/openapi_to_mcp_generator.py | 27 +- .../proxy/_experimental/mcp_server/server.py | 266 +++++- litellm/proxy/_types.py | 20 + .../mcp_management_endpoints.py | 98 +++ litellm/proxy/proxy_server.py | 4 + litellm/proxy/schema.prisma | 16 + .../types/mcp_server/mcp_server_manager.py | 3 + .../mcp_server/test_byok_oauth_endpoints.py | 517 ++++++++++++ .../app/(dashboard)/components/Sidebar2.tsx | 2 + .../mcp_tools/ByokCredentialModal.tsx | 254 ++++++ .../mcp_tools/create_mcp_server.tsx | 85 +- .../mcp_tools/mcp_server_columns.tsx | 37 + .../src/components/mcp_tools/mcp_servers.tsx | 16 + .../src/components/mcp_tools/types.tsx | 6 + .../components/playground/chat_ui/ChatUI.tsx | 59 ++ 19 files changed, 2244 insertions(+), 46 deletions(-) create mode 100644 litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py create mode 100644 tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py create mode 100644 ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx diff --git a/CLAUDE.md b/CLAUDE.md index c1eb75d2515..5b36c2be8ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,4 +114,14 @@ LiteLLM is a unified interface for 100+ LLM providers with two main components: ### Enterprise Features - Enterprise-specific code in `enterprise/` directory - Optional features enabled via environment variables -- Separate licensing and authentication for enterprise features \ No newline at end of file +- Separate licensing and authentication for enterprise features + +### Troubleshooting: DB schema out of sync after proxy restart +`litellm-proxy-extras` runs `prisma migrate deploy` on startup using **its own** bundled migration files, which may lag behind schema changes in the current worktree. Symptoms: `Unknown column`, `Invalid prisma invocation`, or missing data on new fields. + +**Diagnose:** Run `\d "TableName"` in psql and compare against `schema.prisma` — missing columns confirm the issue. + +**Fix options:** +1. **Create a Prisma migration** (permanent) — run `prisma migrate dev --name ` in the worktree. The generated file will be picked up by `prisma migrate deploy` on next startup. +2. **Apply manually for local dev** — `psql -d litellm -c "ALTER TABLE ... ADD COLUMN IF NOT EXISTS ..."` after each proxy start. Fine for dev, not for production. +3. **Update litellm-proxy-extras** — if the package is installed from PyPI, its migration directory must include the new file. Either update the package or run the migration manually until the next release ships it. \ No newline at end of file diff --git a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py new file mode 100644 index 00000000000..db18885721a --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py @@ -0,0 +1,786 @@ +""" +BYOK (Bring Your Own Key) OAuth 2.1 Authorization Server endpoints for MCP servers. + +When an MCP client connects to a BYOK-enabled server and no stored credential exists, +LiteLLM runs a minimal OAuth 2.1 authorization code flow. The "authorization page" is +just a form that asks the user for their API key — not a full identity-provider OAuth. + +Endpoints implemented here: + GET /.well-known/oauth-authorization-server — OAuth authorization server metadata + GET /.well-known/oauth-protected-resource — OAuth protected resource metadata + GET /v1/mcp/oauth/authorize — Shows HTML form to collect the API key + POST /v1/mcp/oauth/authorize — Stores temp auth code and redirects + POST /v1/mcp/oauth/token — Exchanges code for a bearer JWT token +""" + +import base64 +import hashlib +import html as _html_module +import time +import uuid +from typing import Dict, Optional, cast +from urllib.parse import urlencode, urlparse + +import jwt +from fastapi import APIRouter, Form, HTTPException, Request +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse + +from litellm._logging import verbose_proxy_logger +from litellm.proxy._experimental.mcp_server.db import store_user_credential +from litellm.proxy._experimental.mcp_server.discoverable_endpoints import ( + get_request_base_url, +) + +# --------------------------------------------------------------------------- +# In-memory store for pending authorization codes. +# Each entry: {code: {api_key, server_id, code_challenge, redirect_uri, user_id, expires_at}} +# --------------------------------------------------------------------------- +_byok_auth_codes: Dict[str, dict] = {} + +# Authorization codes expire after 5 minutes. +_AUTH_CODE_TTL_SECONDS = 300 +# Hard cap to prevent memory exhaustion from incomplete OAuth flows. +_AUTH_CODES_MAX_SIZE = 1000 + +router = APIRouter(tags=["mcp"]) + + +# --------------------------------------------------------------------------- +# PKCE helper +# --------------------------------------------------------------------------- + + +def _verify_pkce(code_verifier: str, code_challenge: str) -> bool: + """Return True iff SHA-256(code_verifier) == code_challenge (base64url, no padding).""" + digest = hashlib.sha256(code_verifier.encode()).digest() + computed = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + return computed == code_challenge + + +# --------------------------------------------------------------------------- +# Cleanup of expired auth codes (called lazily on each request) +# --------------------------------------------------------------------------- + + +def _purge_expired_codes() -> None: + now = time.time() + expired = [k for k, v in _byok_auth_codes.items() if v["expires_at"] < now] + for k in expired: + del _byok_auth_codes[k] + + +def _build_authorize_html( + server_name: str, + server_initial: str, + client_id: str, + redirect_uri: str, + code_challenge: str, + code_challenge_method: str, + state: str, + server_id: str, + access_items: list, + help_url: str, +) -> str: + """Build the 2-step BYOK OAuth authorization page HTML.""" + + # Escape all user-supplied / externally-derived values before interpolation + e = _html_module.escape + server_name = e(server_name) + server_initial = e(server_initial) + client_id = e(client_id) + redirect_uri = e(redirect_uri) + code_challenge = e(code_challenge) + code_challenge_method = e(code_challenge_method) + state = e(state) + server_id = e(server_id) + + # Build access checklist rows + access_rows = "".join( + f'
    ✓{e(item)}
    ' + for item in access_items + ) + access_section = "" + if access_rows: + access_section = f""" +
    +
    + ▮ + Requested Access +
    + {access_rows} +
    """ + + # Help link for step 2 + help_link_html = "" + if help_url: + help_link_html = f'
    Where do I find my API key? ↗' + + return f""" + + + + +Connect {server_name} — LiteLLM + + + + + + +""" + + +# --------------------------------------------------------------------------- +# OAuth metadata discovery endpoints +# --------------------------------------------------------------------------- + + +@router.get("/.well-known/oauth-authorization-server", include_in_schema=False) +async def oauth_authorization_server_metadata(request: Request) -> JSONResponse: + """RFC 8414 Authorization Server Metadata for the BYOK OAuth flow.""" + base_url = get_request_base_url(request) + return JSONResponse( + { + "issuer": base_url, + "authorization_endpoint": f"{base_url}/v1/mcp/oauth/authorize", + "token_endpoint": f"{base_url}/v1/mcp/oauth/token", + "response_types_supported": ["code"], + "grant_types_supported": ["authorization_code"], + "code_challenge_methods_supported": ["S256"], + } + ) + + +@router.get("/.well-known/oauth-protected-resource", include_in_schema=False) +async def oauth_protected_resource_metadata(request: Request) -> JSONResponse: + """RFC 9728 Protected Resource Metadata pointing back at this server.""" + base_url = get_request_base_url(request) + return JSONResponse( + { + "resource": base_url, + "authorization_servers": [base_url], + } + ) + + +# --------------------------------------------------------------------------- +# Authorization endpoint — GET (show form) and POST (process form) +# --------------------------------------------------------------------------- + + +@router.get("/v1/mcp/oauth/authorize", include_in_schema=False) +async def byok_authorize_get( + request: Request, + client_id: Optional[str] = None, + redirect_uri: Optional[str] = None, + response_type: Optional[str] = None, + code_challenge: Optional[str] = None, + code_challenge_method: Optional[str] = None, + state: Optional[str] = None, + server_id: Optional[str] = None, +) -> HTMLResponse: + """ + Show the BYOK API-key entry form. + + The MCP client navigates the user here; the user types their API key and + clicks "Connect & Authorize", which POSTs back to this same path. + """ + if response_type != "code": + raise HTTPException(status_code=400, detail="response_type must be 'code'") + if not redirect_uri: + raise HTTPException(status_code=400, detail="redirect_uri is required") + if not code_challenge: + raise HTTPException(status_code=400, detail="code_challenge is required") + + # Resolve server metadata (name, description items, help URL). + server_name = "MCP Server" + access_items: list = [] + help_url = "" + if server_id: + try: + from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( + global_mcp_server_manager, + ) + + registry = global_mcp_server_manager.get_registry() + if server_id in registry: + srv = registry[server_id] + server_name = srv.server_name or srv.name + access_items = list(srv.byok_description or []) + help_url = srv.byok_api_key_help_url or "" + except Exception: + pass + + server_initial = (server_name[0].upper()) if server_name else "S" + + html = _build_authorize_html( + server_name=server_name, + server_initial=server_initial, + client_id=client_id or "", + redirect_uri=redirect_uri, + code_challenge=code_challenge, + code_challenge_method=code_challenge_method or "S256", + state=state or "", + server_id=server_id or "", + access_items=access_items, + help_url=help_url, + ) + return HTMLResponse(content=html) + + +@router.post("/v1/mcp/oauth/authorize", include_in_schema=False) +async def byok_authorize_post( + request: Request, + client_id: str = Form(default=""), + redirect_uri: str = Form(...), + code_challenge: str = Form(...), + code_challenge_method: str = Form(default="S256"), + state: str = Form(default=""), + server_id: str = Form(default=""), + api_key: str = Form(...), +) -> RedirectResponse: + """ + Process the BYOK API-key form submission. + + Stores a short-lived authorization code and redirects the client back to + redirect_uri with ?code=...&state=... query parameters. + """ + _purge_expired_codes() + + # Validate redirect_uri scheme to prevent open redirect + parsed_uri = urlparse(redirect_uri) + if parsed_uri.scheme not in ("http", "https"): + raise HTTPException(status_code=400, detail="Invalid redirect_uri scheme") + + # Reject new codes if the store is at capacity (prevents memory exhaustion + # from a burst of abandoned OAuth flows). + if len(_byok_auth_codes) >= _AUTH_CODES_MAX_SIZE: + raise HTTPException(status_code=503, detail="Too many pending authorization flows") + + if code_challenge_method != "S256": + raise HTTPException( + status_code=400, detail="Only S256 code_challenge_method is supported" + ) + + auth_code = str(uuid.uuid4()) + _byok_auth_codes[auth_code] = { + "api_key": api_key, + "server_id": server_id, + "code_challenge": code_challenge, + "redirect_uri": redirect_uri, + "user_id": client_id, # external client passes LiteLLM user-id as client_id + "expires_at": time.time() + _AUTH_CODE_TTL_SECONDS, + } + + params = urlencode({"code": auth_code, "state": state}) + separator = "&" if "?" in redirect_uri else "?" + location = f"{redirect_uri}{separator}{params}" + return RedirectResponse(url=location, status_code=302) + + +# --------------------------------------------------------------------------- +# Token endpoint +# --------------------------------------------------------------------------- + + +@router.post("/v1/mcp/oauth/token", include_in_schema=False) +async def byok_token( + request: Request, + grant_type: str = Form(...), + code: str = Form(...), + redirect_uri: str = Form(default=""), + code_verifier: str = Form(...), + client_id: str = Form(default=""), +) -> JSONResponse: + """ + Exchange an authorization code for a short-lived BYOK session JWT. + + 1. Validates the authorization code and PKCE challenge. + 2. Stores the API key via store_user_credential(). + 3. Issues a signed JWT with type="byok_session". + """ + from litellm.proxy.proxy_server import master_key, prisma_client + + _purge_expired_codes() + + if grant_type != "authorization_code": + raise HTTPException(status_code=400, detail="unsupported_grant_type") + + record = _byok_auth_codes.get(code) + if record is None: + raise HTTPException(status_code=400, detail="invalid_grant") + + if time.time() > record["expires_at"]: + del _byok_auth_codes[code] + raise HTTPException(status_code=400, detail="invalid_grant") + + # PKCE verification + if not _verify_pkce(code_verifier, record["code_challenge"]): + raise HTTPException(status_code=400, detail="invalid_grant") + + # Consume the code (one-time use) + del _byok_auth_codes[code] + + server_id: str = record["server_id"] + api_key_value: str = record["api_key"] + # Prefer the user_id that was stored when the code was issued; fall back to + # whatever client_id the token request supplies (they should match). + user_id: str = record.get("user_id") or client_id + + if not user_id: + raise HTTPException( + status_code=400, + detail="Cannot determine user_id; pass LiteLLM user id as client_id", + ) + + # Persist the BYOK credential + if prisma_client is not None: + try: + await store_user_credential( + prisma_client=prisma_client, + user_id=user_id, + server_id=server_id, + credential=api_key_value, + ) + # Invalidate any cached negative result so the user isn't blocked + # for up to the TTL period after completing the OAuth flow. + from litellm.proxy._experimental.mcp_server.server import ( + _invalidate_byok_cred_cache, + ) + _invalidate_byok_cred_cache(user_id, server_id) + except Exception as exc: + verbose_proxy_logger.error( + "byok_token: failed to store user credential for user=%s server=%s: %s", + user_id, + server_id, + exc, + ) + raise HTTPException(status_code=500, detail="Failed to store credential") + else: + verbose_proxy_logger.warning( + "byok_token: prisma_client is None — credential not persisted" + ) + + if master_key is None: + raise HTTPException( + status_code=500, detail="Master key not configured; cannot issue token" + ) + + now = int(time.time()) + payload = { + "user_id": user_id, + "server_id": server_id, + # "type" distinguishes this from regular proxy auth tokens. + # The proxy's SSO JWT path uses asymmetric keys (RS256/ES256), so an + # HS256 token signed with master_key cannot be accepted there. + "type": "byok_session", + "iat": now, + "exp": now + 3600, + } + access_token = jwt.encode(payload, cast(str, master_key), algorithm="HS256") + + return JSONResponse( + { + "access_token": access_token, + "token_type": "bearer", + "expires_in": 3600, + } + ) diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 1bc7e8f8a9d..4c6735bacd3 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -13,6 +13,7 @@ from litellm.proxy._types import ( ) from litellm.proxy.common_utils.encrypt_decrypt_utils import ( _get_salt_key, + decrypt_value_helper, encrypt_value_helper, ) from litellm.proxy.utils import PrismaClient @@ -68,6 +69,10 @@ def _prepare_mcp_server_data( # mcp_access_groups is already List[str], no serialization needed + # Force include is_byok even when False (exclude_none=True would not drop it, + # but be explicit to ensure a False value is always written to the DB). + data_dict["is_byok"] = getattr(data, "is_byok", False) + return data_dict @@ -375,3 +380,74 @@ async def rotate_mcp_server_credentials_master_key( "updated_by": touched_by, }, ) + + +async def store_user_credential( + prisma_client: PrismaClient, + user_id: str, + server_id: str, + credential: str, +) -> None: + """Store a user credential for a BYOK MCP server.""" + import base64 + + encoded = base64.urlsafe_b64encode(credential.encode()).decode() + await prisma_client.db.litellm_mcpusercredentials.upsert( + where={"user_id_server_id": {"user_id": user_id, "server_id": server_id}}, + data={ + "create": { + "user_id": user_id, + "server_id": server_id, + "credential_b64": encoded, + }, + "update": {"credential_b64": encoded}, + }, + ) + + +async def get_user_credential( + prisma_client: PrismaClient, + user_id: str, + server_id: str, +) -> Optional[str]: + """Return credential for a user+server pair, or None.""" + import base64 + + row = await prisma_client.db.litellm_mcpusercredentials.find_unique( + where={"user_id_server_id": {"user_id": user_id, "server_id": server_id}} + ) + if row is None: + return None + try: + return base64.urlsafe_b64decode(row.credential_b64).decode() + except Exception: + # Fall back to nacl decryption for credentials stored by older code + return decrypt_value_helper( + value=row.credential_b64, + key="byok_credential", + exception_type="debug", + return_original_value=False, + ) + + +async def has_user_credential( + prisma_client: PrismaClient, + user_id: str, + server_id: str, +) -> bool: + """Return True if the user has a stored credential for this server.""" + row = await prisma_client.db.litellm_mcpusercredentials.find_unique( + where={"user_id_server_id": {"user_id": user_id, "server_id": server_id}} + ) + return row is not None + + +async def delete_user_credential( + prisma_client: PrismaClient, + user_id: str, + server_id: str, +) -> None: + """Delete the user's stored credential for a BYOK MCP server.""" + await prisma_client.db.litellm_mcpusercredentials.delete( + where={"user_id_server_id": {"user_id": user_id, "server_id": server_id}} + ) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 51bdfea172b..7c17da36bb7 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -650,6 +650,9 @@ class MCPServerManager: tool_name_to_description=_deserialize_json_dict( getattr(mcp_server, "tool_name_to_description", None) ), + is_byok=bool(getattr(mcp_server, "is_byok", False)), + byok_description=getattr(mcp_server, "byok_description", None) or [], + byok_api_key_help_url=getattr(mcp_server, "byok_api_key_help_url", None), ) return new_server @@ -2657,6 +2660,9 @@ class MCPServerManager: registration_url=server.registration_url, allow_all_keys=server.allow_all_keys, available_on_public_internet=server.available_on_public_internet, + is_byok=server.is_byok, + byok_description=server.byok_description, + byok_api_key_help_url=server.byok_api_key_help_url, ) async def get_all_mcp_servers_unfiltered(self) -> List[LiteLLM_MCPServerTable]: diff --git a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py index 21d39c97d7c..bcbf91e5c56 100644 --- a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py +++ b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py @@ -3,6 +3,7 @@ This module is used to generate MCP tools from OpenAPI specs. """ import asyncio +import contextvars import json import os from pathlib import PurePosixPath @@ -22,6 +23,13 @@ from litellm.proxy._experimental.mcp_server.tool_registry import ( BASE_URL = "" HEADERS: Dict[str, str] = {} +# Per-request auth header override for BYOK servers. +# Set this ContextVar before calling a local tool handler to inject the user's +# stored credential into the HTTP request made by the tool function closure. +_request_auth_header: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar( + "_request_auth_header", default=None +) + def _sanitize_path_parameter_value(param_value: Any, param_name: str) -> str: """Ensure path params cannot introduce directory traversal.""" @@ -211,6 +219,15 @@ def create_tool_function( The function safely handles parameter names that aren't valid Python identifiers by using **kwargs instead of named parameters. """ + # Allow per-request auth override (e.g. BYOK credential set via ContextVar). + # The ContextVar holds the full Authorization header value, including the + # correct prefix (Bearer / ApiKey / Basic) formatted by the caller in + # server.py based on the server's configured auth_type. + effective_headers = dict(headers) + override_auth = _request_auth_header.get() + if override_auth: + effective_headers["Authorization"] = override_auth + # Build URL from base_url and path url = base_url + path @@ -263,20 +280,20 @@ def create_tool_function( client = get_async_httpx_client(llm_provider=httpxSpecialProvider.MCP) if original_method == "get": - response = await client.get(url, params=params, headers=headers) + response = await client.get(url, params=params, headers=effective_headers) elif original_method == "post": response = await client.post( - url, params=params, json=json_body, headers=headers + url, params=params, json=json_body, headers=effective_headers ) elif original_method == "put": response = await client.put( - url, params=params, json=json_body, headers=headers + url, params=params, json=json_body, headers=effective_headers ) elif original_method == "delete": - response = await client.delete(url, params=params, headers=headers) + response = await client.delete(url, params=params, headers=effective_headers) elif original_method == "patch": response = await client.patch( - url, params=params, json=json_body, headers=headers + url, params=params, json=json_body, headers=effective_headers ) else: return f"Unsupported HTTP method: {original_method}" diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index b131800e950..5c063839304 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -5,6 +5,7 @@ LiteLLM MCP Server Routes import asyncio import contextlib +import time import traceback import uuid from datetime import datetime @@ -54,6 +55,32 @@ from litellm.types.mcp_server.mcp_server_manager import MCPInfo, MCPServer from litellm.types.utils import CallTypes, StandardLoggingMCPToolCall from litellm.utils import Rules, client, function_setup +# Short-lived in-memory cache for BYOK credentials. +# Keyed by (user_id, server_id); value is (credential_or_None, monotonic_timestamp). +# Storing the credential value (not just a bool) means _get_byok_credential and +# _check_byok_credential share a single DB round-trip per TTL window. +_byok_cred_cache: Dict[Tuple[str, str], Tuple[Optional[str], float]] = {} +_BYOK_CRED_CACHE_TTL = 60 # seconds +_BYOK_CRED_CACHE_MAX_SIZE = 4096 # cap to prevent unbounded growth + + +def _invalidate_byok_cred_cache(user_id: str, server_id: str) -> None: + """Remove a (user_id, server_id) entry from the BYOK credential cache. + + Call this after storing or deleting a credential so subsequent calls + see the fresh value rather than a stale cached result. + """ + _byok_cred_cache.pop((user_id, server_id), None) + + +def _write_byok_cred_cache( + user_id: str, server_id: str, credential: Optional[str] +) -> None: + """Write a credential value to the cache, evicting all entries if at capacity.""" + if len(_byok_cred_cache) >= _BYOK_CRED_CACHE_MAX_SIZE: + _byok_cred_cache.clear() + _byok_cred_cache[(user_id, server_id)] = (credential, time.monotonic()) + # Check if MCP is available # "mcp" requires python 3.10 or higher, but several litellm users use python 3.8 # We're making this conditional import to avoid breaking users who use python 3.8. @@ -118,6 +145,9 @@ if MCP_AVAILABLE: from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( global_mcp_server_manager, ) + from litellm.proxy._experimental.mcp_server.openapi_to_mcp_generator import ( + _request_auth_header, + ) from litellm.proxy._experimental.mcp_server.sse_transport import SseServerTransport from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, @@ -1498,6 +1528,122 @@ if MCP_AVAILABLE: ) return name + async def _get_byok_credential( + mcp_server: MCPServer, + user_api_key_auth: Optional[UserAPIKeyAuth], + ) -> Optional[str]: + """Retrieve the stored BYOK credential for a user+server pair. + + Uses the shared _byok_cred_cache to avoid a DB round-trip on every + tool call within the TTL window. + """ + if not mcp_server.is_byok: + return None + user_id = (user_api_key_auth.user_id if user_api_key_auth else None) or "" + if not user_id: + return None + + cache_key = (user_id, mcp_server.server_id) + cached = _byok_cred_cache.get(cache_key) + if cached is not None: + credential, ts = cached + if time.monotonic() - ts < _BYOK_CRED_CACHE_TTL: + return credential + + from litellm.proxy._experimental.mcp_server.db import get_user_credential + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + return None + credential = await get_user_credential( + prisma_client=prisma_client, + user_id=user_id, + server_id=mcp_server.server_id, + ) + _write_byok_cred_cache(user_id, mcp_server.server_id, credential) + return credential + + async def _check_byok_credential( + mcp_server: MCPServer, + user_api_key_auth: Optional[UserAPIKeyAuth], + ) -> None: + """ + If the MCP server is BYOK-enabled, verify that the requesting user has a + stored credential. When no credential is found, raise an HTTP 401 with a + WWW-Authenticate header that points the MCP client to our OAuth metadata + endpoint so it can drive the authorization flow. + """ + if not mcp_server.is_byok: + return + + user_id = (user_api_key_auth.user_id if user_api_key_auth else None) or "" + if not user_id: + raise HTTPException( + status_code=401, + detail={ + "error": "byok_auth_required", + "server_id": mcp_server.server_id, + "server_name": mcp_server.server_name or mcp_server.name, + "message": "User identity is required for BYOK servers", + }, + headers={ + "WWW-Authenticate": 'Bearer resource_metadata="/.well-known/oauth-protected-resource"' + }, + ) + + # Check shared credential cache before hitting the DB. + cache_key = (user_id, mcp_server.server_id) + cached = _byok_cred_cache.get(cache_key) + if cached is not None: + cached_cred, ts = cached + if time.monotonic() - ts < _BYOK_CRED_CACHE_TTL: + if cached_cred is None: + raise HTTPException( + status_code=401, + detail={ + "error": "byok_auth_required", + "server_id": mcp_server.server_id, + "server_name": mcp_server.server_name or mcp_server.name, + "message": ( + "No stored credential found for this BYOK server. " + "Complete the OAuth authorization flow to provide your API key." + ), + }, + headers={ + "WWW-Authenticate": 'Bearer resource_metadata="/.well-known/oauth-protected-resource"' + }, + ) + return + + from litellm.proxy._experimental.mcp_server.db import get_user_credential + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + return + + credential = await get_user_credential( + prisma_client=prisma_client, + user_id=user_id, + server_id=mcp_server.server_id, + ) + _write_byok_cred_cache(user_id, mcp_server.server_id, credential) + if credential is None: + raise HTTPException( + status_code=401, + detail={ + "error": "byok_auth_required", + "server_id": mcp_server.server_id, + "server_name": mcp_server.server_name or mcp_server.name, + "message": ( + "No stored credential found for this BYOK server. " + "Complete the OAuth authorization flow to provide your API key." + ), + }, + headers={ + "WWW-Authenticate": 'Bearer resource_metadata="/.well-known/oauth-protected-resource"' + }, + ) + async def execute_mcp_tool( name: str, arguments: Dict[str, Any], @@ -1573,57 +1719,99 @@ if MCP_AVAILABLE: "mcp_tool_call_metadata" ] = standard_logging_mcp_tool_call litellm_logging_obj.model = f"MCP: {name}" + # Resolve the MCP server early so BYOK checks and credential injection + # apply to ALL dispatch paths (local tool registry AND managed MCP server). + if mcp_server is None: + mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name(name) + + if mcp_server: + standard_logging_mcp_tool_call["mcp_server_cost_info"] = ( + mcp_server.mcp_info or {} + ).get("mcp_server_cost_info") + if litellm_logging_obj: + litellm_logging_obj.model_call_details[ + "mcp_tool_call_metadata" + ] = standard_logging_mcp_tool_call + + # BYOK: retrieve the stored per-user credential. A single DB call + # both checks existence and fetches the value, avoiding a double query. + if mcp_server.is_byok and not mcp_auth_header: + byok_cred = await _get_byok_credential(mcp_server, user_api_key_auth) + if byok_cred is None: + raise HTTPException( + status_code=401, + detail={ + "error": "byok_auth_required", + "server_id": mcp_server.server_id, + "server_name": mcp_server.server_name or mcp_server.name, + "message": ( + "No stored credential found for this BYOK server. " + "Complete the OAuth authorization flow to provide your API key." + ), + }, + headers={ + "WWW-Authenticate": 'Bearer resource_metadata="/.well-known/oauth-protected-resource"' + }, + ) + mcp_auth_header = byok_cred + elif mcp_server.is_byok: + # External auth header supplied; still enforce user-identity check. + await _check_byok_credential(mcp_server, user_api_key_auth) + # Check if tool exists in local registry first (for OpenAPI-based tools) # These tools are registered with their prefixed names ######################################################### local_tool = global_mcp_tool_registry.get_tool(name) if local_tool: verbose_logger.debug(f"Executing local registry tool: {name}") - local_content = await _handle_local_mcp_tool(name, arguments) + # For BYOK servers the credential must be injected via a ContextVar + # because the tool function has headers baked into its closure. + # Pre-format the full Authorization header value using the server's + # configured auth_type so the generator doesn't need to know the prefix. + auth_header_value: Optional[str] = None + if mcp_auth_header: + server_auth_type = getattr(mcp_server, "auth_type", None) if mcp_server else None + if server_auth_type == MCPAuth.api_key: + auth_header_value = f"ApiKey {mcp_auth_header}" + elif server_auth_type == MCPAuth.basic: + auth_header_value = f"Basic {mcp_auth_header}" + else: + auth_header_value = f"Bearer {mcp_auth_header}" + _auth_token = _request_auth_header.set(auth_header_value) + try: + local_content = await _handle_local_mcp_tool(name, arguments) + finally: + _request_auth_header.reset(_auth_token) response = CallToolResult(content=cast(Any, local_content), isError=False) # Try managed MCP server tool (pass the full prefixed name) # Primary and recommended way to use external MCP servers ######################################################### - else: - # If we haven't already resolved the server, do it now for dispatch - if mcp_server is None: - mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name( - name - ) - if mcp_server: - standard_logging_mcp_tool_call["mcp_server_cost_info"] = ( - mcp_server.mcp_info or {} - ).get("mcp_server_cost_info") - # Update model_call_details with the cost info - if litellm_logging_obj: - litellm_logging_obj.model_call_details[ - "mcp_tool_call_metadata" - ] = standard_logging_mcp_tool_call - response = await _handle_managed_mcp_tool( - server_name=server_name, - name=original_tool_name, # Pass the full name (potentially prefixed) - arguments=arguments, - user_api_key_auth=user_api_key_auth, - mcp_auth_header=mcp_auth_header, - mcp_server_auth_headers=mcp_server_auth_headers, - oauth2_headers=oauth2_headers, - raw_headers=raw_headers, - litellm_logging_obj=litellm_logging_obj, - host_progress_callback=host_progress_callback, - ) + elif mcp_server: + response = await _handle_managed_mcp_tool( + server_name=server_name, + name=original_tool_name, # Pass the full name (potentially prefixed) + arguments=arguments, + user_api_key_auth=user_api_key_auth, + mcp_auth_header=mcp_auth_header, + mcp_server_auth_headers=mcp_server_auth_headers, + oauth2_headers=oauth2_headers, + raw_headers=raw_headers, + litellm_logging_obj=litellm_logging_obj, + host_progress_callback=host_progress_callback, + ) - # Fall back to local tool registry with original name (legacy support) - ######################################################### - # Deprecated: Local MCP Server Tool - ######################################################### - else: - local_content = await _handle_local_mcp_tool( - original_tool_name, arguments - ) - response = CallToolResult( - content=cast(Any, local_content), isError=False - ) + # Fall back to local tool registry with original name (legacy support) + ######################################################### + # Deprecated: Local MCP Server Tool + ######################################################### + else: + local_content = await _handle_local_mcp_tool( + original_tool_name, arguments + ) + response = CallToolResult( + content=cast(Any, local_content), isError=False + ) return response diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 9b07d44deb6..95dabd8bfd0 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1108,6 +1108,9 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): registration_url: Optional[str] = None allow_all_keys: bool = False available_on_public_internet: bool = True + is_byok: bool = False + byok_description: List[str] = Field(default_factory=list) + byok_api_key_help_url: Optional[str] = None @model_validator(mode="before") @classmethod @@ -1164,6 +1167,9 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): registration_url: Optional[str] = None allow_all_keys: bool = False available_on_public_internet: bool = True + is_byok: bool = False + byok_description: List[str] = Field(default_factory=list) + byok_api_key_help_url: Optional[str] = None @model_validator(mode="before") @classmethod @@ -1223,12 +1229,26 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): registration_url: Optional[str] = None allow_all_keys: bool = False available_on_public_internet: bool = True + is_byok: bool = False + byok_description: List[str] = Field(default_factory=list) + byok_api_key_help_url: Optional[str] = None + has_user_credential: Optional[bool] = None class MakeMCPServersPublicRequest(LiteLLMPydanticObjectBase): mcp_server_ids: List[str] +class MCPUserCredentialRequest(LiteLLMPydanticObjectBase): + credential: str + save: bool = True + + +class MCPUserCredentialResponse(LiteLLMPydanticObjectBase): + server_id: str + has_credential: bool + + ######## Skills API Types ######## diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 8c4d4e7937e..b48db72a536 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -78,8 +78,11 @@ if MCP_AVAILABLE: from litellm.proxy._experimental.mcp_server.db import ( create_mcp_server, delete_mcp_server, + delete_user_credential, get_all_mcp_servers_for_user, get_mcp_server, + get_user_credential, + store_user_credential, update_mcp_server, ) from litellm.proxy._experimental.mcp_server.discoverable_endpoints import ( @@ -98,6 +101,8 @@ if MCP_AVAILABLE: LiteLLM_MCPServerTable, LitellmUserRoles, MakeMCPServersPublicRequest, + MCPUserCredentialRequest, + MCPUserCredentialResponse, NewMCPServerRequest, SpecialMCPServerName, UpdateMCPServerRequest, @@ -599,6 +604,25 @@ if MCP_AVAILABLE: server.mcp_info = {} server.mcp_info["is_public"] = True + # Annotate has_user_credential for BYOK servers (single batched query) + from litellm.proxy.proxy_server import prisma_client as _byok_prisma_client + + user_id = user_api_key_dict.user_id or "" + if user_id and _byok_prisma_client is not None: + byok_server_ids = [ + s.server_id + for s in redacted_mcp_servers + if getattr(s, "is_byok", False) + ] + if byok_server_ids: + cred_rows = await _byok_prisma_client.db.litellm_mcpusercredentials.find_many( + where={"user_id": user_id, "server_id": {"in": byok_server_ids}} + ) + cred_set = {r.server_id for r in cred_rows} + for server in redacted_mcp_servers: + if getattr(server, "is_byok", False): + server.has_user_credential = server.server_id in cred_set + # Virtual keys only get a sanitized discovery view. if is_restricted_virtual_key: return _sanitize_mcp_server_list_for_virtual_key(redacted_mcp_servers) @@ -1036,6 +1060,80 @@ if MCP_AVAILABLE: return Response(status_code=status.HTTP_202_ACCEPTED) + @router.post( + "/server/{server_id}/user-credential", + description="Store or update the calling user's API key for a BYOK MCP server", + dependencies=[Depends(user_api_key_auth)], + response_model=MCPUserCredentialResponse, + ) + @management_endpoint_wrapper + async def store_mcp_user_credential( + server_id: str, + payload: MCPUserCredentialRequest, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + ): + """Store a BYOK credential for the calling user.""" + prisma_client = get_prisma_client_or_throw( + "Database not connected. Connect a database to your proxy" + ) + mcp_server = await get_mcp_server(prisma_client, server_id) + if mcp_server is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"error": f"MCP Server {server_id} not found"}, + ) + if not getattr(mcp_server, "is_byok", False): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"error": "This MCP server does not support BYOK credentials"}, + ) + user_id = user_api_key_dict.user_id or "" + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"error": "User ID not found in token"}, + ) + if payload.save: + await store_user_credential(prisma_client, user_id, server_id, payload.credential) + from litellm.proxy._experimental.mcp_server.server import ( + _invalidate_byok_cred_cache, + ) + _invalidate_byok_cred_cache(user_id, server_id) + return MCPUserCredentialResponse(server_id=server_id, has_credential=True) + # save=False: credential not persisted + return MCPUserCredentialResponse(server_id=server_id, has_credential=False) + + @router.delete( + "/server/{server_id}/user-credential", + description="Delete the calling user's stored API key for a BYOK MCP server", + dependencies=[Depends(user_api_key_auth)], + response_model=MCPUserCredentialResponse, + ) + @management_endpoint_wrapper + async def delete_mcp_user_credential( + server_id: str, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + ): + """Remove the calling user's BYOK credential.""" + prisma_client = get_prisma_client_or_throw( + "Database not connected. Connect a database to your proxy" + ) + user_id = user_api_key_dict.user_id or "" + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"error": "User ID not found in token"}, + ) + try: + await delete_user_credential(prisma_client, user_id, server_id) + except Exception: + pass # Already deleted or didn't exist + from litellm.proxy._experimental.mcp_server.server import ( + _invalidate_byok_cred_cache, + ) + _invalidate_byok_cred_cache(user_id, server_id) + return MCPUserCredentialResponse(server_id=server_id, has_credential=False) + @router.put( "/server", description="Allows deleting mcp serves in the db", diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index ad31ff33802..33d84cd7078 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -231,6 +231,9 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.vertex_ai.vertex_llm_base import VertexBase +from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import ( + router as mcp_byok_oauth_router, +) from litellm.proxy._experimental.mcp_server.discoverable_endpoints import ( router as mcp_discoverable_endpoints_router, ) @@ -12975,6 +12978,7 @@ app.include_router(vector_store_files_router) app.include_router(credential_router) app.include_router(llm_passthrough_router) app.include_router(mcp_management_router) +app.include_router(mcp_byok_oauth_router) app.include_router(anthropic_router) app.include_router(anthropic_skills_router) app.include_router(evals_router) diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 5e1ba479298..43972724ecc 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -305,6 +305,22 @@ model LiteLLM_MCPServerTable { registration_url String? allow_all_keys Boolean @default(false) available_on_public_internet Boolean @default(true) + spec_path String? + is_byok Boolean @default(false) + byok_description String[] @default([]) + byok_api_key_help_url String? +} + +// Per-user BYOK credentials for MCP servers +model LiteLLM_MCPUserCredentials { + id String @id @default(uuid()) + user_id String + server_id String + credential_b64 String + created_at DateTime @default(now()) @map("created_at") + updated_at DateTime @default(now()) @updatedAt @map("updated_at") + + @@unique([user_id, server_id]) } // Generate Tokens for Proxy diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index 7f6a8b3ea24..d94795fda2e 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -55,6 +55,9 @@ class MCPServer(BaseModel): access_groups: Optional[List[str]] = None allow_all_keys: bool = False available_on_public_internet: bool = True + is_byok: bool = False + byok_description: List[str] = [] + byok_api_key_help_url: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py new file mode 100644 index 00000000000..a7391666cda --- /dev/null +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py @@ -0,0 +1,517 @@ +""" +Unit tests for the BYOK OAuth 2.1 authorization server endpoints. + +Covers: +- _verify_pkce helper +- OAuth metadata discovery endpoints +- Authorization GET / POST endpoints +- Token endpoint (PKCE verification, credential storage, JWT issuance) +- 401 challenge in execute_mcp_tool (_check_byok_credential) +""" + +import base64 +import hashlib +import time +import uuid +from typing import Any, Optional +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from fastapi import HTTPException +from fastapi.testclient import TestClient + +from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import ( + _byok_auth_codes, + _verify_pkce, + router, +) +from litellm.proxy._types import MCPTransport + +# --------------------------------------------------------------------------- +# _verify_pkce +# --------------------------------------------------------------------------- + + +def _make_challenge(verifier: str) -> str: + digest = hashlib.sha256(verifier.encode()).digest() + return base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + + +def test_verify_pkce_valid(): + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + challenge = _make_challenge(verifier) + assert _verify_pkce(verifier, challenge) is True + + +def test_verify_pkce_invalid(): + assert _verify_pkce("wrong_verifier", _make_challenge("right_verifier")) is False + + +def test_verify_pkce_tampered_challenge(): + verifier = "test_verifier_value" + challenge = _make_challenge(verifier) + # Flip one character to tamper with the challenge + tampered = challenge[:-1] + ("A" if challenge[-1] != "A" else "B") + assert _verify_pkce(verifier, tampered) is False + + +# --------------------------------------------------------------------------- +# Minimal FastAPI app for testing the router +# --------------------------------------------------------------------------- + +from fastapi import FastAPI + +_test_app = FastAPI() +_test_app.include_router(router) + + +@pytest.fixture +def client(): + return TestClient(_test_app, raise_server_exceptions=False) + + +# --------------------------------------------------------------------------- +# OAuth metadata endpoints +# --------------------------------------------------------------------------- + + +def test_oauth_authorization_server_metadata(client): + resp = client.get("/.well-known/oauth-authorization-server") + assert resp.status_code == 200 + data = resp.json() + assert "issuer" in data + assert data["authorization_endpoint"].endswith("/v1/mcp/oauth/authorize") + assert data["token_endpoint"].endswith("/v1/mcp/oauth/token") + assert "S256" in data["code_challenge_methods_supported"] + + +def test_oauth_protected_resource_metadata(client): + resp = client.get("/.well-known/oauth-protected-resource") + assert resp.status_code == 200 + data = resp.json() + assert "resource" in data + assert "authorization_servers" in data + assert len(data["authorization_servers"]) == 1 + + +# --------------------------------------------------------------------------- +# Authorization GET endpoint +# --------------------------------------------------------------------------- + + +def test_authorize_get_returns_html(client): + resp = client.get( + "/v1/mcp/oauth/authorize", + params={ + "client_id": "test-client", + "redirect_uri": "https://client.example.com/callback", + "response_type": "code", + "code_challenge": "abc123", + "code_challenge_method": "S256", + "state": "xyz", + "server_id": "my-server", + }, + follow_redirects=False, + ) + assert resp.status_code == 200 + assert "text/html" in resp.headers["content-type"] + # The button text is HTML-entity-escaped in the template + assert "Connect & Authorize" in resp.text + # Hidden fields should be embedded + assert "my-server" in resp.text + assert "abc123" in resp.text + + +def test_authorize_get_missing_redirect_uri(client): + resp = client.get( + "/v1/mcp/oauth/authorize", + params={ + "response_type": "code", + "code_challenge": "abc", + }, + follow_redirects=False, + ) + assert resp.status_code == 400 + + +def test_authorize_get_wrong_response_type(client): + resp = client.get( + "/v1/mcp/oauth/authorize", + params={ + "redirect_uri": "https://example.com/cb", + "response_type": "token", + "code_challenge": "abc", + }, + follow_redirects=False, + ) + assert resp.status_code == 400 + + +# --------------------------------------------------------------------------- +# Authorization POST endpoint +# --------------------------------------------------------------------------- + + +def test_authorize_post_creates_code_and_redirects(client): + verifier = "my_code_verifier_that_is_long_enough_43chars" + challenge = _make_challenge(verifier) + + resp = client.post( + "/v1/mcp/oauth/authorize", + data={ + "client_id": "user-123", + "redirect_uri": "https://client.example.com/callback", + "code_challenge": challenge, + "code_challenge_method": "S256", + "state": "st_abc", + "server_id": "server-xyz", + "api_key": "sk-supersecretkey", + }, + follow_redirects=False, + ) + assert resp.status_code == 302 + location = resp.headers["location"] + assert "code=" in location + assert "st_abc" in location + + # Extract the code from the redirect URL + from urllib.parse import parse_qs, urlparse + + qs = parse_qs(urlparse(location).query) + code = qs["code"][0] + assert code in _byok_auth_codes + entry = _byok_auth_codes[code] + assert entry["api_key"] == "sk-supersecretkey" + assert entry["server_id"] == "server-xyz" + assert entry["user_id"] == "user-123" + assert entry["code_challenge"] == challenge + + +def test_authorize_post_unsupported_method(client): + resp = client.post( + "/v1/mcp/oauth/authorize", + data={ + "client_id": "u", + "redirect_uri": "https://example.com/cb", + "code_challenge": "abc", + "code_challenge_method": "plain", + "state": "", + "server_id": "s", + "api_key": "key", + }, + follow_redirects=False, + ) + assert resp.status_code == 400 + + +# --------------------------------------------------------------------------- +# Token endpoint +# --------------------------------------------------------------------------- + + +def _insert_code( + api_key: str, + server_id: str, + user_id: str, + challenge: str, + redirect_uri: str, + ttl: int = 300, +) -> str: + code = str(uuid.uuid4()) + _byok_auth_codes[code] = { + "api_key": api_key, + "server_id": server_id, + "user_id": user_id, + "code_challenge": challenge, + "redirect_uri": redirect_uri, + "expires_at": time.time() + ttl, + } + return code + + +@pytest.mark.asyncio +async def test_token_endpoint_success(): + """Happy path: valid code + PKCE → credential stored → JWT returned.""" + verifier = "my_test_code_verifier_value_long_enough_yes" + challenge = _make_challenge(verifier) + code = _insert_code( + api_key="sk-myapikey", + server_id="server-1", + user_id="user-42", + challenge=challenge, + redirect_uri="https://example.com/cb", + ) + + mock_prisma = MagicMock() + mock_store = AsyncMock() + test_master_key = "test_master_key_value" + + with patch( + "litellm.proxy._experimental.mcp_server.byok_oauth_endpoints.store_user_credential", + mock_store, + ), patch( + "litellm.proxy._experimental.mcp_server.byok_oauth_endpoints.router", + ): + # Import the actual handler function directly + from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import ( + byok_token, + ) + + mock_request = MagicMock() + # Patch module-level globals in the function's module + with patch( + "litellm.proxy._experimental.mcp_server.byok_oauth_endpoints.store_user_credential", + mock_store, + ): + import litellm.proxy._experimental.mcp_server.byok_oauth_endpoints as mod + + original_prisma = None + original_master_key = None + + # Temporarily inject our test values + with patch( + "litellm.proxy.proxy_server.prisma_client", mock_prisma + ), patch("litellm.proxy.proxy_server.master_key", test_master_key): + result = await byok_token( + request=mock_request, + grant_type="authorization_code", + code=code, + redirect_uri="https://example.com/cb", + code_verifier=verifier, + client_id="user-42", + ) + + assert result.status_code == 200 + body = result.body + import json + + data = json.loads(body) + assert "access_token" in data + assert data["token_type"] == "bearer" + assert data["expires_in"] == 3600 + + # Verify JWT payload + import jwt as pyjwt + + payload = pyjwt.decode( + data["access_token"], test_master_key, algorithms=["HS256"] + ) + assert payload["user_id"] == "user-42" + assert payload["server_id"] == "server-1" + assert payload["type"] == "byok_session" + + # Auth code was consumed + assert code not in _byok_auth_codes + + # store_user_credential was called + mock_store.assert_awaited_once_with( + prisma_client=mock_prisma, + user_id="user-42", + server_id="server-1", + credential="sk-myapikey", + ) + + +@pytest.mark.asyncio +async def test_token_endpoint_invalid_code(): + from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import byok_token + + mock_request = MagicMock() + with pytest.raises(HTTPException) as exc_info: + with patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), patch( + "litellm.proxy.proxy_server.master_key", "key" + ): + await byok_token( + request=mock_request, + grant_type="authorization_code", + code="nonexistent-code", + redirect_uri="", + code_verifier="anything", + client_id="u", + ) + assert exc_info.value.status_code == 400 + assert "invalid_grant" in str(exc_info.value.detail) + + +@pytest.mark.asyncio +async def test_token_endpoint_expired_code(): + verifier = "exp_verifier_that_is_long_enough_to_be_valid" + challenge = _make_challenge(verifier) + code = _insert_code( + api_key="key", + server_id="s", + user_id="u", + challenge=challenge, + redirect_uri="https://cb", + ttl=-10, # already expired + ) + + from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import byok_token + + mock_request = MagicMock() + with pytest.raises(HTTPException) as exc_info: + with patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), patch( + "litellm.proxy.proxy_server.master_key", "key" + ): + await byok_token( + request=mock_request, + grant_type="authorization_code", + code=code, + redirect_uri="", + code_verifier=verifier, + client_id="u", + ) + assert exc_info.value.status_code == 400 + + +@pytest.mark.asyncio +async def test_token_endpoint_wrong_verifier(): + verifier = "correct_verifier_value_that_is_long_enough" + challenge = _make_challenge(verifier) + code = _insert_code( + api_key="key", + server_id="s", + user_id="u", + challenge=challenge, + redirect_uri="https://cb", + ) + + from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import byok_token + + mock_request = MagicMock() + with pytest.raises(HTTPException) as exc_info: + with patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), patch( + "litellm.proxy.proxy_server.master_key", "key" + ): + await byok_token( + request=mock_request, + grant_type="authorization_code", + code=code, + redirect_uri="", + code_verifier="wrong_verifier_value_that_wont_match", + client_id="u", + ) + assert exc_info.value.status_code == 400 + assert "invalid_grant" in str(exc_info.value.detail) + + +@pytest.mark.asyncio +async def test_token_endpoint_unsupported_grant_type(): + from litellm.proxy._experimental.mcp_server.byok_oauth_endpoints import byok_token + + mock_request = MagicMock() + with pytest.raises(HTTPException) as exc_info: + with patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), patch( + "litellm.proxy.proxy_server.master_key", "key" + ): + await byok_token( + request=mock_request, + grant_type="client_credentials", + code="any", + redirect_uri="", + code_verifier="v", + client_id="u", + ) + assert exc_info.value.status_code == 400 + assert "unsupported_grant_type" in str(exc_info.value.detail) + + +# --------------------------------------------------------------------------- +# _check_byok_credential (the 401 challenge in execute_mcp_tool) +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_check_byok_credential_not_byok(): + """Non-BYOK servers should pass through without any DB check.""" + from litellm.proxy._experimental.mcp_server.server import _check_byok_credential + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + server = MCPServer( + server_id="s1", + name="normal-server", + transport=MCPTransport.http, + is_byok=False, + ) + # Should not raise + await _check_byok_credential(server, None) + + +@pytest.mark.asyncio +async def test_check_byok_credential_no_user_id(): + """BYOK server with no user identity → 401.""" + from litellm.proxy._experimental.mcp_server.server import _check_byok_credential + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + server = MCPServer( + server_id="byok-1", + name="byok-server", + transport=MCPTransport.http, + is_byok=True, + ) + with pytest.raises(HTTPException) as exc_info: + await _check_byok_credential(server, None) + + assert exc_info.value.status_code == 401 + assert "WWW-Authenticate" in (exc_info.value.headers or {}) # type: ignore[operator] + assert "byok_auth_required" in str(exc_info.value.detail) + + +@pytest.mark.asyncio +async def test_check_byok_credential_missing_credential(): + """BYOK server with a known user but no stored credential → 401.""" + from litellm.proxy._experimental.mcp_server.server import _check_byok_credential + from litellm.proxy._types import UserAPIKeyAuth + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + server = MCPServer( + server_id="byok-2", + name="byok-server", + transport=MCPTransport.http, + is_byok=True, + ) + user_auth = UserAPIKeyAuth(user_id="user-99", api_key="sk-test") + + mock_prisma = MagicMock() + + with patch( + "litellm.proxy._experimental.mcp_server.db.get_user_credential", + new=AsyncMock(return_value=None), + ), patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): + ), patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): + with pytest.raises(HTTPException) as exc_info: + await _check_byok_credential(server, user_auth) + + assert exc_info.value.status_code == 401 + detail: Any = exc_info.value.detail + assert detail["error"] == "byok_auth_required" + assert detail["server_id"] == "byok-2" + headers = exc_info.value.headers or {} + assert "WWW-Authenticate" in headers # type: ignore[operator] + assert "oauth-protected-resource" in headers["WWW-Authenticate"] # type: ignore[index] + + +@pytest.mark.asyncio +async def test_check_byok_credential_has_credential(): + """BYOK server with a valid stored credential → no error raised.""" + from litellm.proxy._experimental.mcp_server.server import _check_byok_credential + from litellm.proxy._types import UserAPIKeyAuth + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + server = MCPServer( + server_id="byok-3", + name="byok-server", + transport=MCPTransport.http, + is_byok=True, + ) + user_auth = UserAPIKeyAuth(user_id="user-77", api_key="sk-test") + + mock_prisma = MagicMock() + + with patch( + "litellm.proxy._experimental.mcp_server.db.get_user_credential", + new=AsyncMock(return_value="some-credential-value"), + ), patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): + ), patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): + # Should not raise + await _check_byok_credential(server, user_auth) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index b3829d0a8f4..dbc1c4d10e2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -111,6 +111,8 @@ const routeFor = (slug: string): string => { return "tools/mcp-servers"; case "vector-stores": return "tools/vector-stores"; + case "byok-demo": + return "tools/byok-demo"; // experimental case "caching": diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx new file mode 100644 index 00000000000..473918c1267 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx @@ -0,0 +1,254 @@ +"use client"; + +import React, { useState } from "react"; +import { Modal, Input, Switch, message } from "antd"; +import { + KeyOutlined, + LockOutlined, + CheckOutlined, + ArrowRightOutlined, + ArrowLeftOutlined, + CloseOutlined, + LinkOutlined, +} from "@ant-design/icons"; +import { MCPServer } from "./types"; + +interface ByokCredentialModalProps { + server: MCPServer; + open: boolean; + onClose: () => void; + onSuccess: (serverId: string) => void; + accessToken: string; +} + +export const ByokCredentialModal: React.FC = ({ + server, + open, + onClose, + onSuccess, + accessToken, +}) => { + const [step, setStep] = useState<1 | 2>(1); + const [apiKey, setApiKey] = useState(""); + const [saveKey, setSaveKey] = useState(true); + const [loading, setLoading] = useState(false); + + const serverDisplayName = server.alias || server.server_name || "Service"; + const firstLetter = serverDisplayName.charAt(0).toUpperCase(); + + const handleClose = () => { + setStep(1); + setApiKey(""); + setSaveKey(true); + setLoading(false); + onClose(); + }; + + const handleAuthorize = async () => { + if (!apiKey.trim()) { + message.error("Please enter your API key"); + return; + } + setLoading(true); + try { + const response = await fetch(`/v1/mcp/server/${server.server_id}/user-credential`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ credential: apiKey.trim(), save: saveKey }), + }); + if (!response.ok) { + const err = await response.json(); + throw new Error(err?.detail?.error || "Failed to save credential"); + } + message.success(`Connected to ${serverDisplayName}`); + onSuccess(server.server_id); + handleClose(); + } catch (e: any) { + message.error(e.message || "Failed to connect"); + } finally { + setLoading(false); + } + }; + + return ( + +
    + {/* Step dots + close */} +
    + {step === 2 ? ( + + ) : ( +
    + )} +
    +
    +
    +
    + +
    + + {step === 1 ? ( +
    + {/* Logos */} +
    +
    + L +
    + +
    + {firstLetter} +
    +
    + +

    Connect {serverDisplayName}

    +

    + LiteLLM needs access to {serverDisplayName} to complete your request. +

    + + {/* How it works */} +
    +
    +
    + + + + +
    +
    +

    How it works

    +

    + LiteLLM acts as a secure bridge. Your requests are routed through our MCP client directly to{" "} + {serverDisplayName}'s API. +

    +
    +
    +
    + + {/* Requested access */} + {server.byok_description && server.byok_description.length > 0 && ( +
    +

    + + + + + Requested Access +

    +
      + {server.byok_description.map((item, i) => ( +
    • + + {item} +
    • + ))} +
    +
    + )} + + + +
    + ) : ( +
    + {/* Key icon */} +
    + +
    + +

    Provide API Key

    +

    + Enter your {serverDisplayName} API key to authorize this connection. +

    + +
    + + setApiKey(e.target.value)} + size="large" + className="rounded-lg" + /> + {server.byok_api_key_help_url && ( + + Where do I find my API key? + + )} +
    + + {/* Save toggle */} +
    +
    + + + + Save key for future use +
    + +
    + + {/* Security note */} +
    + +

    + Your key is stored securely and transmitted over HTTPS. It is never shared with third parties. +

    +
    + + +
    + )} +
    + + ); +}; + +export default ByokCredentialModal; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 6dbb18887da..6ca58ffae24 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Modal, Tooltip, Form, Select, Input } from "antd"; +import { Modal, Tooltip, Form, Select, Input, Switch } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Button, TextInput } from "@tremor/react"; import { createMCPServer } from "../networking"; @@ -624,6 +624,89 @@ const CreateMCPServer: React.FC = ({ )} + {/* BYOK toggle - only for OpenAPI */} + {transportType === TRANSPORT.OPENAPI && ( + <> + + BYOK (Bring Your Own Key) + + + + + } + name="is_byok" + valuePropName="checked" + > + + + + prev.is_byok !== cur.is_byok || prev.auth_type !== cur.auth_type}> + {({ getFieldValue }) => + getFieldValue("is_byok") ? ( + <> + {/* Auth format hint */} + {getFieldValue("auth_type") && getFieldValue("auth_type") !== "none" && ( +
    + + + User keys will be sent as:{" "} + + {getFieldValue("auth_type") === "bearer_token" && "Authorization: Bearer {key}"} + {getFieldValue("auth_type") === "api_key" && "x-api-key: {key}"} + {getFieldValue("auth_type") === "basic" && "Authorization: Basic {key}"} + {getFieldValue("auth_type") === "authorization" && "Authorization: {key}"} + + {!getFieldValue("auth_type") && "Set Authentication Type below to specify the format."} + +
    + )} + {!getFieldValue("auth_type") && ( +
    + + Set the Authentication Type below to specify how user keys are sent (e.g., Bearer Token, API Key header). +
    + )} + + Access Description + + + + + } + name="byok_description" + > + + + + ) : null + } +
    + + )} + {/* Authentication - show for HTTP, SSE, and OpenAPI */} {transportType !== "stdio" && transportType !== "" && ( void, onDelete: (serverId: string) => void, isLoadingHealth?: boolean, + onByokConnect?: (server: MCPServer) => void, ): ColumnDef[] => [ { accessorKey: "server_id", @@ -192,6 +194,41 @@ export const mcpServerColumns = ( ); }, }, + { + id: "byok_credential", + header: "Credential", + cell: ({ row }) => { + const server = row.original; + if (!server.is_byok) { + return —; + } + if (server.has_user_credential) { + return ( +
    + + Connected + + {onByokConnect && ( + + )} +
    + ); + } + return onByokConnect ? ( + + ) : null; + }, + }, { id: "actions", header: "Actions", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index 0f87f5e87b8..f48649d6653 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -16,6 +16,7 @@ import { DiscoverableMCPServer, MCPServer, MCPServerProps, Team } from "./types" import MCPSemanticFilterSettings from "../Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings"; import MCPNetworkSettings from "./MCPNetworkSettings"; import MCPDiscovery from "./mcp_discovery"; +import { ByokCredentialModal } from "./ByokCredentialModal"; const { Text: AntdText, Title: AntdTitle } = Typography; const EDIT_OAUTH_UI_STATE_KEY = "litellm-mcp-oauth-edit-state"; @@ -70,6 +71,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) const [isDiscoveryVisible, setDiscoveryVisible] = useState(false); const [prefillData, setPrefillData] = useState(null); const [isDeletingServer, setIsDeletingServer] = useState(false); + const [byokModalServer, setByokModalServer] = useState(null); const isInternalUser = userRole === "Internal User"; useEffect(() => { @@ -170,6 +172,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) }, handleDelete, isLoadingHealth, + (server: MCPServer) => setByokModalServer(server), ), [userRole, isLoadingHealth], ); @@ -427,6 +430,19 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) + + {byokModalServer && ( + setByokModalServer(null)} + onSuccess={(_serverId) => { + refetch(); + setByokModalServer(null); + }} + accessToken={accessToken || ""} + /> + )}
    ); }; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 8a08f13e22a..6ba25012197 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -178,6 +178,12 @@ export interface MCPServer { command?: string | null; args?: string[] | null; env?: Record | null; + + /** BYOK (Bring Your Own Key) fields */ + is_byok?: boolean | null; + byok_description?: string[] | null; + byok_api_key_help_url?: string | null; + has_user_credential?: boolean | null; } export interface MCPServerProps { diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx index 3272e9b589a..9936f34452d 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -33,6 +33,7 @@ import GuardrailSelector from "../../guardrails/GuardrailSelector"; import PolicySelector from "../../policies/PolicySelector"; import MCPToolArgumentsForm, { MCPToolArgumentsFormRef } from "../../mcp_tools/MCPToolArgumentsForm"; import { MCPServer } from "../../mcp_tools/types"; +import { ByokCredentialModal } from "../../mcp_tools/ByokCredentialModal"; import NotificationsManager from "../../molecules/notifications_manager"; import { callMCPTool, fetchMCPServers, listMCPTools } from "../../networking"; import TagSelector from "../../tag_management/TagSelector"; @@ -108,6 +109,7 @@ const ChatUI: React.FC = ({ fixedModel, }) => { const [mcpServers, setMCPServers] = useState([]); + const [byokModalServer, setByokModalServer] = useState(null); const [selectedMCPServers, setSelectedMCPServers] = useState(() => { const saved = sessionStorage.getItem("selectedMCPServers"); try { @@ -1746,6 +1748,49 @@ const ChatUI: React.FC = ({ })}
    )} + + {/* BYOK credential status for selected servers */} + {selectedMCPServers.length > 0 && + !selectedMCPServers.includes("__all__") && + selectedMCPServers.some((serverId) => { + const server = mcpServers.find((s) => s.server_id === serverId); + return server?.is_byok; + }) && ( +
    + {selectedMCPServers.map((serverId) => { + const server = mcpServers.find((s) => s.server_id === serverId); + if (!server?.is_byok) return null; + const serverName = server.alias || server.server_name || serverId; + return ( +
    + + {serverName} requires your API key + + {server.has_user_credential ? ( +
    + + Connected + + +
    + ) : ( + + )} +
    + ); + })} +
    + )}
    @@ -2498,6 +2543,20 @@ const ChatUI: React.FC = ({ {generatedCode} + + {byokModalServer && ( + setByokModalServer(null)} + onSuccess={(_serverId) => { + // Refresh MCP servers to pick up updated has_user_credential + loadMCPServers(); + setByokModalServer(null); + }} + accessToken={accessToken || ""} + /> + )}
    ); };