diff --git a/.circleci/config.yml b/.circleci/config.yml index 32983c92e27..3139bd3cb26 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -158,6 +158,8 @@ jobs: CHOCOLATEY_CONFIRM_ALL: "true" - run: name: Install Dependencies + environment: + UV_HTTP_TIMEOUT: "300" command: | $installer = Join-Path $env:TEMP "uv-install.ps1" Invoke-WebRequest -Uri https://astral.sh/uv/0.10.9/install.ps1 -OutFile $installer diff --git a/.github/workflows/test-unit-proxy-mgmt-behavior.yml b/.github/workflows/test-unit-proxy-mgmt-behavior.yml new file mode 100644 index 00000000000..e73997323a4 --- /dev/null +++ b/.github/workflows/test-unit-proxy-mgmt-behavior.yml @@ -0,0 +1,34 @@ +name: "Unit Tests: Proxy Management-Endpoint Behavior Pinning" + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_branch + - "litellm_**" + +permissions: + contents: read + id-token: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + proxy-mgmt-behavior: + uses: ./.github/workflows/_test-unit-services-base.yml + with: + test-path: tests/proxy_behavior + # workers=0 (no xdist): the world seed is a single shared Postgres + # state — two xdist workers both call seed_world() and race on the + # ``behavior-pin-budget`` row, producing UniqueViolation + cascading + # missing-membership FK failures. The whole suite is ~7s sequentially, + # so the cost of disabling parallelism here is negligible. + workers: 0 + reruns: 0 + enable-postgres: true + artifact-name: proxy-mgmt-behavior + timeout-minutes: 15 diff --git a/litellm/__init__.py b/litellm/__init__.py index 1e8f8613fba..d8d48b5865f 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -225,6 +225,10 @@ use_chat_completions_url_for_anthropic_messages: bool = bool( route_all_chat_openai_to_responses: bool = ( os.getenv("LITELLM_ROUTE_ALL_CHAT_OPENAI_TO_RESPONSES", "false").lower() == "true" ) # When True, routes all OpenAI /chat/completions requests through the Responses API bridge +use_legacy_interactions_schema: bool = ( + os.getenv("LITELLM_USE_LEGACY_INTERACTIONS_SCHEMA", "false").lower() == "true" +) # When True, sends Api-Revision: 2026-05-07 to Google so responses use the legacy `outputs` +# schema instead of the new `steps` schema. Remove this flag after June 8, 2026. retry = True ### AUTH ### api_key: Optional[str] = None diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index a70574952b8..e1a3cecfce5 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -1611,11 +1611,11 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): "masked_entity_count", safe_dumps(masked_entity_count) ) - self.safe_set_attribute( - span=guardrail_span, - key="guardrail_response", - value=guardrail_information.get("guardrail_response"), - ) + guardrail_response = guardrail_information.get("guardrail_response") + if guardrail_response is not None: + guardrail_span.set_attribute( + "guardrail_response", safe_dumps(guardrail_response) + ) self._set_team_attributes_from_kwargs(guardrail_span, kwargs) diff --git a/litellm/interactions/litellm_responses_transformation/streaming_iterator.py b/litellm/interactions/litellm_responses_transformation/streaming_iterator.py index 567e9b523e8..4a3eb63084e 100644 --- a/litellm/interactions/litellm_responses_transformation/streaming_iterator.py +++ b/litellm/interactions/litellm_responses_transformation/streaming_iterator.py @@ -2,7 +2,17 @@ Streaming iterator for transforming Responses API stream to Interactions API stream. """ -from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, cast +from collections import deque +from typing import ( + Any, + AsyncIterator, + Deque, + Dict, + Iterator, + List, + Optional, + cast, +) from litellm.responses.streaming_iterator import ( BaseResponsesAPIStreamingIterator, @@ -15,7 +25,6 @@ from litellm.types.interactions import ( InteractionsAPIStreamingResponse, ) from litellm.types.llms.openai import ( - ContentPartAddedEvent, OutputTextDeltaEvent, ResponseCompletedEvent, ResponseCreatedEvent, @@ -30,7 +39,13 @@ class LiteLLMResponsesInteractionsStreamingIterator: This class handles both sync and async iteration, transforming Responses API streaming events (output.text.delta, response.completed, etc.) to Interactions - API streaming events (content.delta, interaction.complete, etc.). + API streaming events. + + Schema selection: + - New schema (default, use_legacy_interactions_schema=False): + interaction.created -> step.start -> step.delta ... -> step.stop -> interaction.completed + - Legacy schema (use_legacy_interactions_schema=True, remove after June 8 2026): + interaction.start -> content.start -> content.delta ... -> content.stop -> interaction.complete """ def __init__( @@ -42,6 +57,8 @@ class LiteLLMResponsesInteractionsStreamingIterator: custom_llm_provider: Optional[str] = None, litellm_metadata: Optional[Dict[str, Any]] = None, ): + import litellm + self.model = model self.responses_stream_iterator = litellm_custom_stream_wrapper self.request_input = request_input @@ -52,100 +69,156 @@ class LiteLLMResponsesInteractionsStreamingIterator: self.collected_text = "" self.sent_interaction_start = False self.sent_content_start = False - self._pending_events: List[InteractionsAPIStreamingResponse] = [] + # Capture the schema flag once at construction time so all events + # emitted by this stream use a consistent schema, even if the global + # flag is mutated mid-stream (e.g. by a config reload). + self._use_legacy: bool = litellm.use_legacy_interactions_schema + # Buffer of events that have been derived from upstream chunks but not + # yet returned to the caller. A single Responses API chunk may expand + # into multiple Interactions API events (e.g. the first text delta + # produces interaction.created + step.start + step.delta), and the + # terminal sequence on stream end may also span multiple events + # (step.stop + interaction.completed). + self._pending_events: Deque[InteractionsAPIStreamingResponse] = deque() + # Tracks whether we've already emitted a terminal completion event so + # the StopIteration fallback path doesn't double-emit. + self._sent_completion_event = False + # ID resolved from the first upstream chunk (item_id on a text delta or + # response.id on response.created). Persisted so the EOF terminal + # events stay correlated with the start events delivered earlier. + self._interaction_id: Optional[str] = None - def _transform_responses_chunk_to_interactions_chunk( - self, - responses_chunk: ResponsesAPIStreamingResponse, - ) -> Optional[InteractionsAPIStreamingResponse]: + # ------------------------------------------------------------------ + # Event builders + # ------------------------------------------------------------------ + + def _build_interaction_start_event( + self, interaction_id: str + ) -> InteractionsAPIStreamingResponse: + event_type = "interaction.start" if self._use_legacy else "interaction.created" + return InteractionsAPIStreamingResponse( + event_type=event_type, + id=interaction_id, + object="interaction", + status="in_progress", + model=self.model, + ) + + def _build_content_start_event( + self, interaction_id: str + ) -> InteractionsAPIStreamingResponse: + if self._use_legacy: + return InteractionsAPIStreamingResponse( + event_type="content.start", + id=interaction_id, + object="content", + delta={"type": "text", "text": ""}, + ) + return InteractionsAPIStreamingResponse( + event_type="step.start", + index=0, + step={"type": "model_output", "content": []}, + ) + + def _build_text_delta_event( + self, interaction_id: str, delta_text: str + ) -> InteractionsAPIStreamingResponse: + if self._use_legacy: + return InteractionsAPIStreamingResponse( + event_type="content.delta", + id=interaction_id, + object="content", + delta={"type": "text", "text": delta_text}, + ) + return InteractionsAPIStreamingResponse( + event_type="step.delta", + index=0, + delta={"type": "text", "text": delta_text}, + ) + + def _build_content_stop_event( + self, interaction_id: Optional[str] + ) -> InteractionsAPIStreamingResponse: + if self._use_legacy: + return InteractionsAPIStreamingResponse( + event_type="content.stop", + id=interaction_id, + object="content", + delta={"type": "text", "text": self.collected_text}, + ) + return InteractionsAPIStreamingResponse( + event_type="step.stop", + index=0, + ) + + def _build_completion_event( + self, response_id: str + ) -> InteractionsAPIStreamingResponse: + if self._use_legacy: + return InteractionsAPIStreamingResponse( + event_type="interaction.complete", + id=response_id, + object="interaction", + status="completed", + model=self.model, + outputs=[{"type": "text", "text": self.collected_text}], + ) + return InteractionsAPIStreamingResponse( + event_type="interaction.completed", + id=response_id, + object="interaction", + status="completed", + model=self.model, + steps=[ + { + "type": "model_output", + "content": [{"type": "text", "text": self.collected_text}], + } + ], + ) + + # ------------------------------------------------------------------ + # Per-chunk transform (returns a list of events to enqueue) + # ------------------------------------------------------------------ + + def _events_for_chunk( + self, responses_chunk: ResponsesAPIStreamingResponse + ) -> List[InteractionsAPIStreamingResponse]: """ - Transform a Responses API streaming chunk to an Interactions API streaming chunk. + Translate a single upstream Responses API chunk into the list of + Interactions API events it should produce. - Responses API events: - - output.text.delta -> content.delta - - response.completed -> interaction.complete - - Interactions API events: - - interaction.start - - content.start - - content.delta - - content.stop - - interaction.complete + Returning a list (rather than a single event) lets a chunk emit any + synthetic start events that haven't been sent yet *together with* the + actual delta event, so we never silently drop the chunk's payload. """ if not responses_chunk: - return None + return [] - # Handle OutputTextDeltaEvent -> content.delta + # Text delta: emit any missing start events, then the delta itself. if isinstance(responses_chunk, OutputTextDeltaEvent): delta_text = ( responses_chunk.delta if isinstance(responses_chunk.delta, str) else "" ) self.collected_text += delta_text - - # Fallback: emit interaction.start, and queue content.start carrying this - # delta so the first token is preserved in the stream. - if not self.sent_interaction_start: - self.sent_interaction_start = True - self.sent_content_start = True - self._pending_events.append( - InteractionsAPIStreamingResponse( - event_type="content.start", - id=getattr(responses_chunk, "item_id", None), - object="content", - delta={"type": "text", "text": delta_text}, - ) - ) - return InteractionsAPIStreamingResponse( - event_type="interaction.start", - id=getattr(responses_chunk, "item_id", None) - or f"interaction_{id(self)}", - object="interaction", - status="in_progress", - model=self.model, - ) - - # Fallback: emit content.start if ContentPartAddedEvent never arrived - if not self.sent_content_start: - self.sent_content_start = True - return InteractionsAPIStreamingResponse( - event_type="content.start", - id=getattr(responses_chunk, "item_id", None), - object="content", - delta={"type": "text", "text": delta_text}, - ) - - # Normal path: emit content.delta with type field - return InteractionsAPIStreamingResponse( - event_type="content.delta", - id=getattr(responses_chunk, "item_id", None), - object="content", - delta={"type": "text", "text": delta_text}, + interaction_id = ( + getattr(responses_chunk, "item_id", None) or f"interaction_{id(self)}" ) + if self._interaction_id is None: + self._interaction_id = interaction_id - # Handle ContentPartAddedEvent -> content.start (arrives before text deltas) - if isinstance(responses_chunk, ContentPartAddedEvent): - # Fallback: emit interaction.start if ResponseCreatedEvent never arrived + events: List[InteractionsAPIStreamingResponse] = [] if not self.sent_interaction_start: self.sent_interaction_start = True - return InteractionsAPIStreamingResponse( - event_type="interaction.start", - id=getattr(responses_chunk, "item_id", None) - or f"interaction_{id(self)}", - object="interaction", - status="in_progress", - model=self.model, - ) + events.append(self._build_interaction_start_event(interaction_id)) if not self.sent_content_start: self.sent_content_start = True - return InteractionsAPIStreamingResponse( - event_type="content.start", - id=getattr(responses_chunk, "item_id", None), - object="content", - delta={"type": "text", "text": ""}, - ) - return None + events.append(self._build_content_start_event(interaction_id)) + events.append(self._build_text_delta_event(interaction_id, delta_text)) + return events - # Handle ResponseCreatedEvent or ResponseInProgressEvent -> interaction.start + # Response created / in-progress: synthesize interaction start if we + # haven't already sent one. if isinstance(responses_chunk, (ResponseCreatedEvent, ResponseInProgressEvent)): if not self.sent_interaction_start: self.sent_interaction_start = True @@ -153,177 +226,136 @@ class LiteLLMResponsesInteractionsStreamingIterator: getattr(responses_chunk.response, "id", None) if hasattr(responses_chunk, "response") else None - ) - return InteractionsAPIStreamingResponse( - event_type="interaction.start", - id=response_id or f"interaction_{id(self)}", - object="interaction", - status="in_progress", - model=self.model, - ) + ) or f"interaction_{id(self)}" + if self._interaction_id is None: + self._interaction_id = response_id + return [self._build_interaction_start_event(response_id)] + return [] - # Handle ResponseCompletedEvent -> interaction.complete + # Response completed: emit step.stop (if content was started) followed + # by the terminal completion event. Prefer the interaction id already + # established by earlier events so consumers can correlate the start + # and completion events by id (response.id may differ from the item_id + # used to derive the initial id when the stream starts directly with a + # text delta). if isinstance(responses_chunk, ResponseCompletedEvent): self.finished = True response = responses_chunk.response - - # Send content.stop first if content was started - if self.sent_content_start: - # Note: We'll send this in the iterator, not here - pass - - # Send interaction.complete - return InteractionsAPIStreamingResponse( - event_type="interaction.complete", - id=getattr(response, "id", None) or f"interaction_{id(self)}", - object="interaction", - status="completed", - model=self.model, - outputs=[ - { - "type": "text", - "text": self.collected_text, - } - ], + response_id = ( + self._interaction_id + or getattr(response, "id", None) + or f"interaction_{id(self)}" ) - # For other event types, return None (skip) - return None + terminal: List[InteractionsAPIStreamingResponse] = [] + if self.sent_content_start: + terminal.append(self._build_content_stop_event(response_id)) + terminal.append(self._build_completion_event(response_id)) + self._sent_completion_event = True + return terminal + + return [] + + def _build_terminal_events_on_eof( + self, + ) -> List[InteractionsAPIStreamingResponse]: + """ + Build the events to flush when the upstream stream ends without a + ResponseCompletedEvent. Ensures consumers always observe a terminal + interaction.completed/interaction.complete carrying the full text. + """ + if self._sent_completion_event: + return [] + + fallback_id = self._interaction_id or f"interaction_{id(self)}" + terminal: List[InteractionsAPIStreamingResponse] = [] + if self.sent_content_start: + terminal.append(self._build_content_stop_event(fallback_id)) + if self.sent_interaction_start or self.collected_text: + terminal.append(self._build_completion_event(fallback_id)) + self._sent_completion_event = True + return terminal + + # ------------------------------------------------------------------ + # Iteration + # ------------------------------------------------------------------ def __iter__(self) -> Iterator[InteractionsAPIStreamingResponse]: - """Sync iterator implementation.""" return self def __next__(self) -> InteractionsAPIStreamingResponse: - """Get next chunk in sync mode.""" + if self._pending_events: + return self._pending_events.popleft() + if self.finished: raise StopIteration - # Check if we have a pending interaction.complete to send - if hasattr(self, "_pending_interaction_complete"): - pending: InteractionsAPIStreamingResponse = getattr( - self, "_pending_interaction_complete" - ) - delattr(self, "_pending_interaction_complete") - return pending - - # Drain events queued from a prior chunk (e.g. content.start emitted alongside - # the interaction.start fallback for the first OutputTextDeltaEvent). - if self._pending_events: - return self._pending_events.pop(0) - # Use a loop instead of recursion to avoid stack overflow sync_iterator = cast( SyncResponsesAPIStreamingIterator, self.responses_stream_iterator ) while True: try: - # Get next chunk from responses API stream chunk = next(sync_iterator) - - # Transform chunk (chunk is already a ResponsesAPIStreamingResponse) - transformed = self._transform_responses_chunk_to_interactions_chunk( - chunk - ) - - if transformed: - # If we finished and content was started, send content.stop before interaction.complete - if ( - self.finished - and self.sent_content_start - and transformed.event_type == "interaction.complete" - ): - # Send content.stop first - content_stop = InteractionsAPIStreamingResponse( - event_type="content.stop", - id=transformed.id, - object="content", - delta={"type": "text", "text": self.collected_text}, - ) - # Store the interaction.complete to send next - self._pending_interaction_complete = transformed - return content_stop - return transformed - - # If no transformation, continue to next chunk (loop continues) - except StopIteration: self.finished = True + self._pending_events.extend(self._build_terminal_events_on_eof()) + if self._pending_events: + return self._pending_events.popleft() + raise - # Send final events if needed - if self.sent_content_start: - return InteractionsAPIStreamingResponse( - event_type="content.stop", - object="content", - delta={"type": "text", "text": self.collected_text}, - ) - - raise StopIteration + events = self._events_for_chunk(chunk) + if events: + self._pending_events.extend(events) + return self._pending_events.popleft() def __aiter__(self) -> AsyncIterator[InteractionsAPIStreamingResponse]: - """Async iterator implementation.""" return self async def __anext__(self) -> InteractionsAPIStreamingResponse: - """Get next chunk in async mode.""" + if self._pending_events: + return self._pending_events.popleft() + if self.finished: raise StopAsyncIteration - # Check if we have a pending interaction.complete to send - if hasattr(self, "_pending_interaction_complete"): - pending: InteractionsAPIStreamingResponse = getattr( - self, "_pending_interaction_complete" - ) - delattr(self, "_pending_interaction_complete") - return pending - - # Drain events queued from a prior chunk (e.g. content.start emitted alongside - # the interaction.start fallback for the first OutputTextDeltaEvent). - if self._pending_events: - return self._pending_events.pop(0) - # Use a loop instead of recursion to avoid stack overflow async_iterator = cast( ResponsesAPIStreamingIterator, self.responses_stream_iterator ) while True: try: - # Get next chunk from responses API stream chunk = await async_iterator.__anext__() - - # Transform chunk (chunk is already a ResponsesAPIStreamingResponse) - transformed = self._transform_responses_chunk_to_interactions_chunk( - chunk - ) - - if transformed: - # If we finished and content was started, send content.stop before interaction.complete - if ( - self.finished - and self.sent_content_start - and transformed.event_type == "interaction.complete" - ): - # Send content.stop first - content_stop = InteractionsAPIStreamingResponse( - event_type="content.stop", - id=transformed.id, - object="content", - delta={"type": "text", "text": self.collected_text}, - ) - # Store the interaction.complete to send next - self._pending_interaction_complete = transformed - return content_stop - return transformed - - # If no transformation, continue to next chunk (loop continues) - except StopAsyncIteration: self.finished = True + self._pending_events.extend(self._build_terminal_events_on_eof()) + if self._pending_events: + return self._pending_events.popleft() + raise - # Send final events if needed - if self.sent_content_start: - return InteractionsAPIStreamingResponse( - event_type="content.stop", - object="content", - delta={"type": "text", "text": self.collected_text}, - ) + events = self._events_for_chunk(chunk) + if events: + self._pending_events.extend(events) + return self._pending_events.popleft() - raise StopAsyncIteration + # ------------------------------------------------------------------ + # Backwards-compatible single-chunk transform (used by tests and any + # external callers that drove the iterator chunk-by-chunk pre-fix). + # ------------------------------------------------------------------ + + def _transform_responses_chunk_to_interactions_chunk( + self, + responses_chunk: ResponsesAPIStreamingResponse, + ) -> Optional[InteractionsAPIStreamingResponse]: + """ + Compatibility shim: returns the *first* event produced for this chunk + and queues any remaining events on ``self._pending_events`` so they + are surfaced on subsequent calls/iterations. + + Prefer ``_events_for_chunk`` in new code. + """ + events = self._events_for_chunk(responses_chunk) + if not events: + return None + first = events[0] + if len(events) > 1: + self._pending_events.extend(events[1:]) + return first diff --git a/litellm/interactions/litellm_responses_transformation/transformation.py b/litellm/interactions/litellm_responses_transformation/transformation.py index 100300af7b5..173d4ca8764 100644 --- a/litellm/interactions/litellm_responses_transformation/transformation.py +++ b/litellm/interactions/litellm_responses_transformation/transformation.py @@ -226,29 +226,37 @@ class LiteLLMResponsesInteractionsConfig: - Map status - Extract usage """ - # Extract text from outputs - outputs = [] + # Extract text from outputs and build both `outputs` (legacy) and `steps` (new schema). + outputs: List[Dict[str, Any]] = [] + steps: List[Dict[str, Any]] = [] if hasattr(responses_response, "output") and responses_response.output: for output_item in responses_response.output: # Use getattr with None default to safely access content content = getattr(output_item, "content", None) if content is not None: content_items = content if isinstance(content, list) else [content] + model_output_contents: List[Dict[str, Any]] = [] for content_item in content_items: # Check if content_item has text attribute text = getattr(content_item, "text", None) if text is not None: - outputs.append( - { - "type": "text", - "text": text, - } - ) + # Use independent dict instances so mutations to one + # of `outputs` / `steps` don't leak into the other. + outputs.append({"type": "text", "text": text}) + model_output_contents.append({"type": "text", "text": text}) elif ( isinstance(content_item, dict) and content_item.get("type") == "text" ): - outputs.append(content_item) + outputs.append({**content_item}) + model_output_contents.append({**content_item}) + if model_output_contents: + steps.append( + { + "type": "model_output", + "content": model_output_contents, + } + ) # Convert created_at to ISO string created_at = getattr(responses_response, "created_at", None) @@ -270,12 +278,14 @@ class LiteLLMResponsesInteractionsConfig: else: interactions_status = status - # Build interactions response + # Build interactions response — populate both `outputs` (legacy schema) and + # `steps` (new schema) so callers work regardless of which schema they expect. interactions_response_dict: Dict[str, Any] = { "id": getattr(responses_response, "id", ""), "object": "interaction", "status": interactions_status, "outputs": outputs, + "steps": steps, "model": model or getattr(responses_response, "model", ""), "created": created, } diff --git a/litellm/interactions/streaming_iterator.py b/litellm/interactions/streaming_iterator.py index a5a7f9e06e5..561686a3e1b 100644 --- a/litellm/interactions/streaming_iterator.py +++ b/litellm/interactions/streaming_iterator.py @@ -101,10 +101,14 @@ class BaseInteractionsAPIStreamingIterator: ) ) - # Store the completed response (check for status=completed) - if ( - streaming_response - and getattr(streaming_response, "status", None) == "completed" + # Store the completed response. + # Legacy schema signals completion via status="completed". + # New schema (Api-Revision: 2026-05-20) uses event_type="interaction.completed". + # Remove the legacy check after June 8, 2026. + if streaming_response and ( + getattr(streaming_response, "status", None) == "completed" + or getattr(streaming_response, "event_type", None) + == "interaction.completed" ): self.completed_response = streaming_response self._handle_logging_completed_response() diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index d6c011990c1..a29f5005570 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -1344,6 +1344,7 @@ def _get_dummy_thought_signature() -> str: def convert_to_gemini_tool_call_invoke( message: ChatCompletionAssistantMessage, model: Optional[str] = None, + custom_llm_provider: Optional[str] = None, ) -> List[VertexPartType]: """ OpenAI tool invokes: @@ -1394,7 +1395,10 @@ def convert_to_gemini_tool_call_invoke( ) forward_tool_call_id = bool( - model and VertexGeminiConfig._is_gemini_3_or_newer(model) + model + and VertexGeminiConfig._forward_gemini_function_call_id( + model, custom_llm_provider + ) ) if tool_calls is not None: @@ -1475,6 +1479,7 @@ def convert_to_gemini_tool_call_result( # noqa: PLR0915 message: Union[ChatCompletionToolMessage, ChatCompletionFunctionMessage], last_message_with_tool_calls: Optional[dict], model: Optional[str] = None, + custom_llm_provider: Optional[str] = None, ) -> Union[VertexPartType, List[VertexPartType]]: """ OpenAI message with a tool result looks like: @@ -1616,14 +1621,16 @@ def convert_to_gemini_tool_call_result( # noqa: PLR0915 name = tool.get("function", {}).get("name", "") # Echo the OpenAI tool_call_id on functionResponse (strip thought-signature suffix). - # Only Gemini 3+ accepts (and returns) an `id` on function_response parts; - # older Gemini models reject the field with a 400. + # Only Google AI Studio Gemini 3+ accepts `id` on function_response parts. + # Vertex AI and older Gemini models reject the field with HTTP 400. from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( VertexGeminiConfig, ) gemini_call_id: Optional[str] = None - if model and VertexGeminiConfig._is_gemini_3_or_newer(model): + if model and VertexGeminiConfig._forward_gemini_function_call_id( + model, custom_llm_provider + ): raw_tool_call_id = message.get("tool_call_id") if raw_tool_call_id and isinstance(raw_tool_call_id, str): stripped_id = raw_tool_call_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1)[0] diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 1ce80207552..0b56eb86d9c 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1506,9 +1506,21 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params["metadata"] = {"user_id": value} elif param == "thinking": optional_params["thinking"] = value - elif param == "reasoning_effort" and isinstance(value, str): + elif param == "reasoning_effort": + # Accept both string ("low") and dict ({"effort": "low", + # "summary": "concise"}). The Responses->Chat parser keeps the + # full dict when `summary` is set (see #25359), so a dict here + # is the standard shape Otto/OpenAI-Responses-Bridge callers + # send. Coerce to the effort string before mapping — same + # shape-tolerance the GPT-5 path already implements in + # `_normalize_reasoning_effort_for_chat_completion`. + effort_value = value + if isinstance(effort_value, dict): + effort_value = effort_value.get("effort") + if not isinstance(effort_value, str): + continue mapped_thinking = AnthropicConfig._map_reasoning_effort( - reasoning_effort=value, + reasoning_effort=effort_value, model=model, llm_provider=self.custom_llm_provider or "anthropic", ) @@ -1519,12 +1531,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params["thinking"] = mapped_thinking if AnthropicConfig._is_adaptive_thinking_model(model): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( - value + effort_value ) if mapped_effort is None: AnthropicConfig._raise_invalid_reasoning_effort( model=model, - value=value, + value=effort_value, llm_provider=self.custom_llm_provider or "anthropic", ) optional_params["output_config"] = {"effort": mapped_effort} diff --git a/litellm/llms/azure/containers/transformation.py b/litellm/llms/azure/containers/transformation.py index 586b2e379a0..cd897511585 100644 --- a/litellm/llms/azure/containers/transformation.py +++ b/litellm/llms/azure/containers/transformation.py @@ -1,9 +1,16 @@ from typing import Optional +from urllib.parse import parse_qs, urlparse, urlunparse from litellm.llms.azure.common_utils import BaseAzureLLM from litellm.llms.openai.containers.transformation import OpenAIContainerConfig from litellm.types.router import GenericLiteLLMParams +# Endpoint-specific path suffixes that may appear in a deployment's api_base +# (e.g. the responses endpoint URL is stored as api_base for Azure models). +# Strip these before building the containers URL so we always start from the +# resource root (https://resource.cognitiveservices.azure.com). +_AZURE_ENDPOINT_PATHS = ("/openai/responses",) + class AzureContainerConfig(OpenAIContainerConfig): """ @@ -27,6 +34,27 @@ class AzureContainerConfig(OpenAIContainerConfig): litellm_params=GenericLiteLLMParams(api_key=api_key), ) + @staticmethod + def _normalize_api_base(api_base: Optional[str]) -> Optional[str]: + """Strip endpoint-specific path suffixes from api_base to get the resource root.""" + if not api_base: + return api_base + parsed = urlparse(api_base) + path = parsed.path.rstrip("/") + for ep in _AZURE_ENDPOINT_PATHS: + if path.endswith(ep): + return urlunparse( + (parsed.scheme, parsed.netloc, path[: -len(ep)], "", "", "") + ) + return api_base + + @staticmethod + def _extract_api_version(api_base: Optional[str]) -> Optional[str]: + """Return the api-version query param from api_base if present.""" + if not api_base: + return None + return parse_qs(urlparse(api_base).query).get("api-version", [None])[0] + def get_complete_url( self, api_base: Optional[str], @@ -39,10 +67,19 @@ class AzureContainerConfig(OpenAIContainerConfig): {endpoint}/openai/v1/containers when api_version is 'v1', 'latest', or 'preview'; otherwise: {endpoint}/openai/containers + + The deployment's api_base may be the responses endpoint URL + (e.g. .../openai/responses?api-version=2025-04-01-preview). We + prefer the api-version embedded there over the deployment's + api_version field, which may point to an older chat API version. """ + effective_params = dict(litellm_params) + api_version_from_base = self._extract_api_version(api_base) + if api_version_from_base: + effective_params["api_version"] = api_version_from_base return BaseAzureLLM._get_base_azure_url( - api_base=api_base, - litellm_params=litellm_params, + api_base=self._normalize_api_base(api_base), + litellm_params=effective_params, route="/openai/containers", default_api_version="v1", ) diff --git a/litellm/llms/custom_httpx/container_handler.py b/litellm/llms/custom_httpx/container_handler.py index 599cd705ebf..501390d840b 100644 --- a/litellm/llms/custom_httpx/container_handler.py +++ b/litellm/llms/custom_httpx/container_handler.py @@ -257,14 +257,19 @@ class GenericContainerHandler: returns_binary = endpoint_config.get("returns_binary", False) is_multipart = endpoint_config.get("is_multipart", False) + # An empty dict passed as `params` to httpx strips any existing query + # string from the URL (e.g. ?api-version=...). Use None instead so + # httpx leaves the URL's own query string intact. + effective_params = query_params or None + try: if method == "GET": response = http_client.get( - url=url, headers=headers, params=query_params + url=url, headers=headers, params=effective_params ) elif method == "DELETE": response = http_client.delete( - url=url, headers=headers, params=query_params + url=url, headers=headers, params=effective_params ) elif method == "POST": if is_multipart and "file" in kwargs: @@ -272,11 +277,11 @@ class GenericContainerHandler: kwargs["file"], headers ) response = http_client.post( - url=url, headers=headers, params=query_params, files=files + url=url, headers=headers, params=effective_params, files=files ) else: response = http_client.post( - url=url, headers=headers, params=query_params + url=url, headers=headers, params=effective_params ) else: raise ValueError(f"Unsupported HTTP method: {method}") @@ -376,14 +381,19 @@ class GenericContainerHandler: returns_binary = endpoint_config.get("returns_binary", False) is_multipart = endpoint_config.get("is_multipart", False) + # An empty dict passed as `params` to httpx strips any existing query + # string from the URL (e.g. ?api-version=...). Use None instead so + # httpx leaves the URL's own query string intact. + effective_params = query_params or None + try: if method == "GET": response = await http_client.get( - url=url, headers=headers, params=query_params + url=url, headers=headers, params=effective_params ) elif method == "DELETE": response = await http_client.delete( - url=url, headers=headers, params=query_params + url=url, headers=headers, params=effective_params ) elif method == "POST": if is_multipart and "file" in kwargs: @@ -391,11 +401,11 @@ class GenericContainerHandler: kwargs["file"], headers ) response = await http_client.post( - url=url, headers=headers, params=query_params, files=files + url=url, headers=headers, params=effective_params, files=files ) else: response = await http_client.post( - url=url, headers=headers, params=query_params + url=url, headers=headers, params=effective_params ) else: raise ValueError(f"Unsupported HTTP method: {method}") diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 2ff63cc2d7f..d2af0a3dd52 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -7834,7 +7834,7 @@ class BaseLLMHTTPHandler: response = sync_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_list_response( @@ -7911,7 +7911,7 @@ class BaseLLMHTTPHandler: response = await async_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_list_response( @@ -8001,7 +8001,7 @@ class BaseLLMHTTPHandler: response = sync_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_retrieve_response( @@ -8078,7 +8078,7 @@ class BaseLLMHTTPHandler: response = await async_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_retrieve_response( @@ -8168,7 +8168,7 @@ class BaseLLMHTTPHandler: response = sync_httpx_client.delete( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_delete_response( @@ -8245,7 +8245,7 @@ class BaseLLMHTTPHandler: response = await async_httpx_client.delete( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_delete_response( @@ -8341,7 +8341,7 @@ class BaseLLMHTTPHandler: response = sync_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_file_list_response( @@ -8420,7 +8420,7 @@ class BaseLLMHTTPHandler: response = await async_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_file_list_response( @@ -8508,7 +8508,7 @@ class BaseLLMHTTPHandler: response = sync_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_file_content_response( @@ -8584,7 +8584,7 @@ class BaseLLMHTTPHandler: response = await async_httpx_client.get( url=url, headers=headers, - params=params, + params=params or None, ) return container_provider_config.transform_container_file_content_response( diff --git a/litellm/llms/gemini/chat/transformation.py b/litellm/llms/gemini/chat/transformation.py index 16e17dcc876..b69b7e1913e 100644 --- a/litellm/llms/gemini/chat/transformation.py +++ b/litellm/llms/gemini/chat/transformation.py @@ -164,5 +164,8 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig): # If conversion fails, leave as is and let the API handle it pass return _gemini_convert_messages_with_history( - messages=messages, model=model, litellm_params=litellm_params + messages=messages, + model=model, + litellm_params=litellm_params, + custom_llm_provider="gemini", ) diff --git a/litellm/llms/gemini/interactions/transformation.py b/litellm/llms/gemini/interactions/transformation.py index 73435c8db6a..b18b6a28ce4 100644 --- a/litellm/llms/gemini/interactions/transformation.py +++ b/litellm/llms/gemini/interactions/transformation.py @@ -6,13 +6,18 @@ Per OpenAPI spec (https://ai.google.dev/static/api/interactions.openapi.json): - Get: GET https://generativelanguage.googleapis.com/{api_version}/interactions/{interaction_id} - Delete: DELETE https://generativelanguage.googleapis.com/{api_version}/interactions/{interaction_id} -This is a thin wrapper - no transformation needed since we follow the spec directly. +Schema versioning: +- Default (Api-Revision: 2026-05-20): new `steps` schema. +- Legacy (Api-Revision: 2026-05-07): old `outputs` schema, controlled via + litellm.use_legacy_interactions_schema = True. Remove flag after June 8, 2026. """ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple import httpx +import litellm + from litellm._logging import verbose_logger from litellm.litellm_core_utils.core_helpers import process_response_headers from litellm.litellm_core_utils.url_utils import encode_url_path_segment @@ -84,6 +89,15 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig): api_key = GeminiModelInfo.get_api_key(litellm_params.get("api_key")) if api_key: headers["x-goog-api-key"] = api_key + + # Inject the Api-Revision header to select the response schema. + # Default to the new `steps` schema unless the operator has opted out. + # Remove this conditional after June 8, 2026 and always use 2026-05-20. + if litellm.use_legacy_interactions_schema: + headers["Api-Revision"] = "2026-05-07" + else: + headers["Api-Revision"] = "2026-05-20" + return headers def get_complete_url( @@ -119,8 +133,19 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig): headers: dict, ) -> Dict: """ - Build request body per OpenAPI spec - minimal transformation. + Build request body per OpenAPI spec. + + When on the new schema (use_legacy_interactions_schema=False, the default): + - ``response_mime_type`` is folded into ``response_format`` and stripped from + the body (the field was removed in Api-Revision 2026-05-20). + - ``generation_config.image_config`` is moved to a ``response_format`` entry + with ``"type": "image"`` (also removed from generation_config in 2026-05-20). + + When on the legacy schema (use_legacy_interactions_schema=True): + - All fields are forwarded as-is. """ + use_legacy: bool = litellm.use_legacy_interactions_schema + request_body: Dict[str, Any] = {} # Model or Agent (one required) @@ -135,24 +160,81 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig): if input is not None: request_body["input"] = input - # Pass through optional params directly (they match the spec) + # Pass through optional params — legacy schema keeps all fields as-is. optional_keys = [ "tools", "system_instruction", - "generation_config", "stream", "store", "background", "environment", "response_modalities", - "response_format", - "response_mime_type", "previous_interaction_id", ] for key in optional_keys: if optional_params.get(key) is not None: request_body[key] = optional_params[key] + if use_legacy: + # Legacy schema: forward response_mime_type and response_format as-is. + for key in ("response_format", "response_mime_type", "generation_config"): + if optional_params.get(key) is not None: + request_body[key] = optional_params[key] + else: + # New schema (Api-Revision: 2026-05-20): + # response_mime_type is removed — fold it into response_format. + response_format = optional_params.get("response_format") + response_mime_type = optional_params.get("response_mime_type") + + if ( + response_mime_type + and not isinstance(response_format, list) + and ( + not isinstance(response_format, dict) + or "mime_type" not in response_format + ) + ): + # Wrap the legacy schema into the new polymorphic format. + new_rf: Dict[str, Any] = { + "type": "text", + "mime_type": response_mime_type, + } + if response_format is not None: + new_rf["schema"] = response_format + response_format = new_rf + + if response_format is not None: + request_body["response_format"] = response_format + + # image_config moves out of generation_config into response_format. + generation_config: Optional[Dict[str, Any]] = optional_params.get( + "generation_config" + ) + if generation_config is not None: + image_config = None + if isinstance(generation_config, dict): + generation_config = dict( + generation_config + ) # avoid mutating the caller's dict + image_config = generation_config.pop("image_config", None) + if not generation_config: + generation_config = None + + if generation_config is not None: + request_body["generation_config"] = generation_config + + if image_config is not None: + # Move image_config to response_format with type=image. + image_rf: Dict[str, Any] = {"type": "image", **image_config} + existing_rf = request_body.get("response_format") + if existing_rf is None: + request_body["response_format"] = image_rf + elif isinstance(existing_rf, list): + request_body["response_format"] = [*existing_rf, image_rf] + else: + # Convert single entry to array for multimodal output. + request_body["response_format"] = [existing_rf, image_rf] + return request_body def transform_response( diff --git a/litellm/llms/vertex_ai/context_caching/transformation.py b/litellm/llms/vertex_ai/context_caching/transformation.py index 950edbeb478..3d532113ba0 100644 --- a/litellm/llms/vertex_ai/context_caching/transformation.py +++ b/litellm/llms/vertex_ai/context_caching/transformation.py @@ -174,7 +174,9 @@ def transform_openai_messages_to_gemini_context_caching( ) transformed_messages = _gemini_convert_messages_with_history( - messages=new_messages, model=model + messages=new_messages, + model=model, + custom_llm_provider=custom_llm_provider, ) model_name = "models/{}".format(model) diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index ea57339e35f..2995edd1e07 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -682,6 +682,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 messages: List[AllMessageValues], model: Optional[str] = None, litellm_params: Optional[dict] = None, + custom_llm_provider: Optional[str] = None, ) -> List[ContentType]: """ Converts given messages from OpenAI format to Gemini format @@ -983,7 +984,9 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 or assistant_msg.get("function_call") is not None ): # support assistant tool invoke conversion gemini_tool_call_parts = convert_to_gemini_tool_call_invoke( - assistant_msg, model=model + assistant_msg, + model=model, + custom_llm_provider=custom_llm_provider, ) ## check if gemini_tool_call already exists in assistant_content for gemini_tool_call_part in gemini_tool_call_parts: @@ -1045,6 +1048,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 messages[msg_i], # type: ignore last_message_with_tool_calls, # type: ignore model=model, + custom_llm_provider=custom_llm_provider, ) msg_i += 1 # Handle both single part and list of parts (for Computer Use with images) 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 e99f69fcd3e..189ac7a7f6a 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 @@ -289,6 +289,20 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return True return False + @staticmethod + def _forward_gemini_function_call_id( + model: str, custom_llm_provider: Optional[str] = None + ) -> bool: + """ + Whether to include `id` on function_call / function_response parts. + + Gemini 3+ on Google AI Studio accepts (and returns) `id` for strict + tool-call matching. Vertex AI rejects the field with HTTP 400. + """ + if custom_llm_provider != "gemini": + return False + return VertexGeminiConfig._is_gemini_3_or_newer(model) + def _supports_penalty_parameters(self, model: str) -> bool: # Gemini 3 models do not support penalty parameters if VertexGeminiConfig._is_gemini_3_or_newer(model): @@ -2649,7 +2663,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): litellm_params: Optional[dict] = None, ) -> List[ContentType]: return _gemini_convert_messages_with_history( - messages=messages, model=model, litellm_params=litellm_params + messages=messages, + model=model, + litellm_params=litellm_params, + custom_llm_provider="vertex_ai", ) def get_error_class( diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 9ba337da0a5..41f73ddca5e 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -27296,6 +27296,58 @@ "supports_web_search": true, "tpm": 800000 }, + "openrouter/google/gemini-3.1-flash-lite": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_per_audio_token": 5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_pdf_size_mb": 30, + "max_tokens": 65536, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 1.5e-06, + "output_cost_per_token": 1.5e-06, + "rpm": 2000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-3.1-flash-lite", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_audio_output": false, + "supports_code_execution": true, + "supports_file_search": true, + "supports_function_calling": true, + "supports_parallel_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_url_context": true, + "supports_video_input": true, + "supports_vision": true, + "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, diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index ccdebd95af6..e33d15e7cfd 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1278,6 +1278,7 @@ class MCPServerManager: tools = await self._get_tools_from_server( server=server, mcp_auth_header=server_auth_header, + user_api_key_auth=user_api_key_auth, ) return tools except Exception as e: @@ -1458,6 +1459,7 @@ class MCPServerManager: extra_headers: Optional[Dict[str, str]] = None, add_prefix: bool = True, raw_headers: Optional[Dict[str, str]] = None, + user_api_key_auth: Optional[UserAPIKeyAuth] = None, ) -> List[MCPTool]: """ Helper method to get tools from a single MCP server with prefixed names. @@ -1484,6 +1486,46 @@ class MCPServerManager: extra_headers = {} extra_headers.update(server.static_headers) + # MCPJWTSigner: inject signed JWT for tools/list (list path skips pre_call_hook). + # Skip entirely when the signer is not configured (avoid an unnecessary + # dict copy on every list call), when the server has its own static + # Authorization header, when a per-user mcp_auth_header has already + # been resolved, or when the caller already supplied an Authorization + # entry in extra_headers (e.g. a per-user OAuth token resolved + # upstream) — admin-configured static auth and per-user OAuth must + # take precedence so the signer doesn't silently overwrite e.g. an + # upstream API key or a user's OAuth token (MCPClient._get_auth_headers + # applies extra_headers after writing Authorization from auth_value, so + # an injected JWT would otherwise clobber the per-user token). + if user_api_key_auth is not None and not server.spec_path: + from litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer import ( + get_mcp_jwt_signer, + inject_mcp_jwt_headers_for_upstream, + ) + + static_headers = server.static_headers or {} + has_static_authorization = any( + isinstance(k, str) and k.lower() == "authorization" + for k in static_headers.keys() + ) + has_extra_authorization = bool(extra_headers) and any( + isinstance(k, str) and k.lower() == "authorization" + for k in (extra_headers or {}).keys() + ) + + if ( + get_mcp_jwt_signer() is not None + and not has_static_authorization + and not mcp_auth_header + and not has_extra_authorization + ): + extra_headers = await inject_mcp_jwt_headers_for_upstream( + user_api_key_dict=user_api_key_auth, + extra_headers=extra_headers, + raw_headers=raw_headers, + for_list_tools=True, + ) + stdio_env = self._build_stdio_env(server, raw_headers) client = await self._create_mcp_client( @@ -2878,6 +2920,112 @@ class MCPServerManager: return cast(CallToolResult, result) + def _resolve_mcp_server_for_tool_call( + self, + server_name: str, + name: str, + ) -> MCPServer: + """Resolve MCP server for call_tool (prefixed name, registry, fallback).""" + prefixed_tool_name = add_server_prefix_to_name(name, server_name) + mcp_server = self._get_mcp_server_from_tool_name(prefixed_tool_name) + resolved_by_server_name_only = False + normalized_server_name = normalize_server_name(server_name) + + def _candidate_matches_server_name(candidate: MCPServer) -> bool: + for identifier in ( + candidate.alias, + candidate.server_name, + candidate.name, + ): + if identifier and normalize_server_name(identifier) == ( + normalized_server_name + ): + return True + return False + + if mcp_server is None: + for candidate in self.get_registry().values(): + if _candidate_matches_server_name(candidate): + mcp_server = candidate + resolved_by_server_name_only = True + break + if mcp_server is None: + fallback = self._get_mcp_server_from_tool_name(name) + if fallback is not None and ( + not server_name or _candidate_matches_server_name(fallback) + ): + mcp_server = fallback + if mcp_server is None: + raise ValueError(f"Tool {name} not found") + + if resolved_by_server_name_only: + tool_known = ( + name in self.tool_name_to_mcp_server_name_mapping + or prefixed_tool_name in self.tool_name_to_mcp_server_name_mapping + ) + if not tool_known: + raise ValueError(f"Tool {name} not found") + + return mcp_server + + async def _resolve_oauth2_headers_for_tool_call( + self, + mcp_server: MCPServer, + oauth2_headers: Optional[Dict[str, str]], + user_api_key_auth: Optional[UserAPIKeyAuth], + ) -> Optional[Dict[str, str]]: + """Look up per-user OAuth headers when the client did not supply a token.""" + if ( + not mcp_server.needs_user_oauth_token + or oauth2_headers + or user_api_key_auth is None + ): + return oauth2_headers + + user_id = getattr(user_api_key_auth, "user_id", None) + if not user_id: + return oauth2_headers + + try: + from litellm.proxy._experimental.mcp_server.server import ( # noqa: PLC0415 + _get_user_oauth_extra_headers_from_db, + ) + + stored_headers = await _get_user_oauth_extra_headers_from_db( + server=mcp_server, + user_api_key_auth=user_api_key_auth, + ) + if stored_headers: + return stored_headers + except Exception as _lookup_exc: + verbose_logger.debug( + "call_tool: per-user token lookup failed for " "user=%s server=%s: %s", + user_id, + mcp_server.server_id, + _lookup_exc, + ) + return oauth2_headers + + async def _gather_openapi_tool_tasks( + self, + tasks: List[Any], + proxy_logging_obj: Optional[ProxyLogging], + ) -> CallToolResult: + """Await OpenAPI tool tasks and return the tool call result.""" + try: + mcp_responses = await asyncio.gather(*tasks) + result_index = 1 if proxy_logging_obj else 0 + return cast(CallToolResult, mcp_responses[result_index]) + except ( + BlockedPiiEntityError, + GuardrailRaisedException, + HTTPException, + ) as e: + verbose_logger.error( + f"Guardrail blocked MCP tool call during result check: {str(e)}" + ) + raise e + async def call_tool( self, server_name: str, @@ -2908,12 +3056,7 @@ class MCPServerManager: CallToolResult from the MCP server """ start_time = datetime.datetime.now() - - # Get the MCP server - prefixed_tool_name = add_server_prefix_to_name(name, server_name) - mcp_server = self._get_mcp_server_from_tool_name(prefixed_tool_name) - if mcp_server is None: - raise ValueError(f"Tool {name} not found") + mcp_server = self._resolve_mcp_server_for_tool_call(server_name, name) ######################################################### # Pre MCP Tool Call Hook @@ -2947,36 +3090,9 @@ class MCPServerManager: ) tasks.append(during_hook_task) - # For per-user OAuth servers: if the client didn't supply a token in - # oauth2_headers, look up the stored token from Redis / DB. This is the - # call_tool equivalent of _get_user_oauth_extra_headers_from_db used in - # list_tools. - if ( - mcp_server.needs_user_oauth_token - and not oauth2_headers - and user_api_key_auth is not None - ): - user_id = getattr(user_api_key_auth, "user_id", None) - if user_id: - try: - from litellm.proxy._experimental.mcp_server.server import ( # noqa: PLC0415 - _get_user_oauth_extra_headers_from_db, - ) - - stored_headers = await _get_user_oauth_extra_headers_from_db( - server=mcp_server, - user_api_key_auth=user_api_key_auth, - ) - if stored_headers: - oauth2_headers = stored_headers - except Exception as _lookup_exc: - verbose_logger.debug( - "call_tool: per-user token lookup failed for " - "user=%s server=%s: %s", - user_id, - mcp_server.server_id, - _lookup_exc, - ) + oauth2_headers = await self._resolve_oauth2_headers_for_tool_call( + mcp_server, oauth2_headers, user_api_key_auth + ) # For OpenAPI servers, call the tool handler directly instead of via MCP client if mcp_server.spec_path: @@ -3012,26 +3128,7 @@ class MCPServerManager: hook_extra_headers=hook_result.get("extra_headers"), ) - # For OpenAPI tools, await outside the client context - try: - mcp_responses = await asyncio.gather(*tasks) - - # If proxy_logging_obj is None, the tool call result is at index 0 - # If proxy_logging_obj is not None, the tool call result is at index 1 (after the during hook task) - result_index = 1 if proxy_logging_obj else 0 - result = mcp_responses[result_index] - - return cast(CallToolResult, result) - except ( - BlockedPiiEntityError, - GuardrailRaisedException, - HTTPException, - ) as e: - # Re-raise guardrail exceptions to properly fail the MCP call - verbose_logger.error( - f"Guardrail blocked MCP tool call during result check: {str(e)}" - ) - raise e + return await self._gather_openapi_tool_tasks(tasks, proxy_logging_obj) ######################################################### # End of Methods that call the upstream MCP servers diff --git a/litellm/proxy/_experimental/mcp_server/oauth_utils.py b/litellm/proxy/_experimental/mcp_server/oauth_utils.py index 8541a691e88..09176f7253a 100644 --- a/litellm/proxy/_experimental/mcp_server/oauth_utils.py +++ b/litellm/proxy/_experimental/mcp_server/oauth_utils.py @@ -29,6 +29,16 @@ _DEFAULT_PORTS = {"http": 80, "https": 443} # subdomain. HTTPS only. _TRUSTED_REDIRECT_ORIGINS_ENV = "MCP_TRUSTED_REDIRECT_ORIGINS" +# Comma-separated private-use URI allowlist for native MCP clients. +# A trailing ``*`` is a prefix match; end the prefix with ``/`` (e.g. +# ``myapp://host/oauth/*``) so ``.../oauth/callback*`` does not also +# match ``.../oauth/callback-2``. +_TRUSTED_NATIVE_REDIRECT_URIS_ENV = "MCP_TRUSTED_NATIVE_REDIRECT_URIS" + +# Default allowlist for trusted native redirect URIs. +_DEFAULT_NATIVE_REDIRECT_URIS: List[str] = [ + "cursor://anysphere.cursor-mcp/oauth/callback", +] _warned_invalid_proxy_base_url: Optional[str] = None @@ -212,10 +222,82 @@ def _matches_trusted_origin_entry(netloc: str, entry: str) -> bool: return netloc == entry +def _normalize_native_redirect_uri( + parsed, +) -> str: + """Lowercase scheme, netloc, and path for allowlist comparison.""" + return urlunparse( + ( + (parsed.scheme or "").lower(), + (parsed.netloc or "").lower(), + (parsed.path or "").lower(), + "", + "", + "", + ) + ) + + +def _parse_trusted_native_redirect_uris() -> List[str]: + """Built-in native MCP callbacks plus ``MCP_TRUSTED_NATIVE_REDIRECT_URIS``.""" + entries: List[str] = [uri.lower() for uri in _DEFAULT_NATIVE_REDIRECT_URIS] + raw = os.environ.get(_TRUSTED_NATIVE_REDIRECT_URIS_ENV, "").strip() + if not raw: + return entries + for token in raw.split(","): + entry = token.strip().lower() + if entry and entry not in entries: + entries.append(entry) + return entries + + +def _native_wildcard_prefix_matches(normalized: str, prefix: str) -> bool: + """Prefix match for ``entry*`` allowlist rows. + + When the prefix does not end with ``/``, only exact matches or + deeper path segments (``prefix/...``) are accepted — not siblings + like ``prefix-2``. + """ + if not normalized.startswith(prefix): + return False + suffix = normalized[len(prefix) :] + if not suffix: + return True + if prefix.endswith("/"): + return True + return suffix[0] == "/" + + +def _matches_trusted_native_redirect_uri(parsed) -> bool: + """Allowlisted private-use / custom-scheme OAuth callbacks for native MCP clients.""" + if parsed.fragment: + return False + # Query strings are not part of registered redirect_uris (RFC 6749 §3.1.2). + # Rejecting them prevents allowlist bypass via ``.../callback?injected=...``. + if parsed.query: + return False + if not parsed.netloc: + return False + if parsed.username is not None or parsed.password is not None: + return False + if "\\" in parsed.netloc: + return False + + normalized = _normalize_native_redirect_uri(parsed) + for entry in _parse_trusted_native_redirect_uris(): + if entry.endswith("*"): + if _native_wildcard_prefix_matches(normalized, entry[:-1]): + return True + elif normalized == entry: + return True + return False + + def validate_trusted_redirect_uri(request: Request, redirect_uri: str) -> None: """Accept ``redirect_uri`` when it is (a) same-origin with the - proxy's own request origin, (b) loopback, or (c) listed in the - ``MCP_TRUSTED_REDIRECT_ORIGINS`` ops allowlist. + proxy's own request origin, (b) loopback, (c) listed in the + ``MCP_TRUSTED_REDIRECT_ORIGINS`` ops allowlist, or (d) a built-in / + env-configured native MCP client callback (e.g. ``cursor://``). Same-origin is VERIA-57's threat-model-safe equivalent of loopback: an attacker who can host content on the proxy's own HTTPS origin @@ -239,6 +321,8 @@ def validate_trusted_redirect_uri(request: Request, redirect_uri: str) -> None: except ValueError: raise HTTPException(status_code=400, detail="invalid_request") if parsed.scheme not in ("http", "https"): + if _matches_trusted_native_redirect_uri(parsed): + return raise HTTPException(status_code=400, detail="invalid_request") if parsed.fragment: raise HTTPException(status_code=400, detail="invalid_request") @@ -310,9 +394,12 @@ def validate_trusted_redirect_uri(request: Request, redirect_uri: str) -> None: "Inbound headers: X-Forwarded-Proto=%r X-Forwarded-Host=%r " "X-Forwarded-Port=%r Host=%r. " "Trusted-redirect-origins env=%r. " + "Trusted-native-redirect-uris env=%r. " "If this should be accepted, either align ingress X-Forwarded-* " "with the browser URL, set PROXY_BASE_URL to your public origin, " - "or add the redirect_uri host to MCP_TRUSTED_REDIRECT_ORIGINS.", + "add the redirect_uri host to MCP_TRUSTED_REDIRECT_ORIGINS, or " + "for native MCP clients (cursor://, etc.) add the full redirect_uri " + "to MCP_TRUSTED_NATIVE_REDIRECT_URIS.", redirect_uri, proxy_base, os.environ.get("PROXY_BASE_URL"), @@ -321,5 +408,6 @@ def validate_trusted_redirect_uri(request: Request, redirect_uri: str) -> None: request.headers.get("X-Forwarded-Port"), request.headers.get("Host"), os.environ.get(_TRUSTED_REDIRECT_ORIGINS_ENV), + os.environ.get(_TRUSTED_NATIVE_REDIRECT_URIS_ENV), ) raise HTTPException(status_code=400, detail="invalid_request") diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 0c318fceaf4..32a1ca4f290 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -1,6 +1,17 @@ import importlib from datetime import datetime -from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Set, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + List, + Literal, + Optional, + Set, + Tuple, + Union, +) from fastapi import APIRouter, Depends, HTTPException, Query, Request, status @@ -232,11 +243,32 @@ if MCP_AVAILABLE: ) return mcp_auth_header, mcp_server_auth_headers, raw_headers + def _resolve_mcp_server_id_for_rest( + server_id: str, + allowed_server_ids: Union[Set[str], List[str]], + client_ip: Optional[str] = None, + ) -> str: + """ + Map REST ``server_id`` (UUID, server_name, or alias) to canonical server_id. + + tools/list already did this; tools/call must match so clients can pass + server names like ``order_status_mcp`` instead of only UUIDs. + """ + allowed = set(allowed_server_ids) + if server_id in allowed: + return server_id + by_name = global_mcp_server_manager.get_mcp_server_by_name( + server_id, client_ip=client_ip + ) + if by_name is not None and by_name.server_id in allowed: + return by_name.server_id + return server_id + async def _resolve_allowed_mcp_servers_with_ip_filter( request: Request, user_api_key_dict: UserAPIKeyAuth, server_id: str, - ) -> List[MCPServer]: + ) -> Tuple[List[MCPServer], str]: """ Resolve allowed MCP servers for a tool call with IP filtering. @@ -246,10 +278,10 @@ if MCP_AVAILABLE: server_id: The server ID to validate access for Returns: - List of allowed MCPServer objects + Tuple of (allowed MCPServer objects, canonical server_id) Raises: - HTTPException: If the server_id is not allowed + HTTPException: If the server_id is not allowed or not found """ # Get all auth contexts auth_contexts = await build_effective_auth_contexts(user_api_key_dict) @@ -269,8 +301,41 @@ if MCP_AVAILABLE: ) ) - # Check if the specified server_id is allowed - if server_id not in allowed_server_ids_set: + canonical_server_id = _resolve_mcp_server_id_for_rest( + server_id, allowed_server_ids_set, _rest_client_ip + ) + + if canonical_server_id not in allowed_server_ids_set: + _server = global_mcp_server_manager.get_mcp_server_by_id( + server_id + ) or global_mcp_server_manager.get_mcp_server_by_name(server_id) + if ( + _server is not None + and _rest_client_ip is not None + and not global_mcp_server_manager._is_server_accessible_from_ip( + _server, _rest_client_ip + ) + ): + raise HTTPException( + status_code=403, + detail={ + "error": "ip_filtering", + "message": ( + f"MCP server '{server_id}' is not accessible from your IP address " + f"({_rest_client_ip}). This server is restricted to internal " + "networks only. To make it externally accessible, set " + "'available_on_public_internet: true' in the server configuration." + ), + }, + ) + if _server is None: + raise HTTPException( + status_code=404, + detail={ + "error": "server_not_found", + "message": f"MCP server '{server_id}' was not found", + }, + ) raise HTTPException( status_code=403, detail={ @@ -286,7 +351,7 @@ if MCP_AVAILABLE: if server is not None: allowed_mcp_servers.append(server) - return allowed_mcp_servers + return allowed_mcp_servers, canonical_server_id async def _get_tools_for_single_server( server, @@ -302,6 +367,7 @@ if MCP_AVAILABLE: extra_headers=extra_headers, add_prefix=False, raw_headers=raw_headers, + user_api_key_auth=user_api_key_auth, ) # Filter tools based on allowed_tools configuration @@ -778,7 +844,7 @@ if MCP_AVAILABLE: }, ) - tool_arguments = data.get("arguments") + tool_arguments = data.get("arguments") or {} proxy_base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data) ( @@ -811,14 +877,18 @@ if MCP_AVAILABLE: data["user_api_key_auth"] = data["metadata"]["user_api_key_auth"] # Resolve allowed MCP servers with IP filtering - allowed_mcp_servers = await _resolve_allowed_mcp_servers_with_ip_filter( + ( + allowed_mcp_servers, + canonical_server_id, + ) = await _resolve_allowed_mcp_servers_with_ip_filter( request, user_api_key_dict, server_id ) # Look up per-user OAuth headers for this server (mirrors list_tool_rest_api). user_oauth_extra_headers: Optional[Dict[str, str]] = None target_server = next( - (s for s in allowed_mcp_servers if s.server_id == server_id), None + (s for s in allowed_mcp_servers if s.server_id == canonical_server_id), + None, ) if target_server is not None: user_oauth_extra_headers = await _get_user_oauth_extra_headers( @@ -837,6 +907,7 @@ if MCP_AVAILABLE: oauth2_headers=user_oauth_extra_headers or data.get("oauth2_headers"), raw_headers=data.get("raw_headers"), litellm_logging_obj=data.get("litellm_logging_obj"), + requested_server_id=canonical_server_id, ) return result except BlockedPiiEntityError as e: diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index f34bfb5f399..6ee653d347d 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -1424,6 +1424,7 @@ if MCP_AVAILABLE: extra_headers=extra_headers, add_prefix=True, # Always add server prefix raw_headers=raw_headers, + user_api_key_auth=user_api_key_auth, ) filtered_tools = filter_tools_by_allowed_tools(tools, server) @@ -2130,6 +2131,7 @@ if MCP_AVAILABLE: """ # Track resolved MCP server for both permission checks and dispatch mcp_server: Optional[MCPServer] = None + requested_server_id: Optional[str] = kwargs.get("requested_server_id") # If the client called with a display-name override (e.g. "Get Pet"), # translate it back to the original prefixed name before any routing. @@ -2138,14 +2140,55 @@ if MCP_AVAILABLE: # Remove prefix from tool name for logging and processing original_tool_name, server_name = split_server_prefix_from_name(name) + requested_server: Optional[MCPServer] = None + if requested_server_id: + requested_server = next( + (s for s in allowed_mcp_servers if s.server_id == requested_server_id), + None, + ) + # Resolve the actual MCP server up-front so the permission check uses # the canonical server.name even when the tool name is prefixed with a # short ID (LITELLM_USE_SHORT_MCP_TOOL_PREFIX) that doesn't match the # server's display name directly. mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name(name) + if mcp_server is None and requested_server is not None: + # REST callers may pass the raw tool name (no prefix) plus a + # ``requested_server_id``. The mapping might only contain the + # prefixed form, so retry the lookup with every known prefix of + # the requested server before treating the tool as unresolved — + # otherwise the tool_server_mismatch guard below is silently + # bypassed. + for known_prefix in iter_known_server_prefixes(requested_server): + candidate = global_mcp_server_manager._get_mcp_server_from_tool_name( + add_server_prefix_to_name(name, known_prefix) + ) + if candidate is not None: + mcp_server = candidate + break if mcp_server is not None: server_name = mcp_server.name + # REST /mcp-rest/tools/call passes server_id — tool must belong to that server + if requested_server is not None: + if ( + mcp_server is not None + and mcp_server.server_id != requested_server.server_id + ): + raise HTTPException( + status_code=403, + detail={ + "error": "tool_server_mismatch", + "message": ( + f"Tool '{name}' belongs to MCP server '{mcp_server.name}' " + f"but request specified server_id for '{requested_server.name}'." + ), + }, + ) + if mcp_server is None: + mcp_server = requested_server + server_name = requested_server.name + # Only enforce server-level permissions when we can resolve a server if server_name: if not MCPRequestHandler.is_tool_allowed( 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/_next/static/chunks/e1a670efcb966aaa.js b/litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js index aafe9858009..87d6af3231a 100644 --- a/litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js +++ b/litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js @@ -1,11 +1,19 @@ +<<<<<<<< HEAD:litellm/proxy/_experimental/out/_next/static/chunks/0279e5299e9f6e98.js +(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,111790,758472,280881,e=>{"use strict";e.s([],111790);var t=e.i(843476),s=e.i(708347),r=e.i(750113),l=e.i(994388),a=e.i(197647),n=e.i(653824),i=e.i(881073),o=e.i(404206),c=e.i(723731),d=e.i(599724),m=e.i(629569),u=e.i(844444),x=e.i(869216),h=e.i(212931),p=e.i(199133),g=e.i(592968),f=e.i(898586),b=e.i(271645),j=e.i(500727),y=e.i(266027),v=e.i(912598),N=e.i(243652),_=e.i(764205),w=e.i(135214);let S=(0,N.createQueryKeys)("mcpServerHealth");var C=e.i(727749),T=e.i(988846),k=e.i(678784),A=e.i(995926),I=e.i(328196),P=e.i(302202),O=e.i(409797),M=e.i(54131),F=e.i(440987);let E=[{label:"Documentation",fields:[{key:"description",label:"Description",description:"Must have a non-empty description",check:e=>!!e.description?.trim()},{key:"alias",label:"Alias",description:"Must have a display alias",check:e=>!!e.alias?.trim()}]},{label:"Source",fields:[{key:"source_url",label:"GitHub / Source URL",description:"Must link to a source repository",check:e=>!!e.source_url?.trim()}]},{label:"Connection",fields:[{key:"url",label:"Server URL",description:"Must have a URL configured",check:e=>!!e.url?.trim()}]},{label:"Security",fields:[{key:"auth_type",label:"Auth configured",description:"Must use authentication (not 'none')",check:e=>!!e.auth_type&&"none"!==e.auth_type}]}],L=E.flatMap(e=>e.fields),R="mcp_required_fields",U={active:{label:"Active",bg:"bg-green-50",text:"text-green-700",dot:"bg-green-500"},pending_review:{label:"Pending Review",bg:"bg-yellow-50",text:"text-yellow-700",dot:"bg-yellow-500"},rejected:{label:"Rejected",bg:"bg-red-50",text:"text-red-700",dot:"bg-red-500"}};function z({label:e,value:s,color:r}){return(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg px-4 py-3",children:[(0,t.jsx)("div",{className:`text-2xl font-bold ${r}`,children:s}),(0,t.jsx)("div",{className:"text-xs text-gray-500 mt-0.5",children:e})]})}function B({action:e,serverName:s,isCurrentlyActive:r,onConfirm:l,onCancel:a}){let[n,i]=(0,b.useState)(""),o="approve"===e;return(0,t.jsx)("div",{className:"fixed inset-0 bg-black/30 flex items-center justify-center z-50",children:(0,t.jsxs)("div",{className:"bg-white rounded-xl shadow-xl p-6 max-w-sm w-full mx-4",children:[(0,t.jsx)("div",{className:`w-10 h-10 rounded-full flex items-center justify-center mb-4 ${o?"bg-green-100":"bg-red-100"}`,children:o?(0,t.jsx)(k.CheckIcon,{className:"h-5 w-5 text-green-600"}):(0,t.jsx)(I.AlertCircleIcon,{className:"h-5 w-5 text-red-600"})}),(0,t.jsx)("h3",{className:"text-base font-semibold text-gray-900 mb-1",children:o?"Approve MCP Server":"Reject MCP Server"}),(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-4",children:["Are you sure you want to ",e," ",(0,t.jsxs)("span",{className:"font-medium text-gray-700",children:['"',s,'"']}),"?"," ",o?"This will make it active and available for use.":r?"This server is currently live. Rejecting it will immediately remove it from the proxy runtime.":"This will mark the submission as rejected."]}),!o&&(0,t.jsx)("textarea",{placeholder:"Reason for rejection (optional)",value:n,onChange:e=>i(e.target.value),className:"w-full border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 mb-4 resize-none",rows:3}),(0,t.jsxs)("div",{className:"flex gap-3",children:[(0,t.jsx)("button",{type:"button",onClick:a,className:"flex-1 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors",children:"Cancel"}),(0,t.jsx)("button",{type:"button",onClick:()=>l(o?void 0:n||void 0),className:`flex-1 text-white text-sm font-medium py-2 rounded-md transition-colors ${o?"bg-green-500 hover:bg-green-600":"bg-red-500 hover:bg-red-600"}`,children:o?"Approve":"Reject"})]})]})})}function q({requiredFields:e,onChange:s,onSave:r,isSaving:l}){let[a,n]=(0,b.useState)(!1),i=L.filter(t=>e.includes(t.key));return(0,t.jsxs)("div",{className:"mb-5 border border-gray-200 rounded-lg bg-white overflow-hidden",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between px-4 py-3 cursor-pointer select-none",onClick:()=>n(e=>!e),children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(F.SettingsIcon,{className:"h-4 w-4 text-gray-400"}),(0,t.jsx)("span",{className:"text-sm font-semibold text-gray-800",children:"Submission Rules"}),i.length>0?(0,t.jsxs)("span",{className:"text-xs text-gray-500",children:["(",i.length," required field",1!==i.length?"s":"",")"]}):(0,t.jsx)("span",{className:"text-xs text-gray-400 italic",children:"no rules set"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[!a&&i.length>0&&(0,t.jsx)("div",{className:"flex flex-wrap gap-1.5 max-w-md",children:i.map(e=>(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 border border-blue-200 px-2 py-0.5 rounded-full",children:[(0,t.jsx)(k.CheckIcon,{className:"h-3 w-3"}),e.label]},e.key))}),a?(0,t.jsx)(M.ChevronUpIcon,{className:"h-4 w-4 text-gray-400"}):(0,t.jsx)(O.ChevronDownIcon,{className:"h-4 w-4 text-gray-400"})]})]}),a&&(0,t.jsxs)("div",{className:"border-t border-gray-100 px-4 pt-4 pb-4",children:[(0,t.jsx)("p",{className:"text-xs text-gray-500 mb-4",children:"Select which fields must be filled in before a submission is considered compliant. LiteLLM will show ✓ / ✗ for each rule on every submission card below."}),(0,t.jsx)("div",{className:"grid grid-cols-2 gap-x-8 gap-y-5",children:E.map(r=>(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2",children:r.label}),(0,t.jsx)("div",{className:"space-y-2",children:r.fields.map(r=>{let l=e.includes(r.key);return(0,t.jsxs)("label",{className:"flex items-start gap-2.5 cursor-pointer group",children:[(0,t.jsx)("input",{type:"checkbox",checked:l,onChange:()=>{var t;return t=r.key,void s(e.includes(t)?e.filter(e=>e!==t):[...e,t])},className:"mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"text-sm font-medium text-gray-800 group-hover:text-blue-700 transition-colors",children:r.label}),(0,t.jsx)("div",{className:"text-xs text-gray-400",children:r.description})]})]},r.key)})})]},r.label))}),(0,t.jsxs)("div",{className:"mt-5 flex items-center gap-3",children:[(0,t.jsx)("button",{type:"button",disabled:l,onClick:async()=>{await r(),n(!1)},className:"px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors",children:l?"Saving…":"Save Rules"}),(0,t.jsx)("button",{type:"button",onClick:()=>n(!1),className:"px-4 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:bg-gray-50 transition-colors",children:"Cancel"})]})]})]})}function V({server:e,onApprove:s,onReject:r,requiredFields:l}){let a=e.approval_status??"active",n=U[a]??U.active,i=L.filter(e=>l.includes(e.key)).map(t=>({key:t.key,label:t.label,description:t.description,passed:t.check(e)})),o=i.filter(e=>e.passed).length,c=i.length-o,d=i.length>0&&0===c;return(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg overflow-hidden",children:[(0,t.jsx)("div",{className:"px-4 pt-4 pb-3",children:(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("div",{className:"flex items-center gap-2 mb-1.5",children:(0,t.jsxs)("span",{className:`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full ${n.bg} ${n.text}`,children:[(0,t.jsx)("span",{className:`w-1.5 h-1.5 rounded-full ${n.dot}`}),n.label]})}),(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:e.alias??e.server_name??e.server_id}),e.description&&(0,t.jsx)("p",{className:"text-xs text-gray-500 mt-0.5 line-clamp-1",children:e.description}),e.url&&(0,t.jsxs)("div",{className:"flex items-center gap-1.5 mt-1.5",children:[(0,t.jsx)(P.ServerIcon,{className:"h-3.5 w-3.5 text-gray-400 flex-shrink-0"}),(0,t.jsx)("code",{className:"text-xs text-gray-500 font-mono truncate",children:e.url})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3 mt-1.5 text-xs text-gray-400",children:[(0,t.jsxs)("span",{children:["Transport: ",(0,t.jsx)("span",{className:"text-gray-600",children:e.transport??"sse"})]}),(0,t.jsx)("span",{children:"·"}),(0,t.jsxs)("span",{children:["Submitted by: ",(0,t.jsx)("span",{className:"text-gray-600",children:e.submitted_by??"—"})]}),(0,t.jsx)("span",{children:"·"}),(0,t.jsx)("span",{children:function(e){if(!e)return"—";try{let t=new Date(e);return isNaN(t.getTime())?e:t.toISOString().slice(0,10)}catch{return e}}(e.submitted_at)})]}),"rejected"===a&&e.review_notes&&(0,t.jsxs)("p",{className:"text-xs text-red-600 mt-1.5",children:["Rejection reason: ",e.review_notes]})]}),0===i.length&&"rejected"!==a&&(0,t.jsxs)("div",{className:"flex items-center gap-2 flex-shrink-0",children:["active"!==a&&(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Approve"}),(0,t.jsx)("button",{type:"button",onClick:r,className:"text-xs border border-red-300 text-red-600 hover:bg-red-50 px-3 py-1.5 rounded-md transition-colors font-medium",children:"Reject"})]}),0===i.length&&"rejected"===a&&(0,t.jsx)("div",{className:"flex items-center gap-2 flex-shrink-0",children:(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Re-approve"})})]})}),i.length>0&&(0,t.jsxs)("div",{className:"border-t border-gray-200",children:[(0,t.jsxs)("div",{className:`flex items-center gap-3 px-4 py-3 ${d?"bg-green-50 border-b border-green-100":"bg-red-50 border-b border-red-100"}`,children:[(0,t.jsx)("div",{className:`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${d?"bg-green-500":"bg-red-500"}`,children:d?(0,t.jsx)(k.CheckIcon,{className:"h-4 w-4 text-white"}):(0,t.jsx)(A.XIcon,{className:"h-4 w-4 text-white"})}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("div",{className:`text-sm font-semibold leading-tight ${d?"text-green-800":"text-red-800"}`,children:d?"All checks passed":`${c} check${1!==c?"s":""} failed`}),(0,t.jsxs)("div",{className:"text-xs text-gray-500 mt-0.5",children:[o," passing, ",c," failing"]})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2 flex-shrink-0",children:["active"!==a&&"rejected"!==a&&(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Approve"}),"rejected"===a&&(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Re-approve"}),"rejected"!==a&&(0,t.jsx)("button",{type:"button",onClick:r,className:"text-xs border border-red-300 text-red-600 hover:bg-red-50 bg-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Reject"})]})]}),(0,t.jsx)("div",{className:"divide-y divide-gray-100",children:i.map(e=>(0,t.jsxs)("div",{className:"flex items-center gap-3 px-4 py-2.5",children:[(0,t.jsx)("div",{className:`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${e.passed?"bg-green-100":"bg-red-100"}`,children:e.passed?(0,t.jsx)(k.CheckIcon,{className:"h-3 w-3 text-green-600"}):(0,t.jsx)(A.XIcon,{className:"h-3 w-3 text-red-600"})}),(0,t.jsx)("span",{className:`text-sm flex-1 ${e.passed?"text-gray-700":"text-gray-800"}`,children:e.label}),(0,t.jsx)("span",{className:`text-xs ${e.passed?"text-green-600":"text-red-500"}`,children:e.passed?"Passes":"Missing"})]},e.key))})]})]})}function $({accessToken:e}){let[s,r]=(0,b.useState)({total:0,pending_review:0,active:0,rejected:0,items:[]}),[l,a]=(0,b.useState)(""),[n,i]=(0,b.useState)("all"),[o,c]=(0,b.useState)(null),[d,m]=(0,b.useState)(!0),[u,x]=(0,b.useState)(null),[h,p]=(0,b.useState)([]),[g,f]=(0,b.useState)(!1),j=(0,b.useCallback)(async()=>{if(!e)return void m(!1);m(!0),x(null);try{let[t,s]=await Promise.all([(0,_.fetchMCPSubmissions)(e),(0,_.getGeneralSettingsCall)(e).catch(e=>(console.warn("MCPSubmissionsTab: failed to load general settings, compliance rules will be empty:",e),null))]);if(r(t),s?.data&&Array.isArray(s.data)){let e=s.data.find(e=>e.field_name===R);e&&Array.isArray(e.field_value)&&p(e.field_value)}}catch(e){x(e instanceof Error?e.message:"Failed to load submissions")}finally{m(!1)}},[e]);(0,b.useEffect)(()=>{j()},[j]);let y=async()=>{if(e){f(!0);try{await (0,_.updateConfigFieldSetting)(e,R,h),C.default.success("Submission rules saved")}catch{C.default.fromBackend("Failed to save submission rules")}finally{f(!1)}}},v=s.items.filter(e=>{if("all"!==n&&e.approval_status!==n)return!1;if(l.trim()){let t=l.toLowerCase(),s=(e.alias??e.server_name??e.server_id??"").toLowerCase(),r=(e.url??"").toLowerCase();return s.includes(t)||r.includes(t)}return!0});async function N(t,s){if(e)try{await (0,_.approveMCPServer)(e,t),await j(),C.default.success(`MCP server "${s}" approved`)}catch{C.default.fromBackend("Failed to approve MCP server")}finally{c(null)}}async function w(t,s,r){if(e)try{await (0,_.rejectMCPServer)(e,t,r),await j(),C.default.success(`MCP server "${s}" rejected`)}catch{C.default.fromBackend("Failed to reject MCP server")}finally{c(null)}}return(0,t.jsxs)("div",{className:"p-6",children:[(0,t.jsx)(q,{requiredFields:h,onChange:p,onSave:y,isSaving:g}),(0,t.jsxs)("div",{className:"grid grid-cols-4 gap-4 mb-6",children:[(0,t.jsx)(z,{label:"Total Submitted",value:s.total,color:"text-gray-900"}),(0,t.jsx)(z,{label:"Pending Review",value:s.pending_review,color:"text-yellow-600"}),(0,t.jsx)(z,{label:"Active",value:s.active,color:"text-green-600"}),(0,t.jsx)(z,{label:"Rejected",value:s.rejected,color:"text-red-600"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-5",children:[(0,t.jsxs)("div",{className:"relative flex-1 max-w-xs",children:[(0,t.jsx)(T.SearchIcon,{className:"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400"}),(0,t.jsx)("input",{type:"text",placeholder:"Search MCP servers...",value:l,onChange:e=>a(e.target.value),className:"w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"})]}),(0,t.jsxs)("select",{value:n,onChange:e=>i(e.target.value),className:"border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white",children:[(0,t.jsx)("option",{value:"all",children:"All Status"}),(0,t.jsx)("option",{value:"pending_review",children:"Pending Review"}),(0,t.jsx)("option",{value:"active",children:"Active"}),(0,t.jsx)("option",{value:"rejected",children:"Rejected"})]})]}),(0,t.jsxs)("div",{className:"space-y-3",children:[d&&(0,t.jsx)("div",{className:"text-center py-12 text-gray-500 text-sm",children:"Loading submissions…"}),u&&(0,t.jsx)("div",{className:"text-center py-12 text-red-600 text-sm",children:u}),!d&&!u&&0===v.length&&(0,t.jsx)("div",{className:"text-center py-12 text-gray-400 text-sm",children:"No MCP server submissions match your filters."}),!d&&!u&&v.map(e=>(0,t.jsx)(V,{server:e,requiredFields:h,onApprove:()=>c({serverId:e.server_id,serverName:e.alias??e.server_name??e.server_id,action:"approve"}),onReject:()=>c({serverId:e.server_id,serverName:e.alias??e.server_name??e.server_id,action:"reject",isCurrentlyActive:"active"===e.approval_status})},e.server_id))]}),o&&(0,t.jsx)(B,{action:o.action,serverName:o.serverName,isCurrentlyActive:o.isCurrentlyActive,onConfirm:e=>"approve"===o.action?N(o.serverId,o.serverName):w(o.serverId,o.serverName,e),onCancel:()=>c(null)})]})}var D=e.i(808613),H=e.i(311451),K=e.i(998573),W=e.i(482725),J=e.i(988297),Y=e.i(797672),G=e.i(68155),Q=e.i(699857),Z=e.i(149121);let{Text:X}=f.Typography;function ee({serverId:e,serverName:s,accessToken:r,selectedTools:l,onToggle:a}){let[n,i]=(0,b.useState)([]),[o,c]=(0,b.useState)(!1),[d,m]=(0,b.useState)(!1),u=new Set(l.filter(t=>t.server_id===e).map(e=>e.tool_name)),x=(0,b.useCallback)(async()=>{if(r&&!(n.length>0)){c(!0);try{let t=await (0,_.listMCPTools)(r,e),s=Array.isArray(t)?t:t?.tools??[];i(s.map(e=>({name:e.name??e.tool_name??e,description:e.description??""})))}catch{i([])}finally{c(!1)}}},[r,e,n.length]);return(0,t.jsxs)("div",{className:"border border-gray-200 rounded-lg overflow-hidden",children:[(0,t.jsxs)("button",{type:"button",className:"w-full flex items-center justify-between px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors",onClick:()=>{d||x(),m(!d)},children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center gap-2",children:[(0,t.jsx)("span",{className:"inline-block w-2 h-2 rounded-full bg-blue-500 flex-shrink-0"}),s,u.size>0&&(0,t.jsxs)("span",{className:"ml-1 text-xs text-purple-600 font-semibold",children:[u.size," selected"]})]}),(0,t.jsx)("span",{className:"text-gray-400 text-xs",children:d?"▲":"▼"})]}),d&&(0,t.jsx)("div",{className:"p-2",children:o?(0,t.jsx)("div",{className:"flex justify-center py-3",children:(0,t.jsx)(W.Spin,{size:"small"})}):0===n.length?(0,t.jsx)("p",{className:"text-xs text-gray-400 px-2 py-2",children:"No tools found for this server."}):(0,t.jsx)("div",{className:"flex flex-col gap-1",children:n.map(s=>{let r=u.has(s.name);return(0,t.jsxs)("button",{type:"button",onClick:()=>a({server_id:e,tool_name:s.name}),className:`flex items-start justify-between px-3 py-2 rounded-lg text-left transition-colors ${r?"bg-purple-50 border border-purple-300":"bg-white border border-gray-100 hover:bg-gray-50"}`,children:[(0,t.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,t.jsx)("p",{className:`text-sm font-medium leading-tight ${r?"text-purple-800":"text-gray-800"}`,children:s.name}),s.description&&(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-0.5 leading-tight line-clamp-2",children:s.description})]}),r&&(0,t.jsx)("span",{className:"text-purple-500 text-xs font-semibold ml-2 flex-shrink-0 mt-0.5",children:"✓"})]},s.name)})})})]})}function et({open:e,onClose:s,onSave:r,accessToken:a,initialToolset:n}){let[i]=D.Form.useForm(),[o,c]=(0,b.useState)(n?.tools||[]),[m,u]=(0,b.useState)(!1),[x,p]=(0,b.useState)(""),{data:g=[]}=(0,j.useMCPServers)();b.default.useEffect(()=>{e&&(i.setFieldsValue({toolset_name:n?.toolset_name||"",description:n?.description||""}),c(n?.tools||[]),p(""))},[e,n]);let f=e=>{c(t=>t.some(t=>t.server_id===e.server_id&&t.tool_name===e.tool_name)?t.filter(t=>t.server_id!==e.server_id||t.tool_name!==e.tool_name):[...t,e])},y=async()=>{let e=await i.validateFields();u(!0);try{await r(e.toolset_name,e.description,o),s()}finally{u(!1)}},v=g.filter(e=>{let t=x.toLowerCase();return!t||(e.alias||"").toLowerCase().includes(t)||(e.server_name||"").toLowerCase().includes(t)});return(0,t.jsxs)(h.Modal,{open:e,onCancel:s,title:n?"Edit Toolset":"New Toolset",width:960,footer:null,forceRender:!0,children:[(0,t.jsx)(D.Form,{form:i,layout:"vertical",className:"mt-2",children:(0,t.jsxs)("div",{className:"flex gap-4 mb-4",children:[(0,t.jsx)(D.Form.Item,{label:"Toolset Name",name:"toolset_name",rules:[{required:!0,message:"Please enter a toolset name"}],className:"flex-1 mb-0",children:(0,t.jsx)(H.Input,{placeholder:"e.g. github-linear-tools"})}),(0,t.jsx)(D.Form.Item,{label:"Description",name:"description",className:"flex-1 mb-0",children:(0,t.jsx)(H.Input,{placeholder:"Optional description"})})]})}),(0,t.jsxs)("div",{className:"flex gap-4 mt-2",style:{minHeight:360},children:[(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("div",{className:"flex items-center justify-between mb-2",children:(0,t.jsx)(d.Text,{className:"text-sm font-semibold text-gray-700",children:"Available Tools"})}),(0,t.jsx)(H.Input,{placeholder:"Search MCP servers...",value:x,onChange:e=>p(e.target.value),className:"mb-2",allowClear:!0}),(0,t.jsx)("div",{className:"space-y-2 overflow-y-auto",style:{maxHeight:300},children:0===v.length?(0,t.jsx)(d.Text,{className:"text-gray-400 text-sm",children:0===g.length?"No MCP servers configured":"No servers match your search"}):v.map(e=>(0,t.jsx)(ee,{serverId:e.server_id,serverName:e.alias||e.server_name||e.server_id,accessToken:a,selectedTools:o,onToggle:f},e.server_id))})]}),(0,t.jsx)("div",{className:"w-px bg-gray-200 flex-shrink-0"}),(0,t.jsxs)("div",{className:"w-72 flex-shrink-0",children:[(0,t.jsxs)(d.Text,{className:"text-sm font-semibold text-gray-700 mb-2 block",children:["Your Toolset"," ",(0,t.jsxs)("span",{className:"text-xs font-normal text-gray-400",children:["(",o.length," tools)"]})]}),(0,t.jsx)("div",{className:"space-y-1 overflow-y-auto",style:{maxHeight:340},children:0===o.length?(0,t.jsx)(d.Text,{className:"text-gray-400 text-sm",children:"No tools added yet"}):o.map((e,s)=>(0,t.jsxs)("button",{type:"button",onClick:()=>f(e),className:"w-full flex items-center justify-between px-3 py-1.5 rounded-lg border border-purple-200 bg-purple-50 hover:bg-red-50 hover:border-red-200 group transition-colors",children:[(0,t.jsxs)("div",{className:"min-w-0 text-left",children:[(0,t.jsx)("span",{className:"text-xs font-medium text-purple-800 group-hover:text-red-600 truncate block",children:e.tool_name}),(0,t.jsxs)("span",{className:"text-[10px] text-purple-400 truncate block",children:[e.server_id.slice(0,8),"…"]})]}),(0,t.jsx)("span",{className:"ml-2 text-purple-300 group-hover:text-red-400 text-xs flex-shrink-0",children:"✕"})]},s))})]})]}),(0,t.jsxs)("div",{className:"flex justify-end gap-2 mt-4 pt-4 border-t border-gray-200",children:[(0,t.jsx)(l.Button,{variant:"secondary",onClick:s,children:"Cancel"}),(0,t.jsx)(l.Button,{onClick:y,loading:m,children:n?"Save Changes":"Create Toolset"})]})]})}function es(){let[e,s]=(0,b.useState)(!1),r=(0,_.getProxyBaseUrl)(),l=`{ +======== (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,111790,758472,280881,e=>{"use strict";e.s([],111790);var t=e.i(843476),s=e.i(708347),r=e.i(750113),l=e.i(994388),a=e.i(197647),n=e.i(653824),i=e.i(881073),o=e.i(404206),c=e.i(723731),d=e.i(599724),m=e.i(629569),u=e.i(844444),x=e.i(869216),h=e.i(212931),p=e.i(199133),g=e.i(592968),f=e.i(898586),b=e.i(271645),j=e.i(500727),y=e.i(266027),v=e.i(912598),N=e.i(243652),_=e.i(764205),w=e.i(135214);let S=(0,N.createQueryKeys)("mcpServerHealth");var C=e.i(727749),T=e.i(988846),k=e.i(678784),A=e.i(995926),I=e.i(328196),P=e.i(302202),O=e.i(409797),M=e.i(54131),F=e.i(440987);let E=[{label:"Documentation",fields:[{key:"description",label:"Description",description:"Must have a non-empty description",check:e=>!!e.description?.trim()},{key:"alias",label:"Alias",description:"Must have a display alias",check:e=>!!e.alias?.trim()}]},{label:"Source",fields:[{key:"source_url",label:"GitHub / Source URL",description:"Must link to a source repository",check:e=>!!e.source_url?.trim()}]},{label:"Connection",fields:[{key:"url",label:"Server URL",description:"Must have a URL configured",check:e=>!!e.url?.trim()}]},{label:"Security",fields:[{key:"auth_type",label:"Auth configured",description:"Must use authentication (not 'none')",check:e=>!!e.auth_type&&"none"!==e.auth_type}]}],L=E.flatMap(e=>e.fields),R="mcp_required_fields",z={active:{label:"Active",bg:"bg-green-50",text:"text-green-700",dot:"bg-green-500"},pending_review:{label:"Pending Review",bg:"bg-yellow-50",text:"text-yellow-700",dot:"bg-yellow-500"},rejected:{label:"Rejected",bg:"bg-red-50",text:"text-red-700",dot:"bg-red-500"}};function U({label:e,value:s,color:r}){return(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg px-4 py-3",children:[(0,t.jsx)("div",{className:`text-2xl font-bold ${r}`,children:s}),(0,t.jsx)("div",{className:"text-xs text-gray-500 mt-0.5",children:e})]})}function B({action:e,serverName:s,isCurrentlyActive:r,onConfirm:l,onCancel:a}){let[n,i]=(0,b.useState)(""),o="approve"===e;return(0,t.jsx)("div",{className:"fixed inset-0 bg-black/30 flex items-center justify-center z-50",children:(0,t.jsxs)("div",{className:"bg-white rounded-xl shadow-xl p-6 max-w-sm w-full mx-4",children:[(0,t.jsx)("div",{className:`w-10 h-10 rounded-full flex items-center justify-center mb-4 ${o?"bg-green-100":"bg-red-100"}`,children:o?(0,t.jsx)(k.CheckIcon,{className:"h-5 w-5 text-green-600"}):(0,t.jsx)(I.AlertCircleIcon,{className:"h-5 w-5 text-red-600"})}),(0,t.jsx)("h3",{className:"text-base font-semibold text-gray-900 mb-1",children:o?"Approve MCP Server":"Reject MCP Server"}),(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-4",children:["Are you sure you want to ",e," ",(0,t.jsxs)("span",{className:"font-medium text-gray-700",children:['"',s,'"']}),"?"," ",o?"This will make it active and available for use.":r?"This server is currently live. Rejecting it will immediately remove it from the proxy runtime.":"This will mark the submission as rejected."]}),!o&&(0,t.jsx)("textarea",{placeholder:"Reason for rejection (optional)",value:n,onChange:e=>i(e.target.value),className:"w-full border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 mb-4 resize-none",rows:3}),(0,t.jsxs)("div",{className:"flex gap-3",children:[(0,t.jsx)("button",{type:"button",onClick:a,className:"flex-1 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors",children:"Cancel"}),(0,t.jsx)("button",{type:"button",onClick:()=>l(o?void 0:n||void 0),className:`flex-1 text-white text-sm font-medium py-2 rounded-md transition-colors ${o?"bg-green-500 hover:bg-green-600":"bg-red-500 hover:bg-red-600"}`,children:o?"Approve":"Reject"})]})]})})}function q({requiredFields:e,onChange:s,onSave:r,isSaving:l}){let[a,n]=(0,b.useState)(!1),i=L.filter(t=>e.includes(t.key));return(0,t.jsxs)("div",{className:"mb-5 border border-gray-200 rounded-lg bg-white overflow-hidden",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between px-4 py-3 cursor-pointer select-none",onClick:()=>n(e=>!e),children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(F.SettingsIcon,{className:"h-4 w-4 text-gray-400"}),(0,t.jsx)("span",{className:"text-sm font-semibold text-gray-800",children:"Submission Rules"}),i.length>0?(0,t.jsxs)("span",{className:"text-xs text-gray-500",children:["(",i.length," required field",1!==i.length?"s":"",")"]}):(0,t.jsx)("span",{className:"text-xs text-gray-400 italic",children:"no rules set"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[!a&&i.length>0&&(0,t.jsx)("div",{className:"flex flex-wrap gap-1.5 max-w-md",children:i.map(e=>(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 border border-blue-200 px-2 py-0.5 rounded-full",children:[(0,t.jsx)(k.CheckIcon,{className:"h-3 w-3"}),e.label]},e.key))}),a?(0,t.jsx)(M.ChevronUpIcon,{className:"h-4 w-4 text-gray-400"}):(0,t.jsx)(O.ChevronDownIcon,{className:"h-4 w-4 text-gray-400"})]})]}),a&&(0,t.jsxs)("div",{className:"border-t border-gray-100 px-4 pt-4 pb-4",children:[(0,t.jsx)("p",{className:"text-xs text-gray-500 mb-4",children:"Select which fields must be filled in before a submission is considered compliant. LiteLLM will show ✓ / ✗ for each rule on every submission card below."}),(0,t.jsx)("div",{className:"grid grid-cols-2 gap-x-8 gap-y-5",children:E.map(r=>(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2",children:r.label}),(0,t.jsx)("div",{className:"space-y-2",children:r.fields.map(r=>{let l=e.includes(r.key);return(0,t.jsxs)("label",{className:"flex items-start gap-2.5 cursor-pointer group",children:[(0,t.jsx)("input",{type:"checkbox",checked:l,onChange:()=>{var t;return t=r.key,void s(e.includes(t)?e.filter(e=>e!==t):[...e,t])},className:"mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"text-sm font-medium text-gray-800 group-hover:text-blue-700 transition-colors",children:r.label}),(0,t.jsx)("div",{className:"text-xs text-gray-400",children:r.description})]})]},r.key)})})]},r.label))}),(0,t.jsxs)("div",{className:"mt-5 flex items-center gap-3",children:[(0,t.jsx)("button",{type:"button",disabled:l,onClick:async()=>{await r(),n(!1)},className:"px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors",children:l?"Saving…":"Save Rules"}),(0,t.jsx)("button",{type:"button",onClick:()=>n(!1),className:"px-4 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:bg-gray-50 transition-colors",children:"Cancel"})]})]})]})}function V({server:e,onApprove:s,onReject:r,requiredFields:l}){let a=e.approval_status??"active",n=z[a]??z.active,i=L.filter(e=>l.includes(e.key)).map(t=>({key:t.key,label:t.label,description:t.description,passed:t.check(e)})),o=i.filter(e=>e.passed).length,c=i.length-o,d=i.length>0&&0===c;return(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg overflow-hidden",children:[(0,t.jsx)("div",{className:"px-4 pt-4 pb-3",children:(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("div",{className:"flex items-center gap-2 mb-1.5",children:(0,t.jsxs)("span",{className:`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full ${n.bg} ${n.text}`,children:[(0,t.jsx)("span",{className:`w-1.5 h-1.5 rounded-full ${n.dot}`}),n.label]})}),(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:e.alias??e.server_name??e.server_id}),e.description&&(0,t.jsx)("p",{className:"text-xs text-gray-500 mt-0.5 line-clamp-1",children:e.description}),e.url&&(0,t.jsxs)("div",{className:"flex items-center gap-1.5 mt-1.5",children:[(0,t.jsx)(P.ServerIcon,{className:"h-3.5 w-3.5 text-gray-400 flex-shrink-0"}),(0,t.jsx)("code",{className:"text-xs text-gray-500 font-mono truncate",children:e.url})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3 mt-1.5 text-xs text-gray-400",children:[(0,t.jsxs)("span",{children:["Transport: ",(0,t.jsx)("span",{className:"text-gray-600",children:e.transport??"sse"})]}),(0,t.jsx)("span",{children:"·"}),(0,t.jsxs)("span",{children:["Submitted by: ",(0,t.jsx)("span",{className:"text-gray-600",children:e.submitted_by??"—"})]}),(0,t.jsx)("span",{children:"·"}),(0,t.jsx)("span",{children:function(e){if(!e)return"—";try{let t=new Date(e);return isNaN(t.getTime())?e:t.toISOString().slice(0,10)}catch{return e}}(e.submitted_at)})]}),"rejected"===a&&e.review_notes&&(0,t.jsxs)("p",{className:"text-xs text-red-600 mt-1.5",children:["Rejection reason: ",e.review_notes]})]}),0===i.length&&"rejected"!==a&&(0,t.jsxs)("div",{className:"flex items-center gap-2 flex-shrink-0",children:["active"!==a&&(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Approve"}),(0,t.jsx)("button",{type:"button",onClick:r,className:"text-xs border border-red-300 text-red-600 hover:bg-red-50 px-3 py-1.5 rounded-md transition-colors font-medium",children:"Reject"})]}),0===i.length&&"rejected"===a&&(0,t.jsx)("div",{className:"flex items-center gap-2 flex-shrink-0",children:(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Re-approve"})})]})}),i.length>0&&(0,t.jsxs)("div",{className:"border-t border-gray-200",children:[(0,t.jsxs)("div",{className:`flex items-center gap-3 px-4 py-3 ${d?"bg-green-50 border-b border-green-100":"bg-red-50 border-b border-red-100"}`,children:[(0,t.jsx)("div",{className:`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${d?"bg-green-500":"bg-red-500"}`,children:d?(0,t.jsx)(k.CheckIcon,{className:"h-4 w-4 text-white"}):(0,t.jsx)(A.XIcon,{className:"h-4 w-4 text-white"})}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("div",{className:`text-sm font-semibold leading-tight ${d?"text-green-800":"text-red-800"}`,children:d?"All checks passed":`${c} check${1!==c?"s":""} failed`}),(0,t.jsxs)("div",{className:"text-xs text-gray-500 mt-0.5",children:[o," passing, ",c," failing"]})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2 flex-shrink-0",children:["active"!==a&&"rejected"!==a&&(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Approve"}),"rejected"===a&&(0,t.jsx)("button",{type:"button",onClick:s,className:"text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Re-approve"}),"rejected"!==a&&(0,t.jsx)("button",{type:"button",onClick:r,className:"text-xs border border-red-300 text-red-600 hover:bg-red-50 bg-white px-3 py-1.5 rounded-md transition-colors font-medium",children:"Reject"})]})]}),(0,t.jsx)("div",{className:"divide-y divide-gray-100",children:i.map(e=>(0,t.jsxs)("div",{className:"flex items-center gap-3 px-4 py-2.5",children:[(0,t.jsx)("div",{className:`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${e.passed?"bg-green-100":"bg-red-100"}`,children:e.passed?(0,t.jsx)(k.CheckIcon,{className:"h-3 w-3 text-green-600"}):(0,t.jsx)(A.XIcon,{className:"h-3 w-3 text-red-600"})}),(0,t.jsx)("span",{className:`text-sm flex-1 ${e.passed?"text-gray-700":"text-gray-800"}`,children:e.label}),(0,t.jsx)("span",{className:`text-xs ${e.passed?"text-green-600":"text-red-500"}`,children:e.passed?"Passes":"Missing"})]},e.key))})]})]})}function $({accessToken:e}){let[s,r]=(0,b.useState)({total:0,pending_review:0,active:0,rejected:0,items:[]}),[l,a]=(0,b.useState)(""),[n,i]=(0,b.useState)("all"),[o,c]=(0,b.useState)(null),[d,m]=(0,b.useState)(!0),[u,x]=(0,b.useState)(null),[h,p]=(0,b.useState)([]),[g,f]=(0,b.useState)(!1),j=(0,b.useCallback)(async()=>{if(!e)return void m(!1);m(!0),x(null);try{let[t,s]=await Promise.all([(0,_.fetchMCPSubmissions)(e),(0,_.getGeneralSettingsCall)(e).catch(e=>(console.warn("MCPSubmissionsTab: failed to load general settings, compliance rules will be empty:",e),null))]);if(r(t),s?.data&&Array.isArray(s.data)){let e=s.data.find(e=>e.field_name===R);e&&Array.isArray(e.field_value)&&p(e.field_value)}}catch(e){x(e instanceof Error?e.message:"Failed to load submissions")}finally{m(!1)}},[e]);(0,b.useEffect)(()=>{j()},[j]);let y=async()=>{if(e){f(!0);try{await (0,_.updateConfigFieldSetting)(e,R,h),C.default.success("Submission rules saved")}catch{C.default.fromBackend("Failed to save submission rules")}finally{f(!1)}}},v=s.items.filter(e=>{if("all"!==n&&e.approval_status!==n)return!1;if(l.trim()){let t=l.toLowerCase(),s=(e.alias??e.server_name??e.server_id??"").toLowerCase(),r=(e.url??"").toLowerCase();return s.includes(t)||r.includes(t)}return!0});async function N(t,s){if(e)try{await (0,_.approveMCPServer)(e,t),await j(),C.default.success(`MCP server "${s}" approved`)}catch{C.default.fromBackend("Failed to approve MCP server")}finally{c(null)}}async function w(t,s,r){if(e)try{await (0,_.rejectMCPServer)(e,t,r),await j(),C.default.success(`MCP server "${s}" rejected`)}catch{C.default.fromBackend("Failed to reject MCP server")}finally{c(null)}}return(0,t.jsxs)("div",{className:"p-6",children:[(0,t.jsx)(q,{requiredFields:h,onChange:p,onSave:y,isSaving:g}),(0,t.jsxs)("div",{className:"grid grid-cols-4 gap-4 mb-6",children:[(0,t.jsx)(U,{label:"Total Submitted",value:s.total,color:"text-gray-900"}),(0,t.jsx)(U,{label:"Pending Review",value:s.pending_review,color:"text-yellow-600"}),(0,t.jsx)(U,{label:"Active",value:s.active,color:"text-green-600"}),(0,t.jsx)(U,{label:"Rejected",value:s.rejected,color:"text-red-600"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-5",children:[(0,t.jsxs)("div",{className:"relative flex-1 max-w-xs",children:[(0,t.jsx)(T.SearchIcon,{className:"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400"}),(0,t.jsx)("input",{type:"text",placeholder:"Search MCP servers...",value:l,onChange:e=>a(e.target.value),className:"w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"})]}),(0,t.jsxs)("select",{value:n,onChange:e=>i(e.target.value),className:"border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white",children:[(0,t.jsx)("option",{value:"all",children:"All Status"}),(0,t.jsx)("option",{value:"pending_review",children:"Pending Review"}),(0,t.jsx)("option",{value:"active",children:"Active"}),(0,t.jsx)("option",{value:"rejected",children:"Rejected"})]})]}),(0,t.jsxs)("div",{className:"space-y-3",children:[d&&(0,t.jsx)("div",{className:"text-center py-12 text-gray-500 text-sm",children:"Loading submissions…"}),u&&(0,t.jsx)("div",{className:"text-center py-12 text-red-600 text-sm",children:u}),!d&&!u&&0===v.length&&(0,t.jsx)("div",{className:"text-center py-12 text-gray-400 text-sm",children:"No MCP server submissions match your filters."}),!d&&!u&&v.map(e=>(0,t.jsx)(V,{server:e,requiredFields:h,onApprove:()=>c({serverId:e.server_id,serverName:e.alias??e.server_name??e.server_id,action:"approve"}),onReject:()=>c({serverId:e.server_id,serverName:e.alias??e.server_name??e.server_id,action:"reject",isCurrentlyActive:"active"===e.approval_status})},e.server_id))]}),o&&(0,t.jsx)(B,{action:o.action,serverName:o.serverName,isCurrentlyActive:o.isCurrentlyActive,onConfirm:e=>"approve"===o.action?N(o.serverId,o.serverName):w(o.serverId,o.serverName,e),onCancel:()=>c(null)})]})}var D=e.i(808613),H=e.i(311451),K=e.i(998573),W=e.i(482725),J=e.i(988297),Y=e.i(797672),G=e.i(68155),Q=e.i(699857),Z=e.i(149121);let{Text:X}=f.Typography;function ee({serverId:e,serverName:s,accessToken:r,selectedTools:l,onToggle:a}){let[n,i]=(0,b.useState)([]),[o,c]=(0,b.useState)(!1),[d,m]=(0,b.useState)(!1),u=new Set(l.filter(t=>t.server_id===e).map(e=>e.tool_name)),x=(0,b.useCallback)(async()=>{if(r&&!(n.length>0)){c(!0);try{let t=await (0,_.listMCPTools)(r,e),s=Array.isArray(t)?t:t?.tools??[];i(s.map(e=>({name:e.name??e.tool_name??e,description:e.description??""})))}catch{i([])}finally{c(!1)}}},[r,e,n.length]);return(0,t.jsxs)("div",{className:"border border-gray-200 rounded-lg overflow-hidden",children:[(0,t.jsxs)("button",{type:"button",className:"w-full flex items-center justify-between px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors",onClick:()=>{d||x(),m(!d)},children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center gap-2",children:[(0,t.jsx)("span",{className:"inline-block w-2 h-2 rounded-full bg-blue-500 flex-shrink-0"}),s,u.size>0&&(0,t.jsxs)("span",{className:"ml-1 text-xs text-purple-600 font-semibold",children:[u.size," selected"]})]}),(0,t.jsx)("span",{className:"text-gray-400 text-xs",children:d?"▲":"▼"})]}),d&&(0,t.jsx)("div",{className:"p-2",children:o?(0,t.jsx)("div",{className:"flex justify-center py-3",children:(0,t.jsx)(W.Spin,{size:"small"})}):0===n.length?(0,t.jsx)("p",{className:"text-xs text-gray-400 px-2 py-2",children:"No tools found for this server."}):(0,t.jsx)("div",{className:"flex flex-col gap-1",children:n.map(s=>{let r=u.has(s.name);return(0,t.jsxs)("button",{type:"button",onClick:()=>a({server_id:e,tool_name:s.name}),className:`flex items-start justify-between px-3 py-2 rounded-lg text-left transition-colors ${r?"bg-purple-50 border border-purple-300":"bg-white border border-gray-100 hover:bg-gray-50"}`,children:[(0,t.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,t.jsx)("p",{className:`text-sm font-medium leading-tight ${r?"text-purple-800":"text-gray-800"}`,children:s.name}),s.description&&(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-0.5 leading-tight line-clamp-2",children:s.description})]}),r&&(0,t.jsx)("span",{className:"text-purple-500 text-xs font-semibold ml-2 flex-shrink-0 mt-0.5",children:"✓"})]},s.name)})})})]})}function et({open:e,onClose:s,onSave:r,accessToken:a,initialToolset:n}){let[i]=D.Form.useForm(),[o,c]=(0,b.useState)(n?.tools||[]),[m,u]=(0,b.useState)(!1),[x,p]=(0,b.useState)(""),{data:g=[]}=(0,j.useMCPServers)();b.default.useEffect(()=>{e&&(i.setFieldsValue({toolset_name:n?.toolset_name||"",description:n?.description||""}),c(n?.tools||[]),p(""))},[e,n]);let f=e=>{c(t=>t.some(t=>t.server_id===e.server_id&&t.tool_name===e.tool_name)?t.filter(t=>t.server_id!==e.server_id||t.tool_name!==e.tool_name):[...t,e])},y=async()=>{let e=await i.validateFields();u(!0);try{await r(e.toolset_name,e.description,o),s()}finally{u(!1)}},v=g.filter(e=>{let t=x.toLowerCase();return!t||(e.alias||"").toLowerCase().includes(t)||(e.server_name||"").toLowerCase().includes(t)});return(0,t.jsxs)(h.Modal,{open:e,onCancel:s,title:n?"Edit Toolset":"New Toolset",width:960,footer:null,forceRender:!0,children:[(0,t.jsx)(D.Form,{form:i,layout:"vertical",className:"mt-2",children:(0,t.jsxs)("div",{className:"flex gap-4 mb-4",children:[(0,t.jsx)(D.Form.Item,{label:"Toolset Name",name:"toolset_name",rules:[{required:!0,message:"Please enter a toolset name"}],className:"flex-1 mb-0",children:(0,t.jsx)(H.Input,{placeholder:"e.g. github-linear-tools"})}),(0,t.jsx)(D.Form.Item,{label:"Description",name:"description",className:"flex-1 mb-0",children:(0,t.jsx)(H.Input,{placeholder:"Optional description"})})]})}),(0,t.jsxs)("div",{className:"flex gap-4 mt-2",style:{minHeight:360},children:[(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("div",{className:"flex items-center justify-between mb-2",children:(0,t.jsx)(d.Text,{className:"text-sm font-semibold text-gray-700",children:"Available Tools"})}),(0,t.jsx)(H.Input,{placeholder:"Search MCP servers...",value:x,onChange:e=>p(e.target.value),className:"mb-2",allowClear:!0}),(0,t.jsx)("div",{className:"space-y-2 overflow-y-auto",style:{maxHeight:300},children:0===v.length?(0,t.jsx)(d.Text,{className:"text-gray-400 text-sm",children:0===g.length?"No MCP servers configured":"No servers match your search"}):v.map(e=>(0,t.jsx)(ee,{serverId:e.server_id,serverName:e.alias||e.server_name||e.server_id,accessToken:a,selectedTools:o,onToggle:f},e.server_id))})]}),(0,t.jsx)("div",{className:"w-px bg-gray-200 flex-shrink-0"}),(0,t.jsxs)("div",{className:"w-72 flex-shrink-0",children:[(0,t.jsxs)(d.Text,{className:"text-sm font-semibold text-gray-700 mb-2 block",children:["Your Toolset"," ",(0,t.jsxs)("span",{className:"text-xs font-normal text-gray-400",children:["(",o.length," tools)"]})]}),(0,t.jsx)("div",{className:"space-y-1 overflow-y-auto",style:{maxHeight:340},children:0===o.length?(0,t.jsx)(d.Text,{className:"text-gray-400 text-sm",children:"No tools added yet"}):o.map((e,s)=>(0,t.jsxs)("button",{type:"button",onClick:()=>f(e),className:"w-full flex items-center justify-between px-3 py-1.5 rounded-lg border border-purple-200 bg-purple-50 hover:bg-red-50 hover:border-red-200 group transition-colors",children:[(0,t.jsxs)("div",{className:"min-w-0 text-left",children:[(0,t.jsx)("span",{className:"text-xs font-medium text-purple-800 group-hover:text-red-600 truncate block",children:e.tool_name}),(0,t.jsxs)("span",{className:"text-[10px] text-purple-400 truncate block",children:[e.server_id.slice(0,8),"…"]})]}),(0,t.jsx)("span",{className:"ml-2 text-purple-300 group-hover:text-red-400 text-xs flex-shrink-0",children:"✕"})]},s))})]})]}),(0,t.jsxs)("div",{className:"flex justify-end gap-2 mt-4 pt-4 border-t border-gray-200",children:[(0,t.jsx)(l.Button,{variant:"secondary",onClick:s,children:"Cancel"}),(0,t.jsx)(l.Button,{onClick:y,loading:m,children:n?"Save Changes":"Create Toolset"})]})]})}function es(){let[e,s]=(0,b.useState)(!1),r=(0,_.getProxyBaseUrl)(),l=`{ +>>>>>>>> origin/litellm_internal_staging:litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js "mcpServers": { "my-toolset": { "url": "${r}/toolset//mcp", "headers": { "x-litellm-api-key": "Bearer " } } } +<<<<<<<< HEAD:litellm/proxy/_experimental/out/_next/static/chunks/0279e5299e9f6e98.js +}`,a=async()=>{try{await navigator.clipboard.writeText(l),s(!0),setTimeout(()=>s(!1),1500)}catch{}};return(0,t.jsxs)("div",{className:"mb-6 rounded-lg border border-gray-200 bg-gray-50 px-5 py-4",children:[(0,t.jsx)("p",{className:"text-sm font-medium text-gray-700 mb-1",children:"How toolsets work"}),(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-3",children:["Create a toolset, assign it to a key via ",(0,t.jsx)("span",{className:"font-medium text-gray-700",children:"API Keys → Edit Key → MCP Servers"}),", then point your MCP client at the toolset URL. The client only sees the tools you picked."]}),(0,t.jsx)("div",{className:"text-xs text-gray-400 mb-1",children:"Claude Code / Cursor config"}),(0,t.jsxs)("div",{className:"relative",children:[(0,t.jsx)("pre",{className:"bg-white border border-gray-200 rounded px-4 py-3 text-xs font-mono text-gray-700 overflow-x-auto leading-relaxed pr-14",children:l}),(0,t.jsx)("button",{type:"button",onClick:a,className:"absolute top-2 right-2 px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 text-gray-400 hover:text-gray-600 border-gray-200 transition-colors",children:e?"✓":"copy"})]})]})}function er({accessToken:e,userRole:s}){let r=(0,v.useQueryClient)(),{data:a=[],isLoading:n}=(0,Q.useMCPToolsets)(),[i,o]=(0,b.useState)(!1),[c,u]=(0,b.useState)(null),[x,p]=(0,b.useState)(null),[g,f]=(0,b.useState)(!1),j="Admin"===s||"proxy_admin"===s,y=async(t,s,l)=>{e&&(await (0,_.createMCPToolset)(e,{toolset_name:t,description:s,tools:l}),K.message.success("Toolset created"),r.invalidateQueries({queryKey:["mcpToolsets"]}))},N=async(t,s,l)=>{e&&c&&(await (0,_.updateMCPToolset)(e,{toolset_id:c.toolset_id,toolset_name:t,description:s,tools:l}),K.message.success("Toolset updated"),r.invalidateQueries({queryKey:["mcpToolsets"]}),u(null))},w=async()=>{if(e&&x){f(!0);try{await (0,_.deleteMCPToolset)(e,x),K.message.success("Toolset deleted"),r.invalidateQueries({queryKey:["mcpToolsets"]}),p(null)}finally{f(!1)}}},S=(0,_.getProxyBaseUrl)(),C=[{header:"Toolset ID",accessorKey:"toolset_id",cell:({row:e})=>(0,t.jsxs)("span",{className:"font-mono text-xs bg-gray-100 px-2 py-0.5 rounded text-gray-600",children:[e.original.toolset_id.slice(0,8),"…"]})},{header:"Name",accessorKey:"toolset_name",cell:({row:e})=>{let s=`${S}/toolset/${e.original.toolset_name}/mcp`;return(0,t.jsxs)("div",{className:"flex flex-col gap-0.5",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"inline-block w-2 h-2 rounded-full bg-purple-500 flex-shrink-0"}),(0,t.jsx)("span",{className:"font-medium text-gray-900",children:e.original.toolset_name})]}),(0,t.jsx)("button",{type:"button",className:"text-xs text-gray-400 hover:text-purple-600 font-mono truncate max-w-xs text-left transition-colors",onClick:()=>navigator.clipboard.writeText(s),title:"Click to copy endpoint URL",children:s})]})}},{header:"Description",accessorKey:"description",cell:({row:e})=>(0,t.jsx)("span",{className:"text-sm text-gray-500",children:e.original.description||"—"})},{header:"Tools",accessorKey:"tools",cell:({row:e})=>{let s=e.original.tools;return(0,t.jsxs)("div",{className:"flex flex-wrap gap-1 max-w-xs",children:[s.slice(0,4).map((e,s)=>(0,t.jsx)("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded bg-purple-50 border border-purple-200 text-purple-700 text-xs",children:e.tool_name},s)),s.length>4&&(0,t.jsxs)("span",{className:"text-xs text-gray-400 self-center",children:["+",s.length-4," more"]})]})}},{header:"Created",accessorKey:"created_at",cell:({row:e})=>(0,t.jsx)("span",{className:"text-xs text-gray-500",children:e.original.created_at?new Date(e.original.created_at).toLocaleDateString():"—"})},...j?[{header:"",id:"actions",cell:({row:e})=>(0,t.jsxs)("div",{className:"flex items-center gap-1 justify-end",children:[(0,t.jsx)("button",{type:"button",className:"p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-700 transition-colors",onClick:()=>u(e.original),children:(0,t.jsx)(Y.PencilIcon,{className:"h-4 w-4"})}),(0,t.jsx)("button",{type:"button",className:"p-1.5 rounded-lg hover:bg-red-50 text-gray-400 hover:text-red-500 transition-colors",onClick:()=>p(e.original.toolset_id),children:(0,t.jsx)(G.TrashIcon,{className:"h-4 w-4"})})]})}]:[]];return(0,t.jsxs)("div",{className:"mt-4",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(m.Title,{children:"MCP Toolsets"}),(0,t.jsx)(d.Text,{className:"text-gray-500 text-sm",children:"Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP permissions dropdown."})]}),j&&(0,t.jsx)(l.Button,{icon:J.PlusIcon,onClick:()=>o(!0),children:"New Toolset"})]}),(0,t.jsx)(es,{}),(0,t.jsx)(Z.DataTable,{data:a,columns:C,renderSubComponent:()=>(0,t.jsx)("div",{}),getRowCanExpand:()=>!1,isLoading:n,noDataMessage:"No toolsets yet. Click 'New Toolset' to create one.",loadingMessage:"Loading toolsets...",enableSorting:!0}),(0,t.jsx)(et,{open:i,onClose:()=>o(!1),onSave:y,accessToken:e}),c&&(0,t.jsx)(et,{open:!!c,onClose:()=>u(null),onSave:N,accessToken:e,initialToolset:c}),(0,t.jsx)(h.Modal,{open:!!x,onCancel:()=>p(null),onOk:w,okText:"Delete",okButtonProps:{danger:!0,loading:g},title:"Delete Toolset",children:(0,t.jsx)("p",{children:"Are you sure you want to delete this toolset? Keys and teams using it will lose access to the scoped tools."})})]})}var el=e.i(790848),ea=e.i(362024),en=e.i(827252),ei=e.i(779241),eo=e.i(292335),ec=e.i(28651);let ed="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500",em=({label:e,tooltip:s})=>(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:[e,(0,t.jsx)(g.Tooltip,{title:s,children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),eu=({isM2M:e,isEditing:s=!1,oauthFlow:r,initialFlowType:a,docsUrl:n})=>{let i=s?" (leave blank to keep existing)":"";return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"OAuth Flow Type",tooltip:"Choose how the proxy authenticates with this MCP server. M2M is for server-to-server communication using client credentials. Interactive (PKCE) is for user-facing flows that require browser-based authorization."}),name:"oauth_flow_type",...a?{initialValue:a}:{},children:(0,t.jsxs)(p.Select,{className:"rounded-lg",size:"large",children:[(0,t.jsx)(p.Select.Option,{value:eo.OAUTH_FLOW.M2M,children:(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"font-medium",children:"Machine-to-Machine (M2M)"}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-2",children:"server-to-server, no user interaction"})]})}),(0,t.jsx)(p.Select.Option,{value:eo.OAUTH_FLOW.INTERACTIVE,children:(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"font-medium",children:"Interactive (PKCE)"}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-2",children:"browser-based user authorization"})]})})]})}),e?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Client ID",tooltip:"OAuth2 client ID for the client_credentials grant."}),name:["credentials","client_id"],rules:[{required:!0,message:"Client ID is required for M2M OAuth"}],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter OAuth client ID${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Client Secret",tooltip:"OAuth2 client secret for the client_credentials grant."}),name:["credentials","client_secret"],rules:[{required:!0,message:"Client Secret is required for M2M OAuth"}],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter OAuth client secret${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token URL",tooltip:"Token endpoint URL for the client_credentials grant."}),name:"token_url",rules:[{required:!0,message:"Token URL is required for M2M OAuth"}],children:(0,t.jsx)(ei.TextInput,{placeholder:"https://auth.example.com/oauth/token",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Scopes (optional)",tooltip:"Optional scopes to request with the client_credentials grant."}),name:["credentials","scopes"],children:(0,t.jsx)(p.Select,{mode:"tags",tokenSeparators:[","],placeholder:"Add scopes",className:"rounded-lg",size:"large"})})]}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"flex items-center justify-between w-full",children:[(0,t.jsx)(em,{label:"Client ID (optional)",tooltip:"Provide only if your MCP server cannot handle dynamic client registration."}),n&&(0,t.jsx)("a",{href:n,target:"_blank",rel:"noopener noreferrer",className:"text-xs text-blue-500 hover:text-blue-700 ml-2 font-normal",onClick:e=>e.stopPropagation(),children:"Create OAuth App →"})]}),name:["credentials","client_id"],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter client ID${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Client Secret (optional)",tooltip:"Provide only if your MCP server cannot handle dynamic client registration."}),name:["credentials","client_secret"],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter client secret${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Scopes (optional)",tooltip:"Optional scopes requested during token exchange. Separate multiple scopes with enter or commas."}),name:["credentials","scopes"],children:(0,t.jsx)(p.Select,{mode:"tags",tokenSeparators:[","],placeholder:"Add scopes",className:"rounded-lg",size:"large"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Authorization URL (optional)",tooltip:"Optional override for the authorization endpoint."}),name:"authorization_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://example.com/oauth/authorize",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token URL (optional)",tooltip:"Optional override for the token endpoint."}),name:"token_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://example.com/oauth/token",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Registration URL (optional)",tooltip:"Optional override for the dynamic client registration endpoint."}),name:"registration_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://example.com/oauth/register",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token Validation Rules (optional)",tooltip:'JSON object of key-value rules checked against the OAuth token response before storing. Supports dot-notation for nested fields (e.g. {"organization": "my-org", "team.id": "123"}). Tokens that fail validation are rejected with HTTP 403.'}),name:"token_validation_json",rules:[{validator:(e,t)=>{if(!t||""===t.trim())return Promise.resolve();try{return JSON.parse(t),Promise.resolve()}catch{return Promise.reject(Error("Must be valid JSON"))}}}],children:(0,t.jsx)(H.Input.TextArea,{placeholder:'{\n "organization": "my-org",\n "team.id": "123"\n}',rows:4,className:"font-mono text-sm rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token Storage TTL (seconds, optional)",tooltip:"How long to cache each user's OAuth access token in Redis before evicting it (regardless of the token's own expires_in). Leave blank to derive the TTL from the token's expires_in, or fall back to the 12-hour default."}),name:"token_storage_ttl_seconds",children:(0,t.jsx)(ec.InputNumber,{min:1,placeholder:"e.g. 3600",className:"w-full rounded-lg",style:{width:"100%"}})}),r&&(0,t.jsxs)("div",{className:"rounded-lg border border-dashed border-gray-300 p-4 space-y-2",children:[(0,t.jsx)("p",{className:"text-sm text-gray-600",children:"Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value."}),(0,t.jsx)(l.Button,{variant:"secondary",onClick:r.startOAuthFlow,disabled:"authorizing"===r.status||"exchanging"===r.status,children:"authorizing"===r.status?"Waiting for authorization...":"exchanging"===r.status?"Exchanging authorization code...":"Authorize & Fetch Token"}),r.error&&(0,t.jsx)("p",{className:"text-sm text-red-500",children:r.error}),"success"===r.status&&r.tokenResponse?.access_token&&(0,t.jsxs)("p",{className:"text-sm text-green-600",children:["Token fetched. Expires in ",r.tokenResponse.expires_in??"?"," seconds."]})]})]})]})};var ex=e.i(906579),eh=e.i(458505),ep=e.i(366308),eg=e.i(304967);let ef=({value:e={},onChange:s,tools:r=[],disabled:l=!1})=>(0,t.jsx)(eg.Card,{children:(0,t.jsxs)("div",{className:"space-y-6",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-4",children:[(0,t.jsx)(eh.DollarOutlined,{className:"text-green-600"}),(0,t.jsx)(m.Title,{children:"Cost Configuration"}),(0,t.jsx)(g.Tooltip,{title:"Configure costs for this MCP server's tool calls. Set a default rate and per-tool overrides.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-gray-400"})})]}),(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:["Default Cost per Query ($)",(0,t.jsx)(g.Tooltip,{title:"Default cost charged for each tool call to this server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-1 text-gray-400"})})]}),(0,t.jsx)(ec.InputNumber,{min:0,step:1e-4,precision:4,placeholder:"0.0000",value:e.default_cost_per_query,onChange:t=>{let r={...e,default_cost_per_query:t};s?.(r)},disabled:l,style:{width:"200px"},addonBefore:"$"}),(0,t.jsx)(d.Text,{className:"block mt-1 text-gray-500 text-sm",children:"Set a default cost for all tool calls to this server"})]}),r.length>0&&(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("label",{className:"block text-sm font-medium text-gray-700",children:["Tool-Specific Costs ($)",(0,t.jsx)(g.Tooltip,{title:"Override the default cost for specific tools. Leave blank to use the default rate.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-1 text-gray-400"})})]}),(0,t.jsx)(ea.Collapse,{items:[{key:"1",label:(0,t.jsxs)("div",{className:"flex items-center",children:[(0,t.jsx)(ep.ToolOutlined,{className:"mr-2 text-blue-500"}),(0,t.jsx)("span",{className:"font-medium",children:"Available Tools"}),(0,t.jsx)(ex.Badge,{count:r.length,style:{backgroundColor:"#52c41a",marginLeft:"8px"}})]}),children:(0,t.jsx)("div",{className:"space-y-3 max-h-64 overflow-y-auto",children:r.map((r,a)=>(0,t.jsxs)("div",{className:"flex items-center justify-between p-3 bg-gray-50 rounded-lg",children:[(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)(d.Text,{className:"font-medium text-gray-900",children:r.name}),r.description&&(0,t.jsx)(d.Text,{className:"text-gray-500 text-sm block mt-1",children:r.description})]}),(0,t.jsx)("div",{className:"ml-4",children:(0,t.jsx)(ec.InputNumber,{min:0,step:1e-4,precision:4,placeholder:"Use default",value:e.tool_name_to_cost_per_query?.[r.name],onChange:t=>{var l;let a;return l=r.name,a={...e,tool_name_to_cost_per_query:{...e.tool_name_to_cost_per_query,[l]:t}},void s?.(a)},disabled:l,style:{width:"120px"},addonBefore:"$"})})]},a))})}]})]})]}),(e.default_cost_per_query||e.tool_name_to_cost_per_query&&Object.keys(e.tool_name_to_cost_per_query).length>0)&&(0,t.jsxs)("div",{className:"mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg",children:[(0,t.jsx)(d.Text,{className:"text-blue-800 font-medium",children:"Cost Summary:"}),(0,t.jsxs)("div",{className:"mt-2 space-y-1",children:[e.default_cost_per_query&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• Default cost: $",e.default_cost_per_query.toFixed(4)," per query"]}),e.tool_name_to_cost_per_query&&Object.entries(e.tool_name_to_cost_per_query).map(([e,s])=>null!=s&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• ",e,": $",s.toFixed(4)," per query"]},e))]})]})]})});var eb=e.i(464571),ej=e.i(560445),ey=e.i(245704),ev=e.i(270377),eN=e.i(91979);let e_=({formValues:e,tools:s,isLoadingTools:r,toolsError:l,toolsErrorStackTrace:a,canFetchTools:n,fetchTools:i})=>n||e.url||e.spec_path?(0,t.jsx)(eg.Card,{children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"text-blue-600"}),(0,t.jsx)(m.Title,{children:"Connection Status"})]}),!n&&(e.url||e.spec_path)&&(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"Complete required fields to test connection"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm",children:"Fill in URL, Transport, and Authentication to test MCP server connection"})]}),n&&(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"text-gray-700 font-medium",children:r?"Testing connection to MCP server...":s.length>0?"Connection successful":l?"Connection failed":"Ready to test connection"}),(0,t.jsx)("br",{}),(0,t.jsxs)(d.Text,{className:"text-gray-500 text-sm",children:["Server: ",e.url||e.spec_path]})]}),r&&(0,t.jsxs)("div",{className:"flex items-center text-blue-600",children:[(0,t.jsx)(W.Spin,{size:"small",className:"mr-2"}),(0,t.jsx)(d.Text,{className:"text-blue-600",children:"Connecting..."})]}),!r&&!l&&s.length>0&&(0,t.jsxs)("div",{className:"flex items-center text-green-600",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"mr-1"}),(0,t.jsx)(d.Text,{className:"text-green-600 font-medium",children:"Connected"})]}),l&&(0,t.jsxs)("div",{className:"flex items-center text-red-600",children:[(0,t.jsx)(ev.ExclamationCircleOutlined,{className:"mr-1"}),(0,t.jsx)(d.Text,{className:"text-red-600 font-medium",children:"Failed"})]})]}),r&&(0,t.jsxs)("div",{className:"flex items-center justify-center py-6",children:[(0,t.jsx)(W.Spin,{size:"large"}),(0,t.jsx)(d.Text,{className:"ml-3",children:"Testing connection and loading tools..."})]}),l&&(0,t.jsx)(ej.Alert,{message:"Connection Failed",description:(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{children:l}),a&&(0,t.jsx)(ea.Collapse,{items:[{key:"stack-trace",label:"Stack Trace",children:(0,t.jsx)("pre",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word",fontSize:"12px",fontFamily:"monospace",margin:0,padding:"8px",backgroundColor:"#f5f5f5",borderRadius:"4px",maxHeight:"400px",overflow:"auto"},children:a})}],style:{marginTop:"12px"}})]}),type:"error",showIcon:!0,action:(0,t.jsx)(eb.Button,{icon:(0,t.jsx)(eN.ReloadOutlined,{}),onClick:i,size:"small",children:"Retry"})}),!r&&0===s.length&&!l&&(0,t.jsxs)("div",{className:"text-center py-6 text-gray-500 border rounded-lg border-dashed",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"text-2xl mb-2 text-green-500"}),(0,t.jsx)(d.Text,{className:"text-green-600 font-medium",children:"Connection successful!"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-gray-500",children:"No tools found for this MCP server"})]})]})]})}):null;var ew=e.i(928685),eS=e.i(751904),eC=e.i(536916),eT=e.i(91739);let ek=({accessToken:e,oauthAccessToken:t,formValues:s,enabled:r=!0})=>{let[l,a]=(0,b.useState)([]),[n,i]=(0,b.useState)(!1),[o,c]=(0,b.useState)(null),[d,m]=(0,b.useState)(null),[u,x]=(0,b.useState)(!1),h=s.auth_type===eo.AUTH_TYPE.OAUTH2&&s.oauth_flow_type===eo.OAUTH_FLOW.M2M,p=s.auth_type===eo.AUTH_TYPE.OAUTH2&&!h,g=s.transport===eo.TRANSPORT.OPENAPI,f=g?!!s.spec_path:!!s.url,j=g?!!(f&&e):!!(f&&s.transport&&s.auth_type&&e&&(!p||t)),y=JSON.stringify(s.static_headers??{}),v=JSON.stringify(s.credentials??{}),N=async()=>{if(e&&(s.url||s.spec_path)&&(!p||t||g)){i(!0),c(null);try{let r=Array.isArray(s.static_headers)?s.static_headers.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value!=null?String(t.value):""),e},{}):!Array.isArray(s.static_headers)&&s.static_headers&&"object"==typeof s.static_headers?Object.entries(s.static_headers).reduce((e,[t,s])=>(t&&(e[t]=null!=s?String(s):""),e),{}):{},l=s.credentials&&"object"==typeof s.credentials?Object.entries(s.credentials).reduce((e,[t,s])=>{if(null==s||""===s)return e;if("scopes"===t){if(Array.isArray(s)){let r=s.filter(e=>null!=e&&""!==e);r.length>0&&(e[t]=r)}}else e[t]=s;return e},{}):void 0,n=s.transport===eo.TRANSPORT.OPENAPI?"http":s.transport,i={server_id:s.server_id||"",server_name:s.server_name||"",url:s.url,spec_path:s.spec_path,transport:n,auth_type:s.auth_type,authorization_url:s.authorization_url,token_url:s.token_url,registration_url:s.registration_url,mcp_info:s.mcp_info,static_headers:r};l&&Object.keys(l).length>0&&(i.credentials=l);let o=await (0,_.testMCPToolsListRequest)(e,i,t);if(o.tools&&!o.error)a(o.tools),c(null),m(null),o.tools.length>0&&!u&&x(!0);else{let e=o.message||"Failed to retrieve tools list";c(e),m(o.stack_trace||null),a([]),x(!1)}}catch(e){console.error("Tools fetch error:",e),c(e instanceof Error?e.message:String(e)),m(null),a([]),x(!1)}finally{i(!1)}}},w=()=>{a([]),c(null),m(null),x(!1)};return(0,b.useEffect)(()=>{r&&(j?N():w())},[s.url,s.spec_path,s.transport,s.auth_type,e,r,t,j,y,v]),{tools:l,isLoadingTools:n,toolsError:o,toolsErrorStackTrace:d,hasShownSuccessMessage:u,canFetchTools:j,fetchTools:N,clearTools:w}};var eA=e.i(531516);let eI=({tool:e,isEnabled:s,isEditExpanded:r,toolNameToDisplayName:l,toolNameToDescription:a,onToggle:n,onToggleExpand:i,onDisplayNameChange:o,onDescriptionChange:c})=>(0,t.jsxs)("div",{className:`rounded-lg border transition-colors ${s?"bg-blue-50 border-blue-300 hover:border-blue-400":"bg-gray-50 border-gray-200 hover:border-gray-300"}`,children:[(0,t.jsx)("div",{className:"p-4 cursor-pointer",onClick:()=>n(e.name),children:(0,t.jsxs)("div",{className:"flex items-start gap-3",children:[(0,t.jsx)(eC.Checkbox,{checked:s,onChange:()=>n(e.name)}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"font-medium text-gray-900",children:l[e.name]||e.name}),(0,t.jsx)("span",{className:`px-2 py-0.5 text-xs rounded-full font-medium ${s?"bg-green-100 text-green-800":"bg-red-100 text-red-800"}`,children:s?"Enabled":"Disabled"}),l[e.name]&&(0,t.jsx)("span",{className:"px-2 py-0.5 text-xs rounded-full font-medium bg-purple-100 text-purple-800",children:"Custom name"})]}),(a[e.name]||e.description)&&(0,t.jsx)(d.Text,{className:"text-gray-500 text-sm block mt-1",children:a[e.name]||e.description}),(0,t.jsx)(d.Text,{className:"text-gray-400 text-xs block mt-1",children:s?"✓ Users can call this tool":"✗ Users cannot call this tool"})]}),(0,t.jsx)("button",{type:"button",onClick:t=>i(e.name,t),className:`p-1.5 rounded-md transition-colors ${r?"bg-blue-100 text-blue-600":"text-gray-400 hover:text-gray-600 hover:bg-gray-100"}`,title:"Edit display name and description",children:(0,t.jsx)(eS.EditOutlined,{})})]})}),r&&(0,t.jsxs)("div",{className:"px-4 pb-4 pt-3 border-t border-gray-200 space-y-3 bg-gray-50 rounded-b-lg",onClick:e=>e.stopPropagation(),children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-600 mb-1 block",children:"Display Name"}),(0,t.jsx)(H.Input,{placeholder:e.name,value:l[e.name]||"",onChange:t=>o(e.name,t.target.value)}),(0,t.jsx)(d.Text,{className:"text-xs text-gray-400 mt-1 block",children:"Override how this tool's name appears to users. Leave blank to use original."})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-600 mb-1 block",children:"Description"}),(0,t.jsx)(H.Input.TextArea,{placeholder:e.description||"No description",value:a[e.name]||"",onChange:t=>c(e.name,t.target.value),rows:2}),(0,t.jsx)(d.Text,{className:"text-xs text-gray-400 mt-1 block",children:"Override the tool description shown to users. Leave blank to use original."})]})]})]}),eP=({accessToken:e,oauthAccessToken:s,formValues:r,allowedTools:l,existingAllowedTools:a,onAllowedToolsChange:n,toolNameToDisplayName:i,toolNameToDescription:o,onToolNameToDisplayNameChange:c,onToolNameToDescriptionChange:u,keyTools:x,externalTools:h,externalIsLoading:p,externalError:g,externalCanFetch:f})=>{let j=(0,b.useRef)([]),[y,v]=(0,b.useState)(""),[N,_]=(0,b.useState)("crud"),w=(0,b.useRef)(!1),S=(0,b.useRef)(""),[C,T]=(0,b.useState)(new Set),k=void 0!==h,A=ek({accessToken:e,oauthAccessToken:s,formValues:r,enabled:!k}),I=k?h:A.tools,P=k?p??!1:A.isLoadingTools,O=k?g??null:A.toolsError,M=k?f??!1:A.canFetchTools,F=(0,b.useMemo)(()=>{if(!x||0===x.length||0===I.length)return[];let e=new Set,t=[];for(let s of x){let r=s.name.split("_").map(e=>e.toLowerCase()).filter(e=>e.length>1);if(0===r.length)continue;let l=e=>e.toLowerCase().replace(/[-_/]/g," "),a=I.find(t=>{if(e.has(t.name))return!1;let s=l(t.name);return r.every(e=>s.includes(e))});if(!a){let t=r.find(e=>e.length>3)??r[r.length-1];a=I.find(s=>!e.has(s.name)&&l(s.name).includes(t))}a&&(t.push(a),e.add(a.name))}return t},[x,I]),E=(0,b.useMemo)(()=>new Set(F.map(e=>e.name)),[F]),L=(0,b.useMemo)(()=>I.filter(e=>{let t=y.toLowerCase();return e.name.toLowerCase().includes(t)||e.description&&e.description.toLowerCase().includes(t)}),[I,y]),R=(0,b.useMemo)(()=>L.filter(e=>E.has(e.name)),[L,E]),U=(0,b.useMemo)(()=>L.filter(e=>!E.has(e.name)),[L,E]);(0,b.useEffect)(()=>{let e=I.map(e=>e.name).sort().join(","),t=j.current.map(e=>e.name).sort().join(","),s=F.map(e=>e.name).sort().join(",");if(s!==S.current&&(S.current=s,""!==s&&(w.current=!1)),I.length>0&&e!==t){let e=I.map(e=>e.name);w.current?n(l.filter(t=>e.includes(t))):(w.current=!0,a&&a.length>0?n(a.filter(t=>e.includes(t))):F.length>0?n(F.map(e=>e.name).filter(t=>e.includes(t))):n(e))}j.current=I},[I,l,a,n,F]);let z=e=>{l.includes(e)?n(l.filter(t=>t!==e)):n([...l,e])},B=(e,t)=>{t.stopPropagation(),T(t=>{let s=new Set(t);return s.has(e)?s.delete(e):s.add(e),s})},q=(e,t)=>{let s={...i};t?s[e]=t:delete s[e],c(s)},V=(e,t)=>{let s={...o};t?s[e]=t:delete s[e],u(s)};return M||r.url||r.spec_path?(0,t.jsx)(eg.Card,{children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-blue-600"}),(0,t.jsx)(m.Title,{children:"Tool Configuration"}),I.length>0&&(0,t.jsx)(ex.Badge,{count:I.length,style:{backgroundColor:"#52c41a"}})]}),I.length>0&&(0,t.jsx)(eT.Radio.Group,{value:N,onChange:e=>_(e.target.value),size:"small",optionType:"button",buttonStyle:"solid",options:[{label:"Risk Groups",value:"crud"},{label:"Flat List",value:"flat"}]})]}),(0,t.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,t.jsxs)(d.Text,{className:"text-blue-800 text-sm",children:[(0,t.jsx)("strong",{children:"Select which tools users can call:"})," Only checked tools will be available for users to invoke. Unchecked tools will be blocked from execution."]})}),P&&(0,t.jsxs)("div",{className:"flex items-center justify-center py-6",children:[(0,t.jsx)(W.Spin,{size:"large"}),(0,t.jsx)(d.Text,{className:"ml-3",children:"Loading tools from spec..."})]}),O&&!P&&(0,t.jsxs)("div",{className:"text-center py-6 text-red-500 border rounded-lg border-dashed border-red-300 bg-red-50",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{className:"text-red-600 font-medium",children:"Unable to load tools"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm text-red-500",children:O})]}),!P&&!O&&0===I.length&&M&&(x&&x.length>0?(0,t.jsxs)("div",{className:"text-center py-4 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"No tools loaded from spec"}),(0,t.jsxs)(d.Text,{className:"text-sm block mt-1",children:["Expected tools: ",x.map(e=>e.name).join(", ")]})]}):(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"No tools available for configuration"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm",children:"Connect to an MCP server with tools to configure them"})]})),!M&&(r.url||r.spec_path)&&(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"Complete required fields to configure tools"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm",children:"Fill in URL, Transport, and Authentication to load available tools"})]}),!P&&!O&&I.length>0&&(0,t.jsxs)("div",{className:"space-y-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 p-3 bg-green-50 rounded-lg border border-green-200",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"text-green-600"}),(0,t.jsxs)(d.Text,{className:"text-green-700 font-medium",children:[l.length," of ",I.length," ",1===I.length?"tool":"tools"," enabled for user access"]})]}),(0,t.jsx)(H.Input,{placeholder:"Search tools by name or description...",prefix:(0,t.jsx)(ew.SearchOutlined,{className:"text-gray-400"}),value:y,onChange:e=>v(e.target.value),allowClear:!0,className:"rounded-lg",size:"large"}),"crud"===N&&(0,t.jsx)(eA.default,{tools:I,searchFilter:y,value:0===l.length?void 0:l,onChange:e=>n(e)}),"flat"===N&&(0,t.jsx)(t.Fragment,{children:0===L.length?(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ew.SearchOutlined,{className:"text-2xl mb-2"}),(0,t.jsxs)(d.Text,{children:['No tools found matching "',y,'"']})]}):(0,t.jsxs)("div",{className:"space-y-2",children:[R.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between px-1",children:[(0,t.jsx)("p",{className:"text-xs font-semibold text-gray-500 uppercase tracking-wide",children:"Suggested tools"}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)("button",{type:"button",onClick:()=>{let e=F.map(e=>e.name);n([...l.filter(e=>!E.has(e)),...e])},className:"text-xs text-blue-600 hover:text-blue-700",children:"Enable all"}),(0,t.jsx)("button",{type:"button",onClick:()=>{n(l.filter(e=>!E.has(e)))},className:"text-xs text-gray-500 hover:text-gray-700",children:"Disable all"})]})]}),R.map(e=>(0,t.jsx)(eI,{tool:e,isEnabled:l.includes(e.name),isEditExpanded:C.has(e.name),toolNameToDisplayName:i,toolNameToDescription:o,onToggle:z,onToggleExpand:B,onDisplayNameChange:q,onDescriptionChange:V},e.name))]}),U.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between px-1 pt-2",children:[(0,t.jsx)("p",{className:"text-xs font-semibold text-gray-500 uppercase tracking-wide",children:R.length>0?"All tools":"Tools"}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)("button",{type:"button",onClick:()=>{let e=I.filter(e=>!E.has(e.name)).map(e=>e.name),t=new Set(l);n([...l,...e.filter(e=>!t.has(e))])},className:"text-xs text-blue-600 hover:text-blue-700",children:"Enable all"}),(0,t.jsx)("button",{type:"button",onClick:()=>{n(l.filter(e=>E.has(e)))},className:"text-xs text-gray-500 hover:text-gray-700",children:"Disable all"})]})]}),U.map(e=>(0,t.jsx)(eI,{tool:e,isEnabled:l.includes(e.name),isEditExpanded:C.has(e.name),toolNameToDisplayName:i,toolNameToDescription:o,onToggle:z,onToggleExpand:B,onDisplayNameChange:q,onDescriptionChange:V},e.name))]})]})})]})]})}):null},eO=({isVisible:e,required:s=!0})=>e?(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Stdio Configuration (JSON)",(0,t.jsx)(g.Tooltip,{title:"Paste your stdio MCP server configuration in JSON format. You can use the full mcpServers structure from config.yaml or just the inner server configuration.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"stdio_config",rules:[...s?[{required:!0,message:"Please enter stdio configuration"}]:[],{validator:(e,t)=>{if(!t)return Promise.resolve();try{return JSON.parse(t),Promise.resolve()}catch{return Promise.reject("Please enter valid JSON")}}}],children:(0,t.jsx)(H.Input.TextArea,{placeholder:`{ +======== }`,a=async()=>{try{await navigator.clipboard.writeText(l),s(!0),setTimeout(()=>s(!1),1500)}catch{}};return(0,t.jsxs)("div",{className:"mb-6 rounded-lg border border-gray-200 bg-gray-50 px-5 py-4",children:[(0,t.jsx)("p",{className:"text-sm font-medium text-gray-700 mb-1",children:"How toolsets work"}),(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-3",children:["Create a toolset, assign it to a key via ",(0,t.jsx)("span",{className:"font-medium text-gray-700",children:"API Keys → Edit Key → MCP Servers"}),", then point your MCP client at the toolset URL. The client only sees the tools you picked."]}),(0,t.jsx)("div",{className:"text-xs text-gray-400 mb-1",children:"Claude Code / Cursor config"}),(0,t.jsxs)("div",{className:"relative",children:[(0,t.jsx)("pre",{className:"bg-white border border-gray-200 rounded px-4 py-3 text-xs font-mono text-gray-700 overflow-x-auto leading-relaxed pr-14",children:l}),(0,t.jsx)("button",{type:"button",onClick:a,className:"absolute top-2 right-2 px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 text-gray-400 hover:text-gray-600 border-gray-200 transition-colors",children:e?"✓":"copy"})]})]})}function er({accessToken:e,userRole:s}){let r=(0,v.useQueryClient)(),{data:a=[],isLoading:n}=(0,Q.useMCPToolsets)(),[i,o]=(0,b.useState)(!1),[c,u]=(0,b.useState)(null),[x,p]=(0,b.useState)(null),[g,f]=(0,b.useState)(!1),j="Admin"===s||"proxy_admin"===s,y=async(t,s,l)=>{e&&(await (0,_.createMCPToolset)(e,{toolset_name:t,description:s,tools:l}),K.message.success("Toolset created"),r.invalidateQueries({queryKey:["mcpToolsets"]}))},N=async(t,s,l)=>{e&&c&&(await (0,_.updateMCPToolset)(e,{toolset_id:c.toolset_id,toolset_name:t,description:s,tools:l}),K.message.success("Toolset updated"),r.invalidateQueries({queryKey:["mcpToolsets"]}),u(null))},w=async()=>{if(e&&x){f(!0);try{await (0,_.deleteMCPToolset)(e,x),K.message.success("Toolset deleted"),r.invalidateQueries({queryKey:["mcpToolsets"]}),p(null)}finally{f(!1)}}},S=(0,_.getProxyBaseUrl)(),C=[{header:"Toolset ID",accessorKey:"toolset_id",cell:({row:e})=>(0,t.jsxs)("span",{className:"font-mono text-xs bg-gray-100 px-2 py-0.5 rounded text-gray-600",children:[e.original.toolset_id.slice(0,8),"…"]})},{header:"Name",accessorKey:"toolset_name",cell:({row:e})=>{let s=`${S}/toolset/${e.original.toolset_name}/mcp`;return(0,t.jsxs)("div",{className:"flex flex-col gap-0.5",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"inline-block w-2 h-2 rounded-full bg-purple-500 flex-shrink-0"}),(0,t.jsx)("span",{className:"font-medium text-gray-900",children:e.original.toolset_name})]}),(0,t.jsx)("button",{type:"button",className:"text-xs text-gray-400 hover:text-purple-600 font-mono truncate max-w-xs text-left transition-colors",onClick:()=>navigator.clipboard.writeText(s),title:"Click to copy endpoint URL",children:s})]})}},{header:"Description",accessorKey:"description",cell:({row:e})=>(0,t.jsx)("span",{className:"text-sm text-gray-500",children:e.original.description||"—"})},{header:"Tools",accessorKey:"tools",cell:({row:e})=>{let s=e.original.tools;return(0,t.jsxs)("div",{className:"flex flex-wrap gap-1 max-w-xs",children:[s.slice(0,4).map((e,s)=>(0,t.jsx)("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded bg-purple-50 border border-purple-200 text-purple-700 text-xs",children:e.tool_name},s)),s.length>4&&(0,t.jsxs)("span",{className:"text-xs text-gray-400 self-center",children:["+",s.length-4," more"]})]})}},{header:"Created",accessorKey:"created_at",cell:({row:e})=>(0,t.jsx)("span",{className:"text-xs text-gray-500",children:e.original.created_at?new Date(e.original.created_at).toLocaleDateString():"—"})},...j?[{header:"",id:"actions",cell:({row:e})=>(0,t.jsxs)("div",{className:"flex items-center gap-1 justify-end",children:[(0,t.jsx)("button",{type:"button",className:"p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-700 transition-colors",onClick:()=>u(e.original),children:(0,t.jsx)(Y.PencilIcon,{className:"h-4 w-4"})}),(0,t.jsx)("button",{type:"button",className:"p-1.5 rounded-lg hover:bg-red-50 text-gray-400 hover:text-red-500 transition-colors",onClick:()=>p(e.original.toolset_id),children:(0,t.jsx)(G.TrashIcon,{className:"h-4 w-4"})})]})}]:[]];return(0,t.jsxs)("div",{className:"mt-4",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(m.Title,{children:"MCP Toolsets"}),(0,t.jsx)(d.Text,{className:"text-gray-500 text-sm",children:"Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP permissions dropdown."})]}),j&&(0,t.jsx)(l.Button,{icon:J.PlusIcon,onClick:()=>o(!0),children:"New Toolset"})]}),(0,t.jsx)(es,{}),(0,t.jsx)(Z.DataTable,{data:a,columns:C,renderSubComponent:()=>(0,t.jsx)("div",{}),getRowCanExpand:()=>!1,isLoading:n,noDataMessage:"No toolsets yet. Click 'New Toolset' to create one.",loadingMessage:"Loading toolsets...",enableSorting:!0}),(0,t.jsx)(et,{open:i,onClose:()=>o(!1),onSave:y,accessToken:e}),c&&(0,t.jsx)(et,{open:!!c,onClose:()=>u(null),onSave:N,accessToken:e,initialToolset:c}),(0,t.jsx)(h.Modal,{open:!!x,onCancel:()=>p(null),onOk:w,okText:"Delete",okButtonProps:{danger:!0,loading:g},title:"Delete Toolset",children:(0,t.jsx)("p",{children:"Are you sure you want to delete this toolset? Keys and teams using it will lose access to the scoped tools."})})]})}var el=e.i(790848),ea=e.i(362024),en=e.i(827252),ei=e.i(779241),eo=e.i(292335),ec=e.i(28651);let ed="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500",em=({label:e,tooltip:s})=>(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:[e,(0,t.jsx)(g.Tooltip,{title:s,children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),eu=({isM2M:e,isEditing:s=!1,oauthFlow:r,initialFlowType:a,docsUrl:n})=>{let i=s?" (leave blank to keep existing)":"";return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"OAuth Flow Type",tooltip:"Choose how the proxy authenticates with this MCP server. M2M is for server-to-server communication using client credentials. Interactive (PKCE) is for user-facing flows that require browser-based authorization."}),name:"oauth_flow_type",...a?{initialValue:a}:{},children:(0,t.jsxs)(p.Select,{className:"rounded-lg",size:"large",children:[(0,t.jsx)(p.Select.Option,{value:eo.OAUTH_FLOW.M2M,children:(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"font-medium",children:"Machine-to-Machine (M2M)"}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-2",children:"server-to-server, no user interaction"})]})}),(0,t.jsx)(p.Select.Option,{value:eo.OAUTH_FLOW.INTERACTIVE,children:(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"font-medium",children:"Interactive (PKCE)"}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-2",children:"browser-based user authorization"})]})})]})}),e?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Client ID",tooltip:"OAuth2 client ID for the client_credentials grant."}),name:["credentials","client_id"],rules:[{required:!0,message:"Client ID is required for M2M OAuth"}],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter OAuth client ID${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Client Secret",tooltip:"OAuth2 client secret for the client_credentials grant."}),name:["credentials","client_secret"],rules:[{required:!0,message:"Client Secret is required for M2M OAuth"}],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter OAuth client secret${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token URL",tooltip:"Token endpoint URL for the client_credentials grant."}),name:"token_url",rules:[{required:!0,message:"Token URL is required for M2M OAuth"}],children:(0,t.jsx)(ei.TextInput,{placeholder:"https://auth.example.com/oauth/token",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Scopes (optional)",tooltip:"Optional scopes to request with the client_credentials grant."}),name:["credentials","scopes"],children:(0,t.jsx)(p.Select,{mode:"tags",tokenSeparators:[","],placeholder:"Add scopes",className:"rounded-lg",size:"large"})})]}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"flex items-center justify-between w-full",children:[(0,t.jsx)(em,{label:"Client ID (optional)",tooltip:"Provide only if your MCP server cannot handle dynamic client registration."}),n&&(0,t.jsx)("a",{href:n,target:"_blank",rel:"noopener noreferrer",className:"text-xs text-blue-500 hover:text-blue-700 ml-2 font-normal",onClick:e=>e.stopPropagation(),children:"Create OAuth App →"})]}),name:["credentials","client_id"],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter client ID${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Client Secret (optional)",tooltip:"Provide only if your MCP server cannot handle dynamic client registration."}),name:["credentials","client_secret"],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:`Enter client secret${i}`,className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Scopes (optional)",tooltip:"Optional scopes requested during token exchange. Separate multiple scopes with enter or commas."}),name:["credentials","scopes"],children:(0,t.jsx)(p.Select,{mode:"tags",tokenSeparators:[","],placeholder:"Add scopes",className:"rounded-lg",size:"large"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Authorization URL (optional)",tooltip:"Optional override for the authorization endpoint."}),name:"authorization_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://example.com/oauth/authorize",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token URL (optional)",tooltip:"Optional override for the token endpoint."}),name:"token_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://example.com/oauth/token",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Registration URL (optional)",tooltip:"Optional override for the dynamic client registration endpoint."}),name:"registration_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://example.com/oauth/register",className:ed})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token Validation Rules (optional)",tooltip:'JSON object of key-value rules checked against the OAuth token response before storing. Supports dot-notation for nested fields (e.g. {"organization": "my-org", "team.id": "123"}). Tokens that fail validation are rejected with HTTP 403.'}),name:"token_validation_json",rules:[{validator:(e,t)=>{if(!t||""===t.trim())return Promise.resolve();try{return JSON.parse(t),Promise.resolve()}catch{return Promise.reject(Error("Must be valid JSON"))}}}],children:(0,t.jsx)(H.Input.TextArea,{placeholder:'{\n "organization": "my-org",\n "team.id": "123"\n}',rows:4,className:"font-mono text-sm rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)(em,{label:"Token Storage TTL (seconds, optional)",tooltip:"How long to cache each user's OAuth access token in Redis before evicting it (regardless of the token's own expires_in). Leave blank to derive the TTL from the token's expires_in, or fall back to the 12-hour default."}),name:"token_storage_ttl_seconds",children:(0,t.jsx)(ec.InputNumber,{min:1,placeholder:"e.g. 3600",className:"w-full rounded-lg",style:{width:"100%"}})}),r&&(0,t.jsxs)("div",{className:"rounded-lg border border-dashed border-gray-300 p-4 space-y-2",children:[(0,t.jsx)("p",{className:"text-sm text-gray-600",children:"Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value."}),(0,t.jsx)(l.Button,{variant:"secondary",onClick:r.startOAuthFlow,disabled:"authorizing"===r.status||"exchanging"===r.status,children:"authorizing"===r.status?"Waiting for authorization...":"exchanging"===r.status?"Exchanging authorization code...":"Authorize & Fetch Token"}),r.error&&(0,t.jsx)("p",{className:"text-sm text-red-500",children:r.error}),"success"===r.status&&r.tokenResponse?.access_token&&(0,t.jsxs)("p",{className:"text-sm text-green-600",children:["Token fetched. Expires in ",r.tokenResponse.expires_in??"?"," seconds."]})]})]})]})};var ex=e.i(906579),eh=e.i(458505),ep=e.i(366308),eg=e.i(304967);let ef=({value:e={},onChange:s,tools:r=[],disabled:l=!1})=>(0,t.jsx)(eg.Card,{children:(0,t.jsxs)("div",{className:"space-y-6",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-4",children:[(0,t.jsx)(eh.DollarOutlined,{className:"text-green-600"}),(0,t.jsx)(m.Title,{children:"Cost Configuration"}),(0,t.jsx)(g.Tooltip,{title:"Configure costs for this MCP server's tool calls. Set a default rate and per-tool overrides.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-gray-400"})})]}),(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:["Default Cost per Query ($)",(0,t.jsx)(g.Tooltip,{title:"Default cost charged for each tool call to this server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-1 text-gray-400"})})]}),(0,t.jsx)(ec.InputNumber,{min:0,step:1e-4,precision:4,placeholder:"0.0000",value:e.default_cost_per_query,onChange:t=>{let r={...e,default_cost_per_query:t};s?.(r)},disabled:l,style:{width:"200px"},addonBefore:"$"}),(0,t.jsx)(d.Text,{className:"block mt-1 text-gray-500 text-sm",children:"Set a default cost for all tool calls to this server"})]}),r.length>0&&(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("label",{className:"block text-sm font-medium text-gray-700",children:["Tool-Specific Costs ($)",(0,t.jsx)(g.Tooltip,{title:"Override the default cost for specific tools. Leave blank to use the default rate.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-1 text-gray-400"})})]}),(0,t.jsx)(ea.Collapse,{items:[{key:"1",label:(0,t.jsxs)("div",{className:"flex items-center",children:[(0,t.jsx)(ep.ToolOutlined,{className:"mr-2 text-blue-500"}),(0,t.jsx)("span",{className:"font-medium",children:"Available Tools"}),(0,t.jsx)(ex.Badge,{count:r.length,style:{backgroundColor:"#52c41a",marginLeft:"8px"}})]}),children:(0,t.jsx)("div",{className:"space-y-3 max-h-64 overflow-y-auto",children:r.map((r,a)=>(0,t.jsxs)("div",{className:"flex items-center justify-between p-3 bg-gray-50 rounded-lg",children:[(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)(d.Text,{className:"font-medium text-gray-900",children:r.name}),r.description&&(0,t.jsx)(d.Text,{className:"text-gray-500 text-sm block mt-1",children:r.description})]}),(0,t.jsx)("div",{className:"ml-4",children:(0,t.jsx)(ec.InputNumber,{min:0,step:1e-4,precision:4,placeholder:"Use default",value:e.tool_name_to_cost_per_query?.[r.name],onChange:t=>{var l;let a;return l=r.name,a={...e,tool_name_to_cost_per_query:{...e.tool_name_to_cost_per_query,[l]:t}},void s?.(a)},disabled:l,style:{width:"120px"},addonBefore:"$"})})]},a))})}]})]})]}),(e.default_cost_per_query||e.tool_name_to_cost_per_query&&Object.keys(e.tool_name_to_cost_per_query).length>0)&&(0,t.jsxs)("div",{className:"mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg",children:[(0,t.jsx)(d.Text,{className:"text-blue-800 font-medium",children:"Cost Summary:"}),(0,t.jsxs)("div",{className:"mt-2 space-y-1",children:[e.default_cost_per_query&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• Default cost: $",e.default_cost_per_query.toFixed(4)," per query"]}),e.tool_name_to_cost_per_query&&Object.entries(e.tool_name_to_cost_per_query).map(([e,s])=>null!=s&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• ",e,": $",s.toFixed(4)," per query"]},e))]})]})]})});var eb=e.i(464571),ej=e.i(560445),ey=e.i(245704),ev=e.i(270377),eN=e.i(91979);let e_=({formValues:e,tools:s,isLoadingTools:r,toolsError:l,toolsErrorStackTrace:a,canFetchTools:n,fetchTools:i})=>n||e.url||e.spec_path?(0,t.jsx)(eg.Card,{children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"text-blue-600"}),(0,t.jsx)(m.Title,{children:"Connection Status"})]}),!n&&(e.url||e.spec_path)&&(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"Complete required fields to test connection"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm",children:"Fill in URL, Transport, and Authentication to test MCP server connection"})]}),n&&(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"text-gray-700 font-medium",children:r?"Testing connection to MCP server...":s.length>0?"Connection successful":l?"Connection failed":"Ready to test connection"}),(0,t.jsx)("br",{}),(0,t.jsxs)(d.Text,{className:"text-gray-500 text-sm",children:["Server: ",e.url||e.spec_path]})]}),r&&(0,t.jsxs)("div",{className:"flex items-center text-blue-600",children:[(0,t.jsx)(W.Spin,{size:"small",className:"mr-2"}),(0,t.jsx)(d.Text,{className:"text-blue-600",children:"Connecting..."})]}),!r&&!l&&s.length>0&&(0,t.jsxs)("div",{className:"flex items-center text-green-600",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"mr-1"}),(0,t.jsx)(d.Text,{className:"text-green-600 font-medium",children:"Connected"})]}),l&&(0,t.jsxs)("div",{className:"flex items-center text-red-600",children:[(0,t.jsx)(ev.ExclamationCircleOutlined,{className:"mr-1"}),(0,t.jsx)(d.Text,{className:"text-red-600 font-medium",children:"Failed"})]})]}),r&&(0,t.jsxs)("div",{className:"flex items-center justify-center py-6",children:[(0,t.jsx)(W.Spin,{size:"large"}),(0,t.jsx)(d.Text,{className:"ml-3",children:"Testing connection and loading tools..."})]}),l&&(0,t.jsx)(ej.Alert,{message:"Connection Failed",description:(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{children:l}),a&&(0,t.jsx)(ea.Collapse,{items:[{key:"stack-trace",label:"Stack Trace",children:(0,t.jsx)("pre",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word",fontSize:"12px",fontFamily:"monospace",margin:0,padding:"8px",backgroundColor:"#f5f5f5",borderRadius:"4px",maxHeight:"400px",overflow:"auto"},children:a})}],style:{marginTop:"12px"}})]}),type:"error",showIcon:!0,action:(0,t.jsx)(eb.Button,{icon:(0,t.jsx)(eN.ReloadOutlined,{}),onClick:i,size:"small",children:"Retry"})}),!r&&0===s.length&&!l&&(0,t.jsxs)("div",{className:"text-center py-6 text-gray-500 border rounded-lg border-dashed",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"text-2xl mb-2 text-green-500"}),(0,t.jsx)(d.Text,{className:"text-green-600 font-medium",children:"Connection successful!"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-gray-500",children:"No tools found for this MCP server"})]})]})]})}):null;var ew=e.i(928685),eS=e.i(751904),eC=e.i(536916),eT=e.i(91739);let ek=({accessToken:e,oauthAccessToken:t,formValues:s,enabled:r=!0})=>{let[l,a]=(0,b.useState)([]),[n,i]=(0,b.useState)(!1),[o,c]=(0,b.useState)(null),[d,m]=(0,b.useState)(null),[u,x]=(0,b.useState)(!1),h=s.auth_type===eo.AUTH_TYPE.OAUTH2&&s.oauth_flow_type===eo.OAUTH_FLOW.M2M,p=s.auth_type===eo.AUTH_TYPE.OAUTH2&&!h,g=s.transport===eo.TRANSPORT.OPENAPI,f=g?!!s.spec_path:!!s.url,j=g?!!(f&&e):!!(f&&s.transport&&s.auth_type&&e&&(!p||t)),y=JSON.stringify(s.static_headers??{}),v=JSON.stringify(s.credentials??{}),N=async()=>{if(e&&(s.url||s.spec_path)&&(!p||t||g)){i(!0),c(null);try{let r=Array.isArray(s.static_headers)?s.static_headers.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value!=null?String(t.value):""),e},{}):!Array.isArray(s.static_headers)&&s.static_headers&&"object"==typeof s.static_headers?Object.entries(s.static_headers).reduce((e,[t,s])=>(t&&(e[t]=null!=s?String(s):""),e),{}):{},l=s.credentials&&"object"==typeof s.credentials?Object.entries(s.credentials).reduce((e,[t,s])=>{if(null==s||""===s)return e;if("scopes"===t){if(Array.isArray(s)){let r=s.filter(e=>null!=e&&""!==e);r.length>0&&(e[t]=r)}}else e[t]=s;return e},{}):void 0,n=s.transport===eo.TRANSPORT.OPENAPI?"http":s.transport,i={server_id:s.server_id||"",server_name:s.server_name||"",url:s.url,spec_path:s.spec_path,transport:n,auth_type:s.auth_type,authorization_url:s.authorization_url,token_url:s.token_url,registration_url:s.registration_url,mcp_info:s.mcp_info,static_headers:r};l&&Object.keys(l).length>0&&(i.credentials=l);let o=await (0,_.testMCPToolsListRequest)(e,i,t);if(o.tools&&!o.error)a(o.tools),c(null),m(null),o.tools.length>0&&!u&&x(!0);else{let e=o.message||"Failed to retrieve tools list";c(e),m(o.stack_trace||null),a([]),x(!1)}}catch(e){console.error("Tools fetch error:",e),c(e instanceof Error?e.message:String(e)),m(null),a([]),x(!1)}finally{i(!1)}}},w=()=>{a([]),c(null),m(null),x(!1)};return(0,b.useEffect)(()=>{r&&(j?N():w())},[s.url,s.spec_path,s.transport,s.auth_type,e,r,t,j,y,v]),{tools:l,isLoadingTools:n,toolsError:o,toolsErrorStackTrace:d,hasShownSuccessMessage:u,canFetchTools:j,fetchTools:N,clearTools:w}};var eA=e.i(531516);let eI=({tool:e,isEnabled:s,isEditExpanded:r,toolNameToDisplayName:l,toolNameToDescription:a,onToggle:n,onToggleExpand:i,onDisplayNameChange:o,onDescriptionChange:c})=>(0,t.jsxs)("div",{className:`rounded-lg border transition-colors ${s?"bg-blue-50 border-blue-300 hover:border-blue-400":"bg-gray-50 border-gray-200 hover:border-gray-300"}`,children:[(0,t.jsx)("div",{className:"p-4 cursor-pointer",onClick:()=>n(e.name),children:(0,t.jsxs)("div",{className:"flex items-start gap-3",children:[(0,t.jsx)(eC.Checkbox,{checked:s,onChange:()=>n(e.name)}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"font-medium text-gray-900",children:l[e.name]||e.name}),(0,t.jsx)("span",{className:`px-2 py-0.5 text-xs rounded-full font-medium ${s?"bg-green-100 text-green-800":"bg-red-100 text-red-800"}`,children:s?"Enabled":"Disabled"}),l[e.name]&&(0,t.jsx)("span",{className:"px-2 py-0.5 text-xs rounded-full font-medium bg-purple-100 text-purple-800",children:"Custom name"})]}),(a[e.name]||e.description)&&(0,t.jsx)(d.Text,{className:"text-gray-500 text-sm block mt-1",children:a[e.name]||e.description}),(0,t.jsx)(d.Text,{className:"text-gray-400 text-xs block mt-1",children:s?"✓ Users can call this tool":"✗ Users cannot call this tool"})]}),(0,t.jsx)("button",{type:"button",onClick:t=>i(e.name,t),className:`p-1.5 rounded-md transition-colors ${r?"bg-blue-100 text-blue-600":"text-gray-400 hover:text-gray-600 hover:bg-gray-100"}`,title:"Edit display name and description",children:(0,t.jsx)(eS.EditOutlined,{})})]})}),r&&(0,t.jsxs)("div",{className:"px-4 pb-4 pt-3 border-t border-gray-200 space-y-3 bg-gray-50 rounded-b-lg",onClick:e=>e.stopPropagation(),children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-600 mb-1 block",children:"Display Name"}),(0,t.jsx)(H.Input,{placeholder:e.name,value:l[e.name]||"",onChange:t=>o(e.name,t.target.value)}),(0,t.jsx)(d.Text,{className:"text-xs text-gray-400 mt-1 block",children:"Override how this tool's name appears to users. Leave blank to use original."})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-600 mb-1 block",children:"Description"}),(0,t.jsx)(H.Input.TextArea,{placeholder:e.description||"No description",value:a[e.name]||"",onChange:t=>c(e.name,t.target.value),rows:2}),(0,t.jsx)(d.Text,{className:"text-xs text-gray-400 mt-1 block",children:"Override the tool description shown to users. Leave blank to use original."})]})]})]}),eP=({accessToken:e,oauthAccessToken:s,formValues:r,allowedTools:l,existingAllowedTools:a,onAllowedToolsChange:n,toolNameToDisplayName:i,toolNameToDescription:o,onToolNameToDisplayNameChange:c,onToolNameToDescriptionChange:u,keyTools:x,externalTools:h,externalIsLoading:p,externalError:g,externalCanFetch:f})=>{let j=(0,b.useRef)([]),[y,v]=(0,b.useState)(""),[N,_]=(0,b.useState)("crud"),w=(0,b.useRef)(!1),S=(0,b.useRef)(""),[C,T]=(0,b.useState)(new Set),k=void 0!==h,A=ek({accessToken:e,oauthAccessToken:s,formValues:r,enabled:!k}),I=k?h:A.tools,P=k?p??!1:A.isLoadingTools,O=k?g??null:A.toolsError,M=k?f??!1:A.canFetchTools,F=(0,b.useMemo)(()=>{if(!x||0===x.length||0===I.length)return[];let e=new Set,t=[];for(let s of x){let r=s.name.split("_").map(e=>e.toLowerCase()).filter(e=>e.length>1);if(0===r.length)continue;let l=e=>e.toLowerCase().replace(/[-_/]/g," "),a=I.find(t=>{if(e.has(t.name))return!1;let s=l(t.name);return r.every(e=>s.includes(e))});if(!a){let t=r.find(e=>e.length>3)??r[r.length-1];a=I.find(s=>!e.has(s.name)&&l(s.name).includes(t))}a&&(t.push(a),e.add(a.name))}return t},[x,I]),E=(0,b.useMemo)(()=>new Set(F.map(e=>e.name)),[F]),L=(0,b.useMemo)(()=>I.filter(e=>{let t=y.toLowerCase();return e.name.toLowerCase().includes(t)||e.description&&e.description.toLowerCase().includes(t)}),[I,y]),R=(0,b.useMemo)(()=>L.filter(e=>E.has(e.name)),[L,E]),z=(0,b.useMemo)(()=>L.filter(e=>!E.has(e.name)),[L,E]);(0,b.useEffect)(()=>{let e=I.map(e=>e.name).sort().join(","),t=j.current.map(e=>e.name).sort().join(","),s=F.map(e=>e.name).sort().join(",");if(s!==S.current&&(S.current=s,""!==s&&(w.current=!1)),I.length>0&&e!==t){let e=I.map(e=>e.name);w.current?n(l.filter(t=>e.includes(t))):(w.current=!0,a&&a.length>0?n(a.filter(t=>e.includes(t))):F.length>0?n(F.map(e=>e.name).filter(t=>e.includes(t))):n(e))}j.current=I},[I,l,a,n,F]);let U=e=>{l.includes(e)?n(l.filter(t=>t!==e)):n([...l,e])},B=(e,t)=>{t.stopPropagation(),T(t=>{let s=new Set(t);return s.has(e)?s.delete(e):s.add(e),s})},q=(e,t)=>{let s={...i};t?s[e]=t:delete s[e],c(s)},V=(e,t)=>{let s={...o};t?s[e]=t:delete s[e],u(s)};return M||r.url||r.spec_path?(0,t.jsx)(eg.Card,{children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-blue-600"}),(0,t.jsx)(m.Title,{children:"Tool Configuration"}),I.length>0&&(0,t.jsx)(ex.Badge,{count:I.length,style:{backgroundColor:"#52c41a"}})]}),I.length>0&&(0,t.jsx)(eT.Radio.Group,{value:N,onChange:e=>_(e.target.value),size:"small",optionType:"button",buttonStyle:"solid",options:[{label:"Risk Groups",value:"crud"},{label:"Flat List",value:"flat"}]})]}),(0,t.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,t.jsxs)(d.Text,{className:"text-blue-800 text-sm",children:[(0,t.jsx)("strong",{children:"Select which tools users can call:"})," Only checked tools will be available for users to invoke. Unchecked tools will be blocked from execution."]})}),P&&(0,t.jsxs)("div",{className:"flex items-center justify-center py-6",children:[(0,t.jsx)(W.Spin,{size:"large"}),(0,t.jsx)(d.Text,{className:"ml-3",children:"Loading tools from spec..."})]}),O&&!P&&(0,t.jsxs)("div",{className:"text-center py-6 text-red-500 border rounded-lg border-dashed border-red-300 bg-red-50",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{className:"text-red-600 font-medium",children:"Unable to load tools"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm text-red-500",children:O})]}),!P&&!O&&0===I.length&&M&&(x&&x.length>0?(0,t.jsxs)("div",{className:"text-center py-4 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"No tools loaded from spec"}),(0,t.jsxs)(d.Text,{className:"text-sm block mt-1",children:["Expected tools: ",x.map(e=>e.name).join(", ")]})]}):(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"No tools available for configuration"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm",children:"Connect to an MCP server with tools to configure them"})]})),!M&&(r.url||r.spec_path)&&(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ep.ToolOutlined,{className:"text-2xl mb-2"}),(0,t.jsx)(d.Text,{children:"Complete required fields to configure tools"}),(0,t.jsx)("br",{}),(0,t.jsx)(d.Text,{className:"text-sm",children:"Fill in URL, Transport, and Authentication to load available tools"})]}),!P&&!O&&I.length>0&&(0,t.jsxs)("div",{className:"space-y-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 p-3 bg-green-50 rounded-lg border border-green-200",children:[(0,t.jsx)(ey.CheckCircleOutlined,{className:"text-green-600"}),(0,t.jsxs)(d.Text,{className:"text-green-700 font-medium",children:[l.length," of ",I.length," ",1===I.length?"tool":"tools"," enabled for user access"]})]}),(0,t.jsx)(H.Input,{placeholder:"Search tools by name or description...",prefix:(0,t.jsx)(ew.SearchOutlined,{className:"text-gray-400"}),value:y,onChange:e=>v(e.target.value),allowClear:!0,className:"rounded-lg",size:"large"}),"crud"===N&&(0,t.jsx)(eA.default,{tools:I,searchFilter:y,value:0===l.length?void 0:l,onChange:e=>n(e)}),"flat"===N&&(0,t.jsx)(t.Fragment,{children:0===L.length?(0,t.jsxs)("div",{className:"text-center py-6 text-gray-400 border rounded-lg border-dashed",children:[(0,t.jsx)(ew.SearchOutlined,{className:"text-2xl mb-2"}),(0,t.jsxs)(d.Text,{children:['No tools found matching "',y,'"']})]}):(0,t.jsxs)("div",{className:"space-y-2",children:[R.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between px-1",children:[(0,t.jsx)("p",{className:"text-xs font-semibold text-gray-500 uppercase tracking-wide",children:"Suggested tools"}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)("button",{type:"button",onClick:()=>{let e=F.map(e=>e.name);n([...l.filter(e=>!E.has(e)),...e])},className:"text-xs text-blue-600 hover:text-blue-700",children:"Enable all"}),(0,t.jsx)("button",{type:"button",onClick:()=>{n(l.filter(e=>!E.has(e)))},className:"text-xs text-gray-500 hover:text-gray-700",children:"Disable all"})]})]}),R.map(e=>(0,t.jsx)(eI,{tool:e,isEnabled:l.includes(e.name),isEditExpanded:C.has(e.name),toolNameToDisplayName:i,toolNameToDescription:o,onToggle:U,onToggleExpand:B,onDisplayNameChange:q,onDescriptionChange:V},e.name))]}),z.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between px-1 pt-2",children:[(0,t.jsx)("p",{className:"text-xs font-semibold text-gray-500 uppercase tracking-wide",children:R.length>0?"All tools":"Tools"}),(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)("button",{type:"button",onClick:()=>{let e=I.filter(e=>!E.has(e.name)).map(e=>e.name),t=new Set(l);n([...l,...e.filter(e=>!t.has(e))])},className:"text-xs text-blue-600 hover:text-blue-700",children:"Enable all"}),(0,t.jsx)("button",{type:"button",onClick:()=>{n(l.filter(e=>E.has(e)))},className:"text-xs text-gray-500 hover:text-gray-700",children:"Disable all"})]})]}),z.map(e=>(0,t.jsx)(eI,{tool:e,isEnabled:l.includes(e.name),isEditExpanded:C.has(e.name),toolNameToDisplayName:i,toolNameToDescription:o,onToggle:U,onToggleExpand:B,onDisplayNameChange:q,onDescriptionChange:V},e.name))]})]})})]})]})}):null},eO=({isVisible:e,required:s=!0})=>e?(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Stdio Configuration (JSON)",(0,t.jsx)(g.Tooltip,{title:"Paste your stdio MCP server configuration in JSON format. You can use the full mcpServers structure from config.yaml or just the inner server configuration.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"stdio_config",rules:[...s?[{required:!0,message:"Please enter stdio configuration"}]:[],{validator:(e,t)=>{if(!t)return Promise.resolve();try{return JSON.parse(t),Promise.resolve()}catch{return Promise.reject("Please enter valid JSON")}}}],children:(0,t.jsx)(H.Input.TextArea,{placeholder:`{ +>>>>>>>> origin/litellm_internal_staging:litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js "mcpServers": { "circleci-mcp-server": { "command": "npx", @@ -16,9 +24,15 @@ } } } +<<<<<<<< HEAD:litellm/proxy/_experimental/out/_next/static/chunks/0279e5299e9f6e98.js +}`,rows:12,className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 font-mono text-sm"})}):null;var eM=e.i(770914),eF=e.i(564897),eE=e.i(646563);let{Panel:eL}=ea.Collapse,eR=({availableAccessGroups:e,mcpServer:s,searchValue:r,setSearchValue:l,getAccessGroupOptions:a})=>{let n=D.Form.useFormInstance(),i=D.Form.useWatch("auth_type",n)===eo.AUTH_TYPE.OAUTH2;return(0,b.useEffect)(()=>{if(s){if(s.static_headers){let e=Object.entries(s.static_headers).map(([e,t])=>({header:e,value:null!=t?String(t):""}));n.setFieldValue("static_headers",e)}"boolean"==typeof s.allow_all_keys&&n.setFieldValue("allow_all_keys",s.allow_all_keys),"boolean"==typeof s.available_on_public_internet&&n.setFieldValue("available_on_public_internet",s.available_on_public_internet),"boolean"==typeof s.delegate_auth_to_upstream&&n.setFieldValue("delegate_auth_to_upstream",s.delegate_auth_to_upstream)}else n.setFieldValue("allow_all_keys",!1),n.setFieldValue("available_on_public_internet",!0),n.setFieldValue("delegate_auth_to_upstream",!1)},[s,n]),(0,b.useEffect)(()=>{i||n.setFieldValue("delegate_auth_to_upstream",!1)},[i,n]),(0,t.jsx)(ea.Collapse,{className:"bg-gray-50 border border-gray-200 rounded-lg",expandIconPosition:"end",ghost:!1,children:(0,t.jsx)(eL,{header:(0,t.jsxs)("div",{className:"flex items-center",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-blue-500 rounded-full"}),(0,t.jsx)("h3",{className:"text-lg font-semibold text-gray-900",children:"Permission Management / Access Control"})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 ml-4",children:"Configure access permissions and security settings (Optional)"})]}),className:"border-0",forceRender:!0,children:(0,t.jsxs)("div",{className:"space-y-6 pt-4",children:[(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Allow All LiteLLM Keys",(0,t.jsx)(g.Tooltip,{title:"When enabled, every API key can access this MCP server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 mt-1",children:'Enable if this server should be "public" to all keys.'})]}),(0,t.jsx)(D.Form.Item,{name:"allow_all_keys",valuePropName:"checked",initialValue:s?.allow_all_keys??!1,className:"mb-0",children:(0,t.jsx)(el.Switch,{})})]}),(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Internal network only",(0,t.jsx)(g.Tooltip,{title:"When on, only requests from within your internal network are accepted. Turn off to allow external clients (other clusters, ChatGPT, etc). API key authentication is always required regardless of this setting.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 mt-1",children:"Turn on to restrict access to callers within your internal network only."})]}),(0,t.jsx)(D.Form.Item,{name:"available_on_public_internet",valuePropName:"checked",getValueProps:e=>({checked:!e}),getValueFromEvent:e=>!e,initialValue:!0,className:"mb-0",children:(0,t.jsx)(el.Switch,{})})]}),i&&(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Delegate auth to upstream (PKCE passthrough)",(0,t.jsx)(g.Tooltip,{title:"When on, LiteLLM skips its own API key/SSO check for this server and lets the client complete PKCE directly with the upstream MCP server. Only honored when Auth Type is oauth2. No spend tracking or per-key rate limiting will run on this route.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 mt-1",children:"Bypass LiteLLM auth so clients authenticate directly with the upstream OAuth MCP server."})]}),(0,t.jsx)(D.Form.Item,{name:"delegate_auth_to_upstream",valuePropName:"checked",initialValue:s?.delegate_auth_to_upstream??!1,className:"mb-0",children:(0,t.jsx)(el.Switch,{})})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["MCP Access Groups",(0,t.jsx)(g.Tooltip,{title:"Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"mcp_access_groups",className:"mb-4",children:(0,t.jsx)(p.Select,{mode:"tags",showSearch:!0,placeholder:"Select existing groups or type to create new ones",optionFilterProp:"value",filterOption:(e,t)=>(t?.value??"").toLowerCase().includes(e.toLowerCase()),onSearch:e=>l(e),tokenSeparators:[","],options:a(),maxTagCount:"responsive",allowClear:!0})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Extra Headers",(0,t.jsx)(g.Tooltip,{title:"Forward custom headers from incoming requests to this MCP server (e.g., Authorization, X-Custom-Header, User-Agent)",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})}),s?.extra_headers&&s.extra_headers.length>0&&(0,t.jsxs)("span",{className:"ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full",children:[s.extra_headers.length," configured"]})]}),name:"extra_headers",children:(0,t.jsx)(p.Select,{mode:"tags",placeholder:s?.extra_headers&&s.extra_headers.length>0?`Currently: ${s.extra_headers.join(", ")}`:"Enter header names (e.g., Authorization, X-Custom-Header)",className:"rounded-lg",size:"large",tokenSeparators:[","],allowClear:!0})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Static Headers",(0,t.jsx)(g.Tooltip,{title:"Send these key-value headers with every request to this MCP server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),required:!1,children:(0,t.jsx)(D.Form.List,{name:"static_headers",children:(e,{add:s,remove:r})=>(0,t.jsxs)("div",{className:"space-y-3",children:[e.map(({key:e,name:s,...l})=>(0,t.jsxs)(eM.Space,{className:"flex w-full",align:"baseline",size:"middle",children:[(0,t.jsx)(D.Form.Item,{...l,name:[s,"header"],className:"flex-1",rules:[{required:!0,message:"Header name is required"}],children:(0,t.jsx)(H.Input,{size:"large",allowClear:!0,className:"rounded-lg",placeholder:"Header name (e.g., X-API-Key)"})}),(0,t.jsx)(D.Form.Item,{...l,name:[s,"value"],className:"flex-1",rules:[{required:!0,message:"Header value is required"}],children:(0,t.jsx)(H.Input,{size:"large",allowClear:!0,className:"rounded-lg",placeholder:"Header value"})}),(0,t.jsx)(eF.MinusCircleOutlined,{onClick:()=>r(s),className:"text-gray-500 hover:text-red-500 cursor-pointer"})]},e)),(0,t.jsx)(eb.Button,{type:"dashed",onClick:()=>s(),icon:(0,t.jsx)(eE.PlusOutlined,{}),block:!0,children:"Add Static Header"})]})})})]})},"permissions")})},eU=({accessToken:e,selectedName:s,onSelect:r})=>{let[l,a]=(0,b.useState)([]),[n,i]=(0,b.useState)(!1),[o,c]=(0,b.useState)(new Set);return((0,b.useEffect)(()=>{e&&(i(!0),(0,_.fetchOpenAPIRegistry)(e).then(e=>a(e.apis??[])).catch(()=>a([])).finally(()=>i(!1)))},[e]),n)?(0,t.jsxs)("div",{className:"mb-4",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Popular APIs"}),(0,t.jsx)("div",{className:"flex justify-center py-6",children:(0,t.jsx)(W.Spin,{size:"small"})})]}):0===l.length?null:(0,t.jsxs)("div",{className:"mb-4",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700 block mb-2",children:"Popular APIs"}),(0,t.jsx)("div",{className:"grid grid-cols-5 gap-2",children:l.map(e=>{let l=s===e.name,a=o.has(e.name);return(0,t.jsxs)("button",{type:"button",title:e.description,onClick:()=>r(e),className:`flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all cursor-pointer + ${l?"border-blue-500 bg-blue-50 shadow-sm":"border-gray-200 hover:border-blue-300 hover:bg-gray-50"}`,children:[a?(0,t.jsx)("span",{className:"w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-sm font-bold text-gray-600",children:e.title.charAt(0)}):(0,t.jsx)("img",{src:e.icon_url,alt:e.title,className:"w-7 h-7 object-contain",onError:()=>{var t;return t=e.name,void c(e=>new Set(e).add(t))}}),(0,t.jsx)("span",{className:"text-xs text-gray-600 text-center leading-tight font-medium",children:e.title})]},e.name)})}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-2",children:"Select an API to pre-fill the spec URL and OAuth 2.0 settings, or enter your own spec URL below."})]})},ez=({form:e,accessToken:s,onValuesChange:r,onKeyToolsChange:l,onLogoUrlChange:a,onOAuthDocsUrlChange:n})=>{let[i,o]=(0,b.useState)(null);return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(eU,{accessToken:s,selectedName:i,onSelect:t=>{o(t.name),l?.(t.key_tools??[]),a?.(t.icon_url||void 0);let s={spec_path:t.spec_url};t.oauth?(s.auth_type=eo.AUTH_TYPE.OAUTH2,s.oauth_flow_type=eo.OAUTH_FLOW.INTERACTIVE,s.authorization_url=t.oauth.authorization_url,s.token_url=t.oauth.token_url,e.setFieldsValue(s),n?.(t.oauth.docs_url??null)):(e.resetFields(["auth_type","authorization_url","token_url"]),e.setFieldsValue(s),n?.(null)),r(s)}}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OpenAPI Spec URL",(0,t.jsx)(g.Tooltip,{title:"URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"spec_path",rules:[{required:!0,message:"Please enter an OpenAPI spec URL"}],children:(0,t.jsx)(H.Input,{placeholder:"https://petstore3.swagger.io/api/v3/openapi.json",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500",onChange:()=>{o(null),l?.([]),n?.(null)}})})]})};var eB=e.i(596239);let eq="/ui/assets/logos/",eV=[{name:"GitHub",url:`${eq}github.svg`},{name:"Slack",url:`${eq}slack.svg`},{name:"Notion",url:`${eq}notion.svg`},{name:"Linear",url:`${eq}linear.svg`},{name:"Jira",url:`${eq}jira.svg`},{name:"Figma",url:`${eq}figma.svg`},{name:"Gmail",url:`${eq}gmail.svg`},{name:"Google Drive",url:`${eq}google_drive.svg`},{name:"Stripe",url:`${eq}stripe.svg`},{name:"Shopify",url:`${eq}shopify.svg`},{name:"Salesforce",url:`${eq}salesforce.svg`},{name:"HubSpot",url:`${eq}hubspot.svg`},{name:"Twilio",url:`${eq}twilio.svg`},{name:"Cloudflare",url:`${eq}cloudflare.svg`},{name:"Sentry",url:`${eq}sentry.svg`},{name:"PostgreSQL",url:`${eq}postgresql.svg`},{name:"Snowflake",url:`${eq}snowflake.svg`},{name:"Zapier",url:`${eq}zapier.svg`},{name:"Google",url:`${eq}google.svg`},{name:"GitLab",url:`${eq}gitlab.svg`}],e$=({value:e,onChange:s})=>{let[r,l]=(0,b.useState)(new Set);return(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-2",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Logo"}),(0,t.jsx)(g.Tooltip,{title:"Select a well-known logo or paste a URL to any image. The logo is shown on the admin and chat pages.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-blue-400 hover:text-blue-600 cursor-help"})})]}),e&&(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3 p-3 bg-gray-50 rounded-lg border border-gray-200",children:[(0,t.jsx)("img",{src:e,alt:"Selected logo",className:"w-10 h-10 object-contain rounded",onError:e=>{e.target.style.display="none"}}),(0,t.jsx)("div",{className:"flex-1 min-w-0",children:(0,t.jsx)("div",{className:"text-xs text-gray-500 truncate",children:e})}),(0,t.jsx)("button",{type:"button",onClick:()=>s?.(void 0),className:"text-xs text-gray-400 hover:text-red-500 cursor-pointer bg-transparent border-none",children:"✕"})]}),(0,t.jsx)("div",{className:"grid grid-cols-10 gap-1.5 mb-3",children:eV.map(a=>{let n=e===a.url;return r.has(a.url)?null:(0,t.jsx)(g.Tooltip,{title:a.name,children:(0,t.jsx)("button",{type:"button",onClick:()=>{var t;return t=a.url,void s?.(e===t?void 0:t)},className:`flex items-center justify-center p-2 rounded-lg border transition-all cursor-pointer + ${n?"border-blue-500 bg-blue-50 shadow-sm":"border-gray-200 hover:border-blue-300 hover:bg-gray-50"}`,style:{width:40,height:40},children:(0,t.jsx)("img",{src:a.url,alt:a.name,className:"w-5 h-5 object-contain",onError:()=>{var e;return e=a.url,void l(t=>new Set(t).add(e))}})})},a.name)})}),(0,t.jsx)(H.Input,{prefix:(0,t.jsx)(eB.LinkOutlined,{className:"text-gray-400"}),placeholder:"Or paste a custom logo URL...",value:e&&!eV.some(t=>t.url===e)?e:"",onChange:e=>{let t=e.target.value.trim();s?.(t||void 0)},className:"rounded-lg",size:"small"})]})},eD=e=>{try{let t=e.indexOf("/mcp/");if(-1===t)return{token:null,baseUrl:e};let s=e.split("/mcp/");if(2!==s.length)return{token:null,baseUrl:e};let r=s[0]+"/mcp/",l=s[1];if(!l)return{token:null,baseUrl:e};return{token:l,baseUrl:r}}catch(t){return console.error("Error parsing MCP URL:",t),{token:null,baseUrl:e}}},eH=e=>{let{token:t}=eD(e);return{maskedUrl:(e=>{let{token:t,baseUrl:s}=eD(e);return t?s+"...":e})(e),hasToken:!!t}},eK=e=>e?/^https?:\/\/[^\s/$.?#].[^\s]*$/i.test(e)?Promise.resolve():Promise.reject("Please enter a valid URL (e.g., http://service-name.domain:1234/path or https://example.com)"):Promise.resolve(),eW=e=>e&&(e.includes("-")||e.includes(" "))?Promise.reject("Cannot contain '-' (hyphen) or spaces. Please use '_' (underscore) instead."):Promise.resolve();var eJ=e.i(122520),eY=e.i(165615),eG=e.i(434166);let eQ=({accessToken:e,getCredentials:t,getTemporaryPayload:s,onTokenReceived:r,onBeforeRedirect:l})=>{let[a,n]=(0,b.useState)("idle"),[i,o]=(0,b.useState)(null),[c,d]=(0,b.useState)(null),m=(0,b.useRef)(!1),u="litellm-mcp-oauth-flow-state",x="litellm-mcp-oauth-result",h="litellm-mcp-oauth-return-url",p=(e,t)=>{(0,eG.setSecureItem)(e,t)},g=e=>{try{return(0,eG.getSecureItem)(e)}catch(t){return console.warn(`Failed to get storage item ${e}`,t),null}},f=()=>{try{window.sessionStorage.removeItem(u),window.sessionStorage.removeItem(x),window.sessionStorage.removeItem(h),window.localStorage.removeItem(u),window.localStorage.removeItem(x),window.localStorage.removeItem(h)}catch(e){console.warn("Failed to clear OAuth storage",e)}},j=()=>{let e,t,s;return s=((t=(e=window.location.pathname||"").indexOf("/ui"))>=0?e.slice(0,t+3):"").replace(/\/+$/,""),`${window.location.origin}${s}/mcp/oauth/callback`},y=(0,b.useCallback)(async()=>{let r=t()||{};if(!e){o("Missing admin token"),C.default.error("Access token missing. Please re-authenticate and try again.");return}let a=s();if(!a||!a.url||!a.transport){let e="Please complete server URL and transport before starting OAuth.";o(e),C.default.error(e);return}try{n("authorizing"),o(null);let t=await (0,_.cacheTemporaryMcpServer)(e,a),s=t?.server_id?.trim();if(!s)throw Error("Temporary MCP server identifier missing. Please retry.");let i={};if(!(a.credentials?.client_id&&a.credentials?.client_secret)){let t=await (0,_.registerMcpOAuthClient)(e,s,{client_name:a.alias||a.server_name||s,grant_types:["authorization_code","refresh_token"],response_types:["code"],token_endpoint_auth_method:a.credentials&&a.credentials.client_secret?"client_secret_post":"none"});i={clientId:t?.client_id,clientSecret:t?.client_secret}}let c=(0,eY.generateCodeVerifier)(),d=await (0,eY.generateCodeChallenge)(c),m=crypto.randomUUID(),x=i.clientId||r.client_id,g=Array.isArray(r.scopes)?r.scopes.filter(e=>e&&e.trim().length>0).join(" "):void 0,f=(0,_.buildMcpOAuthAuthorizeUrl)({serverId:s,clientId:x,redirectUri:j(),state:m,codeChallenge:d,scope:g}),b={state:m,codeVerifier:c,clientId:x,clientSecret:i.clientSecret||r.client_secret,serverId:s,redirectUri:j()};if(l)try{l()}catch(e){console.error("Failed to prepare for OAuth redirect",e)}try{p(u,JSON.stringify(b)),p(h,window.location.href)}catch(e){throw Error("Unable to access browser storage for OAuth. Please enable storage and retry.")}window.location.href=f}catch(t){console.error("Failed to start OAuth flow",t),n("error");let e=(0,eJ.extractErrorMessage)(t);o(e),C.default.error(e)}},[e,t,s,l]),v=(0,b.useCallback)(async()=>{if(m.current)return;let t=null,s=null;try{let e=g(x);if(!e)return;m.current=!0,t=JSON.parse(e);let r=g(u);s=r?JSON.parse(r):null}catch(e){f(),m.current=!1,o("Failed to resume OAuth flow. Please retry."),n("error"),C.default.error("Failed to resume OAuth flow. Please retry.");return}if(!t){m.current=!1;return}try{window.sessionStorage.removeItem(x),window.localStorage.removeItem(x)}catch(e){}try{if(!s||!s.state||!s.codeVerifier||!s.serverId)throw Error("OAuth session state was lost. This can happen if you have strict browser privacy settings. Please try again and ensure cookies/storage is enabled.");if(!t.state||t.state!==s.state)throw Error("OAuth state mismatch. Please retry.");if(t.error)throw Error(t.error_description||t.error);if(!t.code)throw Error("Authorization code missing in callback.");n("exchanging");let l=await (0,_.exchangeMcpOAuthToken)({serverId:s.serverId,code:t.code,clientId:s.clientId,clientSecret:s.clientSecret,codeVerifier:s.codeVerifier,redirectUri:s.redirectUri,accessToken:e});r(l),d(l),n("success"),o(null),C.default.success("OAuth token retrieved successfully")}catch(t){let e=(0,eJ.extractErrorMessage)(t);o(e),n("error"),C.default.error(e)}finally{f(),setTimeout(()=>{m.current=!1},1e3)}},[r]);return(0,b.useEffect)(()=>{v()},[v]),{startOAuthFlow:y,status:a,error:i,tokenResponse:c}},eZ="../ui/assets/logos/mcp_logo.png",eX=[eo.AUTH_TYPE.API_KEY,eo.AUTH_TYPE.BEARER_TOKEN,eo.AUTH_TYPE.TOKEN,eo.AUTH_TYPE.BASIC],e0=[...eX,eo.AUTH_TYPE.OAUTH2,eo.AUTH_TYPE.AWS_SIGV4],e2="litellm-mcp-oauth-create-state",e1=e=>Array.isArray(e)?e.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value??""),e},{}):{},e5=({userRole:e,accessToken:r,onCreateSuccess:a,isModalVisible:n,setModalVisible:i,availableAccessGroups:o,prefillData:c,onBackToDiscovery:d})=>{let[m]=D.Form.useForm(),[u,x]=(0,b.useState)(!1),[f,j]=(0,b.useState)({}),[y,v]=(0,b.useState)({}),[N,w]=(0,b.useState)(null),[S,T]=(0,b.useState)(!1),[k,A]=(0,b.useState)([]),[I,P]=(0,b.useState)({}),[O,M]=(0,b.useState)({}),[F,E]=(0,b.useState)(""),[L,R]=(0,b.useState)([]),[U,z]=(0,b.useState)(""),[B,q]=(0,b.useState)(null),[V,$]=(0,b.useState)(void 0),[K,W]=(0,b.useState)(null),{tools:J,isLoadingTools:Y,toolsError:G,toolsErrorStackTrace:Q,canFetchTools:Z,fetchTools:X,clearTools:ee}=ek({accessToken:r,oauthAccessToken:B,formValues:y,enabled:!0}),et=y.auth_type,es=!!et&&eX.includes(et),er=et===eo.AUTH_TYPE.OAUTH2,ec=et===eo.AUTH_TYPE.AWS_SIGV4,ed=er&&y.oauth_flow_type===eo.OAUTH_FLOW.M2M,{startOAuthFlow:em,status:ex,error:eh,tokenResponse:ep}=eQ({accessToken:r,getCredentials:()=>m.getFieldValue("credentials"),getTemporaryPayload:()=>{let e=m.getFieldsValue(!0),t=e.transport||F,s=e.url||(t===eo.TRANSPORT.OPENAPI?e.spec_path:void 0);if(!s||!t)return null;let r=e1(e.static_headers);return{server_id:void 0,server_name:e.server_name,alias:e.alias,description:e.description,url:s,transport:t===eo.TRANSPORT.OPENAPI?"http":t,auth_type:eo.AUTH_TYPE.OAUTH2,credentials:e.credentials,authorization_url:e.authorization_url,token_url:e.token_url,registration_url:e.registration_url,mcp_access_groups:e.mcp_access_groups,static_headers:r,command:e.command,args:e.args,env:e.env}},onTokenReceived:e=>{if(q(e?.access_token??null),e?.access_token){let t={access_token:e.access_token,...e.refresh_token&&{refresh_token:e.refresh_token},...e.expires_in&&{expires_in:e.expires_in},...e.scope&&{scope:e.scope}};m.setFieldsValue({credentials:t}),C.default.success("OAuth authorization successful! Please click 'Create MCP Server' to save the configuration.")}},onBeforeRedirect:()=>{try{let e=m.getFieldsValue(!0);(0,eG.setSecureItem)(e2,JSON.stringify({modalVisible:n,formValues:e,transportType:F,costConfig:f,allowedTools:k,searchValue:U,aliasManuallyEdited:S,logoUrl:V}))}catch(e){console.warn("Failed to persist MCP create state",e)}}});b.default.useEffect(()=>{let e=(0,eG.getSecureItem)(e2);if(e)try{let t=JSON.parse(e);t.modalVisible&&i(!0);let s=t.formValues?.transport||t.transportType||"";s&&E(s),t.formValues&&w({values:t.formValues,transport:s}),t.costConfig&&j(t.costConfig),t.allowedTools&&A(t.allowedTools),t.searchValue&&z(t.searchValue),"boolean"==typeof t.aliasManuallyEdited&&T(t.aliasManuallyEdited),t.logoUrl&&$(t.logoUrl)}catch(e){console.error("Failed to restore MCP create state",e)}finally{window.sessionStorage.removeItem(e2)}},[m,i]),b.default.useEffect(()=>{N&&(F||N.transport,(!N.transport||F)&&(m.setFieldsValue(N.values),v(N.values),w(null)))},[N,m,F]),b.default.useEffect(()=>{if(!n||!c)return;let e=(c.name||"").replace(/[^a-zA-Z0-9_]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,""),t=c.transport||"";E(t);let s={server_name:e,alias:e,description:c.description||"",transport:t};if("stdio"===t){let e={};if(c.command&&(e.command=c.command),c.args&&c.args.length>0&&(e.args=c.args),c.env_vars&&c.env_vars.length>0){let t={};for(let e of c.env_vars)t[e.name]=e.description?`<${e.description}>`:"";e.env=t}Object.keys(e).length>0&&(s.stdio_config=JSON.stringify(e,null,2))}else c.url&&(s.url=c.url);m.setFieldsValue(s),v(s),T(!1)},[n,c,m]);let eg=async e=>{x(!0);try{let{static_headers:t,stdio_config:s,credentials:l,allow_all_keys:n,available_on_public_internet:o,delegate_auth_to_upstream:c,token_validation_json:d,...u}=e,h=u.mcp_access_groups,p=e1(t),g=l&&"object"==typeof l?Object.entries(l).reduce((e,[t,s])=>{if(null==s||""===s)return e;if("scopes"===t){if(Array.isArray(s)){let r=s.filter(e=>null!=e&&""!==e);r.length>0&&(e[t]=r)}}else e[t]=s;return e},{}):void 0,b={};if(s&&"stdio"===F)try{let e=JSON.parse(s),t=e;if(e.mcpServers&&"object"==typeof e.mcpServers){let s=Object.keys(e.mcpServers);if(s.length>0){let r=s[0];t=e.mcpServers[r],u.server_name||(u.server_name=r.replace(/-/g,"_"))}}b={command:t.command,args:t.args,env:t.env},console.log("Parsed stdio config:",b)}catch(e){C.default.fromBackend("Invalid JSON in stdio configuration");return}u.transport===eo.TRANSPORT.OPENAPI&&(u.transport="http");let y=null;if(d&&""!==d.trim())try{y=JSON.parse(d)}catch{C.default.fromBackend("Invalid JSON in Token Validation Rules"),x(!1);return}let v={...u,...b,stdio_config:void 0,mcp_info:{server_name:u.server_name||u.url,description:u.description,logo_url:V||void 0,mcp_server_cost_info:Object.keys(f).length>0?f:null},mcp_access_groups:h,alias:u.alias,allowed_tools:k.length>0?k:null,tool_name_to_display_name:Object.keys(I).length>0?I:null,tool_name_to_description:Object.keys(O).length>0?O:null,allow_all_keys:!!n,available_on_public_internet:!!o,delegate_auth_to_upstream:!!c,static_headers:p,...null!==y&&{token_validation:y}};if(v.static_headers=p,u.auth_type&&e0.includes(u.auth_type)&&g&&Object.keys(g).length>0&&(v.credentials=g),console.log(`Payload: ${JSON.stringify(v)}`),null!=r){let e=ej?await (0,_.createMCPServer)(r,v):await (0,_.registerMCPServer)(r,v);C.default.success(ej?"MCP Server created successfully":"MCP Server submitted for admin review"),m.resetFields(),j({}),ee(),A([]),T(!1),$(void 0),i(!1),a(e)}}catch(t){let e=t instanceof Error?t.message:String(t);C.default.fromBackend(ej?`Error creating MCP Server: ${e}`:`Error submitting MCP Server: ${e}`)}finally{x(!1)}},eb=()=>{m.resetFields(),j({}),ee(),A([]),T(!1),$(void 0),i(!1)};b.default.useEffect(()=>{if(!S&&y.server_name){let e=y.server_name.replace(/\s+/g,"_");m.setFieldsValue({alias:e}),v(t=>({...t,alias:e}))}},[y.server_name]),b.default.useEffect(()=>{n||v({})},[n]);let ej=(0,s.isAdminRole)(e);return(0,t.jsx)(h.Modal,{title:(0,t.jsxs)("div",{className:"flex items-center pb-4 border-b border-gray-100",style:{gap:12},children:[d&&(0,t.jsx)("button",{onClick:d,className:"text-sm text-blue-600 hover:text-blue-800 cursor-pointer bg-transparent border-none",style:{flexShrink:0},children:"←"}),(0,t.jsx)("img",{src:eZ,alt:"MCP Logo",className:"w-8 h-8 object-contain",style:{height:"20px",width:"20px",objectFit:"contain"}}),(0,t.jsx)("h2",{className:"text-xl font-semibold text-gray-900",children:ej?"Add New MCP Server":"Submit MCP Server for Review"})]}),open:n,width:1e3,onCancel:eb,footer:null,forceRender:!0,className:"top-8",styles:{body:{padding:"24px"},header:{padding:"24px 24px 0 24px",border:"none"}},children:(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsxs)(D.Form,{form:m,onFinish:eg,onValuesChange:(e,t)=>v(t),layout:"vertical",className:"space-y-6",children:[!ej&&(0,t.jsxs)("div",{className:"rounded-md bg-blue-50 border border-blue-200 px-4 py-3 text-sm text-blue-800",children:["Your submission will be sent for admin review before it becomes active."," ","Note: the request must be made with a team-scoped API key."]}),(0,t.jsxs)("div",{className:"grid grid-cols-1 gap-6",children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["MCP Server Name",(0,t.jsx)(g.Tooltip,{title:"Best practice: Use a descriptive name that indicates the server's purpose (e.g., 'GitHub_MCP', 'Email_Service'). Cannot contain spaces or hyphens; use underscores instead. Names must comply with SEP-986 and will be rejected if invalid (https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names).",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"server_name",rules:[{required:!1,message:"Please enter a server name"},{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(ei.TextInput,{placeholder:"e.g., GitHub_MCP, Zapier_MCP, etc.",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Alias",(0,t.jsx)(g.Tooltip,{title:"A short, unique identifier for this server. Defaults to the server name if not provided. Cannot contain spaces or hyphens; use underscores instead.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"alias",rules:[{required:!1},{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(ei.TextInput,{placeholder:"e.g., GitHub_MCP, Zapier_MCP, etc.",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500",onChange:()=>T(!0)})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Description"}),name:"description",rules:[{required:!1,message:"Please enter a server description"}],children:(0,t.jsx)(ei.TextInput,{placeholder:"Brief description of what this server does",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(e$,{value:V,onChange:$}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"GitHub / Source URL"}),name:"source_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://github.com/org/mcp-server",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Transport Type"}),name:"transport",rules:[{required:!0,message:"Please select a transport type"}],children:(0,t.jsxs)(p.Select,{placeholder:"Select transport",className:"rounded-lg",size:"large",onChange:e=>{E(e),"stdio"===e?m.setFieldsValue({url:void 0,spec_path:void 0,auth_type:void 0,credentials:void 0}):e===eo.TRANSPORT.OPENAPI?m.setFieldsValue({url:void 0,command:void 0,args:void 0,env:void 0}):m.setFieldsValue({spec_path:void 0,command:void 0,args:void 0,env:void 0})},value:F,children:[(0,t.jsx)(p.Select.Option,{value:"http",children:"Streamable HTTP (Recommended)"}),(0,t.jsx)(p.Select.Option,{value:"sse",children:"Server-Sent Events (SSE)"}),(0,t.jsx)(p.Select.Option,{value:"stdio",children:"Standard Input/Output (stdio)"}),(0,t.jsx)(p.Select.Option,{value:eo.TRANSPORT.OPENAPI,children:"OpenAPI Spec"})]})}),("http"===F||"sse"===F)&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"MCP Server URL"}),name:"url",rules:[{required:!0,message:"Please enter a server URL"},{validator:(e,t)=>eK(t)}],children:(0,t.jsx)(H.Input,{placeholder:"https://your-mcp-server.com",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),F===eo.TRANSPORT.OPENAPI&&(0,t.jsx)(ez,{form:m,accessToken:n?r:null,onValuesChange:e=>v(t=>({...t,...e})),onKeyToolsChange:R,onLogoUrlChange:$,onOAuthDocsUrlChange:W}),F===eo.TRANSPORT.OPENAPI&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center gap-2",children:["BYOK (Bring Your Own Key)",(0,t.jsx)(g.Tooltip,{title:"When enabled, each user provides their own API key for this service. Keys are stored per-user and never shared.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"is_byok",valuePropName:"checked",children:(0,t.jsx)(el.Switch,{})}),(0,t.jsx)(D.Form.Item,{noStyle:!0,shouldUpdate:(e,t)=>e.is_byok!==t.is_byok||e.auth_type!==t.auth_type,children:({getFieldValue:e})=>e("is_byok")?(0,t.jsxs)(t.Fragment,{children:[e("auth_type")&&"none"!==e("auth_type")&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 rounded-lg text-sm text-blue-700 flex items-start gap-2",children:[(0,t.jsx)(en.InfoCircleOutlined,{className:"mt-0.5 flex-shrink-0"}),(0,t.jsxs)("span",{children:["User keys will be sent as:"," ",(0,t.jsxs)("code",{className:"font-mono bg-blue-100 px-1 rounded",children:["bearer_token"===e("auth_type")&&"Authorization: Bearer {key}","token"===e("auth_type")&&"Authorization: token {key}","api_key"===e("auth_type")&&"x-api-key: {key}","basic"===e("auth_type")&&"Authorization: Basic {key}","authorization"===e("auth_type")&&"Authorization: {key}"]}),!e("auth_type")&&"Set Authentication Type below to specify the format."]})]}),!e("auth_type")&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-yellow-50 rounded-lg text-sm text-yellow-700 flex items-start gap-2",children:[(0,t.jsx)(en.InfoCircleOutlined,{className:"mt-0.5 flex-shrink-0"}),(0,t.jsxs)("span",{children:["Set the ",(0,t.jsx)("strong",{children:"Authentication Type"})," below to specify how user keys are sent (e.g., Bearer Token, API Key header)."]})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700",children:["Access Description",(0,t.jsx)(g.Tooltip,{title:"List of permissions shown to users in the connection modal (e.g. 'Create and manage Jira issues')",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"byok_description",children:(0,t.jsx)(p.Select,{mode:"tags",placeholder:"Add access description items (press Enter after each)",className:"w-full",tokenSeparators:[","]})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700",children:["API Key Help URL",(0,t.jsx)(g.Tooltip,{title:"Optional link shown to users to help them find their API key",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"byok_api_key_help_url",children:(0,t.jsx)(H.Input,{placeholder:"https://docs.example.com/api-keys"})})]}):null})]}),"stdio"!==F&&""!==F&&(0,t.jsx)(ea.Collapse,{defaultActiveKey:["auth"],className:"mb-4",items:[{key:"auth",label:(0,t.jsx)("span",{className:"text-sm font-semibold text-gray-700",children:"Authentication"}),children:(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{name:"auth_type",rules:[{required:!0,message:"Please select an auth type"}],children:(0,t.jsxs)(p.Select,{placeholder:"Select auth type",className:"rounded-lg",size:"large",children:[(0,t.jsx)(p.Select.Option,{value:"none",children:"None"}),(0,t.jsx)(p.Select.Option,{value:"api_key",children:"API Key"}),(0,t.jsx)(p.Select.Option,{value:"bearer_token",children:"Bearer Token"}),(0,t.jsx)(p.Select.Option,{value:"token",children:"Token"}),(0,t.jsx)(p.Select.Option,{value:"basic",children:"Basic Auth"}),(0,t.jsx)(p.Select.Option,{value:"oauth2",children:"OAuth"}),(0,t.jsx)(p.Select.Option,{value:"aws_sigv4",children:"AWS SigV4 (Bedrock AgentCore MCPs)"})]})}),es&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Authentication Value",(0,t.jsx)(g.Tooltip,{title:"Token, password, or header value to send with each request for the selected auth type.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","auth_value"],rules:[{validator:(e,t)=>t&&"string"==typeof t&&""===t.trim()?Promise.reject(Error("Authentication value cannot be empty whitespace")):Promise.resolve()}],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:"Enter token or secret",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),er&&(0,t.jsx)(eu,{isM2M:ed,initialFlowType:eo.OAUTH_FLOW.INTERACTIVE,docsUrl:K,oauthFlow:{startOAuthFlow:em,status:ex,error:eh,tokenResponse:ep}})]})}]}),"stdio"!==F&&""!==F&&ec&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-2",children:["For MCP servers hosted on AWS Bedrock AgentCore."," ",(0,t.jsx)("a",{href:"https://docs.litellm.ai/docs/mcp_aws_sigv4",target:"_blank",rel:"noopener noreferrer",className:"text-blue-500 hover:text-blue-700",children:"View docs →"})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Region",(0,t.jsx)(g.Tooltip,{title:"AWS region for SigV4 signing (e.g., us-east-1)",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_region_name"],rules:[{required:!0,message:"AWS region is required for SigV4 auth"}],children:(0,t.jsx)(H.Input,{placeholder:"us-east-1",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Service Name",(0,t.jsx)(g.Tooltip,{title:"AWS service name for SigV4 signing. Defaults to 'bedrock-agentcore'.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_service_name"],children:(0,t.jsx)(H.Input,{placeholder:"bedrock-agentcore",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Access Key ID",(0,t.jsx)(g.Tooltip,{title:"Optional. If not provided, falls back to the boto3 credential chain (IAM role, env vars, etc.).",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_access_key_id"],dependencies:[["credentials","aws_secret_access_key"]],rules:[({getFieldValue:e})=>({validator:(t,s)=>e(["credentials","aws_secret_access_key"])&&!s?Promise.reject(Error("Access Key ID is required when Secret Access Key is provided")):Promise.resolve()})],children:(0,t.jsx)(H.Input.Password,{placeholder:"AKIA... (optional — uses IAM role if blank)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Secret Access Key",(0,t.jsx)(g.Tooltip,{title:"Optional. Required if AWS Access Key ID is provided.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_secret_access_key"],dependencies:[["credentials","aws_access_key_id"]],rules:[({getFieldValue:e})=>({validator:(t,s)=>e(["credentials","aws_access_key_id"])&&!s?Promise.reject(Error("Secret Access Key is required when Access Key ID is provided")):Promise.resolve()})],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter secret key (optional — uses IAM role if blank)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Token",(0,t.jsx)(g.Tooltip,{title:"Optional. Only needed for temporary STS credentials.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_token"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter session token (optional)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Role ARN",(0,t.jsx)(g.Tooltip,{title:"Optional. IAM role ARN to assume via STS before signing. If set, LiteLLM calls sts:AssumeRole to get temporary credentials. Uses ambient credentials (IAM role, env vars) as the source identity unless explicit keys are also provided.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_role_name"],children:(0,t.jsx)(H.Input,{placeholder:"arn:aws:iam::123456789012:role/MyRole (optional)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Name",(0,t.jsx)(g.Tooltip,{title:"Optional. Session name for the AssumeRole call — appears in CloudTrail logs. Auto-generated if omitted.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_name"],children:(0,t.jsx)(H.Input,{placeholder:"litellm-prod (optional, auto-generated if blank)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})})]}),(0,t.jsx)(eO,{isVisible:"stdio"===F})]}),(0,t.jsx)("div",{className:"mt-8",children:(0,t.jsx)(eR,{availableAccessGroups:o,mcpServer:null,searchValue:U,setSearchValue:z,getAccessGroupOptions:()=>{let e=o.map(e=>({value:e,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-green-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:e})]})}));return U&&!o.some(e=>e.toLowerCase().includes(U.toLowerCase()))&&e.push({value:U,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-blue-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:U}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-1",children:"create new group"})]})}),e}})}),(0,t.jsx)("div",{className:"mt-8 pt-6 border-t border-gray-200",children:(0,t.jsx)(e_,{formValues:y,tools:J,isLoadingTools:Y,toolsError:G,toolsErrorStackTrace:Q,canFetchTools:Z,fetchTools:X})}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(eP,{accessToken:r,oauthAccessToken:B,formValues:y,allowedTools:k,existingAllowedTools:null,onAllowedToolsChange:A,toolNameToDisplayName:I,toolNameToDescription:O,onToolNameToDisplayNameChange:P,onToolNameToDescriptionChange:M,keyTools:L,externalTools:J,externalIsLoading:Y,externalError:G,externalCanFetch:Z})}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(ef,{value:f,onChange:j,tools:J.filter(e=>k.includes(e.name)),disabled:!1})}),(0,t.jsxs)("div",{className:"flex items-center justify-end space-x-3 pt-6 border-t border-gray-100",children:[(0,t.jsx)(l.Button,{variant:"secondary",onClick:eb,children:"Cancel"}),(0,t.jsx)(l.Button,{variant:"primary",loading:u,children:u?"Creating...":"Add MCP Server"})]})]})})})};var e4=e.i(175712),e6=e.i(118366),e3=e.i(475254);let e7=(0,e3.default)("code",[["path",{d:"m16 18 6-6-6-6",key:"eg8j8"}],["path",{d:"m8 6-6 6 6 6",key:"ppft3o"}]]);e.s(["Code",()=>e7],758472);let e8=(0,e3.default)("terminal",[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]]),e9=(0,e3.default)("globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);var te=e.i(634831),tt=e.i(438100);let ts=(0,e3.default)("zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);var tr=e.i(500330);let{Title:tl,Text:ta}=f.Typography,{Panel:tn}=ea.Collapse,ti=({icon:e,title:s,description:r,children:l,serverName:a,accessGroups:n=["dev-group"]})=>{let[i,o]=(0,b.useState)(!1);return(0,t.jsxs)(e4.Card,{className:"border border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3",children:[(0,t.jsx)("span",{className:"p-2 rounded-lg bg-gray-50",children:e}),(0,t.jsxs)("div",{children:[(0,t.jsx)(tl,{level:5,className:"mb-0",children:s}),(0,t.jsx)(ta,{className:"text-gray-600",children:r})]})]}),a&&("Implementation Example"===s||"Configuration"===s)&&(0,t.jsxs)(D.Form.Item,{className:"mb-4",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-2",children:[(0,t.jsx)(el.Switch,{size:"small",checked:i,onChange:o}),(0,t.jsxs)(ta,{className:"text-sm",children:["Limit tools to specific MCP servers or MCP groups by passing the ",(0,t.jsx)("code",{children:"x-mcp-servers"})," header"]})]}),i&&(0,t.jsx)(ej.Alert,{className:"mt-2",type:"info",showIcon:!0,message:"Two Options",description:(0,t.jsxs)("div",{children:[(0,t.jsxs)("p",{children:[(0,t.jsx)("strong",{children:"Option 1:"})," Get a specific server: ",(0,t.jsxs)("code",{children:['"',a.replace(/\s+/g,"_"),'"']})]}),(0,t.jsxs)("p",{children:[(0,t.jsx)("strong",{children:"Option 2:"})," Get a group of MCPs: ",(0,t.jsx)("code",{children:'"dev-group"'})]}),(0,t.jsxs)("p",{className:"mt-2 text-sm text-gray-600",children:["You can also mix both: ",(0,t.jsx)("code",{children:'"Server1,dev-group"'})]})]})})]}),b.default.Children.map(l,e=>{if(b.default.isValidElement(e)&&e.props.hasOwnProperty("code")&&e.props.hasOwnProperty("copyKey")){let t=e.props.code;if(t&&t.includes('"headers":'))return b.default.cloneElement(e,{code:t.replace(/"headers":\s*{[^}]*}/,`"headers": ${JSON.stringify((()=>{let e={"x-litellm-api-key":"Bearer YOUR_LITELLM_API_KEY"};if(i&&a){let t=[a.replace(/\s+/g,"_"),...n].join(",");e["x-mcp-servers"]=t}return e})(),null,8)}`)})}return e})]})},to=({currentServerAccessGroups:e=[]})=>{let s=(0,_.getProxyBaseUrl)(),[r,l]=(0,b.useState)({}),[u,x]=(0,b.useState)({openai:[],litellm:[],cursor:[],http:[]}),[h]=(0,b.useState)("Zapier_MCP"),p=async(e,t)=>{await (0,tr.copyToClipboard)(e)&&(l(e=>({...e,[t]:!0})),setTimeout(()=>{l(e=>({...e,[t]:!1}))},2e3))},g=({code:e,copyKey:s,title:l,className:a=""})=>(0,t.jsxs)("div",{className:"relative group",children:[l&&(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-2",children:[(0,t.jsx)(e7,{size:16,className:"text-blue-600"}),(0,t.jsx)(ta,{strong:!0,className:"text-gray-700",children:l})]}),(0,t.jsxs)(e4.Card,{className:`bg-gray-50 border border-gray-200 relative ${a}`,children:[(0,t.jsx)(eb.Button,{type:"text",size:"small",icon:r[s]?(0,t.jsx)(k.CheckIcon,{size:12}):(0,t.jsx)(e6.CopyIcon,{size:12}),onClick:()=>p(e,s),className:`absolute top-2 right-2 z-10 transition-all duration-200 ${r[s]?"text-green-600 bg-green-50 border-green-200":"text-gray-500 hover:text-gray-700 hover:bg-gray-100"}`}),(0,t.jsx)("pre",{className:"text-sm overflow-x-auto pr-10 text-gray-800 font-mono leading-relaxed",children:e})]})]}),f=({step:e,title:s,children:r})=>(0,t.jsxs)("div",{className:"flex gap-4",children:[(0,t.jsx)("div",{className:"flex-shrink-0",children:(0,t.jsx)("div",{className:"w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold",children:e})}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)(ta,{strong:!0,className:"text-gray-800 block mb-2",children:s}),r]})]});return(0,t.jsx)("div",{children:(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(m.Title,{className:"text-3xl font-bold text-gray-900 mb-3",children:"Connect to your MCP client"}),(0,t.jsx)(d.Text,{className:"text-lg text-gray-600",children:"Use tools directly from any MCP client with LiteLLM MCP. Enable your AI assistant to perform real-world tasks through a simple, secure connection."})]}),(0,t.jsxs)(n.TabGroup,{className:"w-full",children:[(0,t.jsx)(i.TabList,{className:"flex justify-start mt-8 mb-6",children:(0,t.jsxs)("div",{className:"flex bg-gray-100 p-1 rounded-lg",children:[(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(e7,{size:18}),"OpenAI API"]})}),(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(ts,{size:18}),"LiteLLM Proxy"]})}),(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(e8,{size:18}),"Cursor"]})}),(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(e9,{size:18}),"Streamable HTTP"]})})]})}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsx)(o.TabPanel,{className:"mt-6",children:(0,t.jsx)(()=>(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsxs)("div",{className:"bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-100",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3",children:[(0,t.jsx)(e7,{className:"text-blue-600",size:24}),(0,t.jsx)(tl,{level:4,className:"mb-0 text-blue-900",children:"OpenAI Responses API Integration"})]}),(0,t.jsx)(ta,{className:"text-blue-700",children:"Connect OpenAI Responses API to your LiteLLM MCP server for seamless tool integration"})]}),(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsx)(ti,{icon:(0,t.jsx)(tt.KeyIcon,{className:"text-blue-600",size:16}),title:"API Key Setup",description:"Configure your OpenAI API key for authentication",children:(0,t.jsxs)(eM.Space,{direction:"vertical",size:"middle",className:"w-full",children:[(0,t.jsx)("div",{children:(0,t.jsxs)(ta,{children:["Get your API key from the"," ",(0,t.jsxs)("a",{href:"https://platform.openai.com/api-keys",target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:text-blue-700 inline-flex items-center gap-1",children:["OpenAI platform ",(0,t.jsx)(te.ExternalLinkIcon,{size:12})]})]})}),(0,t.jsx)(g,{title:"Environment Variable",code:'export OPENAI_API_KEY="sk-..."',copyKey:"openai-env"})]})}),(0,t.jsx)(ti,{icon:(0,t.jsx)(P.ServerIcon,{className:"text-blue-600",size:16}),title:"MCP Server Information",description:"Connection details for your LiteLLM MCP server",children:(0,t.jsx)(g,{title:"Server URL",code:`${s}/mcp`,copyKey:"openai-server-url"})}),(0,t.jsx)(ti,{icon:(0,t.jsx)(e7,{className:"text-blue-600",size:16}),title:"Implementation Example",description:"Complete cURL example for using the Responses API",serverName:"Zapier Gmail",accessGroups:["dev-group"],children:(0,t.jsx)(g,{code:`curl --location 'https://api.openai.com/v1/responses' \\ +======== }`,rows:12,className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 font-mono text-sm"})}):null;var eM=e.i(770914),eF=e.i(564897),eE=e.i(646563);let{Panel:eL}=ea.Collapse,eR=({availableAccessGroups:e,mcpServer:s,searchValue:r,setSearchValue:l,getAccessGroupOptions:a})=>{let n=D.Form.useFormInstance(),i=D.Form.useWatch("auth_type",n)===eo.AUTH_TYPE.OAUTH2,o=D.Form.useWatch("delegate_auth_to_upstream",n),c=D.Form.useWatch("available_on_public_internet",n),d=i&&!0===o&&!1===c;return(0,b.useEffect)(()=>{if(s){if(s.static_headers){let e=Object.entries(s.static_headers).map(([e,t])=>({header:e,value:null!=t?String(t):""}));n.setFieldValue("static_headers",e)}"boolean"==typeof s.allow_all_keys&&n.setFieldValue("allow_all_keys",s.allow_all_keys),"boolean"==typeof s.available_on_public_internet&&n.setFieldValue("available_on_public_internet",s.available_on_public_internet),"boolean"==typeof s.delegate_auth_to_upstream&&n.setFieldValue("delegate_auth_to_upstream",s.delegate_auth_to_upstream)}else n.setFieldValue("allow_all_keys",!1),n.setFieldValue("available_on_public_internet",!0),n.setFieldValue("delegate_auth_to_upstream",!1)},[s,n]),(0,b.useEffect)(()=>{i||n.setFieldValue("delegate_auth_to_upstream",!1)},[i,n]),(0,t.jsx)(ea.Collapse,{className:"bg-gray-50 border border-gray-200 rounded-lg",expandIconPosition:"end",ghost:!1,children:(0,t.jsx)(eL,{header:(0,t.jsxs)("div",{className:"flex items-center",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-blue-500 rounded-full"}),(0,t.jsx)("h3",{className:"text-lg font-semibold text-gray-900",children:"Permission Management / Access Control"})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 ml-4",children:"Configure access permissions and security settings (Optional)"})]}),className:"border-0",forceRender:!0,children:(0,t.jsxs)("div",{className:"space-y-6 pt-4",children:[(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Allow All LiteLLM Keys",(0,t.jsx)(g.Tooltip,{title:"When enabled, every API key can access this MCP server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 mt-1",children:'Enable if this server should be "public" to all keys.'})]}),(0,t.jsx)(D.Form.Item,{name:"allow_all_keys",valuePropName:"checked",initialValue:s?.allow_all_keys??!1,className:"mb-0",children:(0,t.jsx)(el.Switch,{})})]}),(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Internal network only",(0,t.jsx)(g.Tooltip,{title:"When on, only requests from within your internal network are accepted. Turn off to allow external clients (other clusters, ChatGPT, etc). API key authentication is always required regardless of this setting.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 mt-1",children:"Turn on to restrict access to callers within your internal network only."})]}),(0,t.jsx)(D.Form.Item,{name:"available_on_public_internet",valuePropName:"checked",getValueProps:e=>({checked:!e}),getValueFromEvent:e=>!e,initialValue:!0,className:"mb-0",children:(0,t.jsx)(el.Switch,{})})]}),i&&(0,t.jsxs)("div",{className:"flex items-start justify-between gap-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Delegate auth to upstream (PKCE passthrough)",(0,t.jsx)(g.Tooltip,{title:"When on, LiteLLM skips its own API key/SSO check for this server and lets the client complete PKCE directly with the upstream MCP server. Only honored when Auth Type is oauth2. No spend tracking or per-key rate limiting will run on this route.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),(0,t.jsx)("p",{className:"text-sm text-gray-600 mt-1",children:"Bypass LiteLLM auth so clients authenticate directly with the upstream OAuth MCP server."})]}),(0,t.jsx)(D.Form.Item,{name:"delegate_auth_to_upstream",valuePropName:"checked",initialValue:s?.delegate_auth_to_upstream??!1,className:"mb-0",children:(0,t.jsx)(el.Switch,{})})]}),d&&(0,t.jsx)(ej.Alert,{type:"warning",showIcon:!0,className:"mb-2",message:"Internal server with upstream OAuth delegation",description:"This MCP server is configured as internal-only but delegates auth to upstream. Anonymous users will be able to reach the upstream OAuth2 /authorize flow without a LiteLLM session. Ensure your upstream provider and network enforce access controls."}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["MCP Access Groups",(0,t.jsx)(g.Tooltip,{title:"Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"mcp_access_groups",className:"mb-4",children:(0,t.jsx)(p.Select,{mode:"tags",showSearch:!0,placeholder:"Select existing groups or type to create new ones",optionFilterProp:"value",filterOption:(e,t)=>(t?.value??"").toLowerCase().includes(e.toLowerCase()),onSearch:e=>l(e),tokenSeparators:[","],options:a(),maxTagCount:"responsive",allowClear:!0})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Extra Headers",(0,t.jsx)(g.Tooltip,{title:"Forward custom headers from incoming requests to this MCP server (e.g., Authorization, X-Custom-Header, User-Agent)",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})}),s?.extra_headers&&s.extra_headers.length>0&&(0,t.jsxs)("span",{className:"ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full",children:[s.extra_headers.length," configured"]})]}),name:"extra_headers",children:(0,t.jsx)(p.Select,{mode:"tags",placeholder:s?.extra_headers&&s.extra_headers.length>0?`Currently: ${s.extra_headers.join(", ")}`:"Enter header names (e.g., Authorization, X-Custom-Header)",className:"rounded-lg",size:"large",tokenSeparators:[","],allowClear:!0})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Static Headers",(0,t.jsx)(g.Tooltip,{title:"Send these key-value headers with every request to this MCP server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),required:!1,children:(0,t.jsx)(D.Form.List,{name:"static_headers",children:(e,{add:s,remove:r})=>(0,t.jsxs)("div",{className:"space-y-3",children:[e.map(({key:e,name:s,...l})=>(0,t.jsxs)(eM.Space,{className:"flex w-full",align:"baseline",size:"middle",children:[(0,t.jsx)(D.Form.Item,{...l,name:[s,"header"],className:"flex-1",rules:[{required:!0,message:"Header name is required"}],children:(0,t.jsx)(H.Input,{size:"large",allowClear:!0,className:"rounded-lg",placeholder:"Header name (e.g., X-API-Key)"})}),(0,t.jsx)(D.Form.Item,{...l,name:[s,"value"],className:"flex-1",rules:[{required:!0,message:"Header value is required"}],children:(0,t.jsx)(H.Input,{size:"large",allowClear:!0,className:"rounded-lg",placeholder:"Header value"})}),(0,t.jsx)(eF.MinusCircleOutlined,{onClick:()=>r(s),className:"text-gray-500 hover:text-red-500 cursor-pointer"})]},e)),(0,t.jsx)(eb.Button,{type:"dashed",onClick:()=>s(),icon:(0,t.jsx)(eE.PlusOutlined,{}),block:!0,children:"Add Static Header"})]})})})]})},"permissions")})},ez=({accessToken:e,selectedName:s,onSelect:r})=>{let[l,a]=(0,b.useState)([]),[n,i]=(0,b.useState)(!1),[o,c]=(0,b.useState)(new Set);return((0,b.useEffect)(()=>{e&&(i(!0),(0,_.fetchOpenAPIRegistry)(e).then(e=>a(e.apis??[])).catch(()=>a([])).finally(()=>i(!1)))},[e]),n)?(0,t.jsxs)("div",{className:"mb-4",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Popular APIs"}),(0,t.jsx)("div",{className:"flex justify-center py-6",children:(0,t.jsx)(W.Spin,{size:"small"})})]}):0===l.length?null:(0,t.jsxs)("div",{className:"mb-4",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700 block mb-2",children:"Popular APIs"}),(0,t.jsx)("div",{className:"grid grid-cols-5 gap-2",children:l.map(e=>{let l=s===e.name,a=o.has(e.name);return(0,t.jsxs)("button",{type:"button",title:e.description,onClick:()=>r(e),className:`flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all cursor-pointer ${l?"border-blue-500 bg-blue-50 shadow-sm":"border-gray-200 hover:border-blue-300 hover:bg-gray-50"}`,children:[a?(0,t.jsx)("span",{className:"w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-sm font-bold text-gray-600",children:e.title.charAt(0)}):(0,t.jsx)("img",{src:e.icon_url,alt:e.title,className:"w-7 h-7 object-contain",onError:()=>{var t;return t=e.name,void c(e=>new Set(e).add(t))}}),(0,t.jsx)("span",{className:"text-xs text-gray-600 text-center leading-tight font-medium",children:e.title})]},e.name)})}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-2",children:"Select an API to pre-fill the spec URL and OAuth 2.0 settings, or enter your own spec URL below."})]})},eU=({form:e,accessToken:s,onValuesChange:r,onKeyToolsChange:l,onLogoUrlChange:a,onOAuthDocsUrlChange:n})=>{let[i,o]=(0,b.useState)(null);return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(ez,{accessToken:s,selectedName:i,onSelect:t=>{o(t.name),l?.(t.key_tools??[]),a?.(t.icon_url||void 0);let s={spec_path:t.spec_url};t.oauth?(s.auth_type=eo.AUTH_TYPE.OAUTH2,s.oauth_flow_type=eo.OAUTH_FLOW.INTERACTIVE,s.authorization_url=t.oauth.authorization_url,s.token_url=t.oauth.token_url,e.setFieldsValue(s),n?.(t.oauth.docs_url??null)):(e.resetFields(["auth_type","authorization_url","token_url"]),e.setFieldsValue(s),n?.(null)),r(s)}}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OpenAPI Spec URL",(0,t.jsx)(g.Tooltip,{title:"URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"spec_path",rules:[{required:!0,message:"Please enter an OpenAPI spec URL"}],children:(0,t.jsx)(H.Input,{placeholder:"https://petstore3.swagger.io/api/v3/openapi.json",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500",onChange:()=>{o(null),l?.([]),n?.(null)}})})]})};var eB=e.i(596239);let eq="/ui/assets/logos/",eV=[{name:"GitHub",url:`${eq}github.svg`},{name:"Slack",url:`${eq}slack.svg`},{name:"Notion",url:`${eq}notion.svg`},{name:"Linear",url:`${eq}linear.svg`},{name:"Jira",url:`${eq}jira.svg`},{name:"Figma",url:`${eq}figma.svg`},{name:"Gmail",url:`${eq}gmail.svg`},{name:"Google Drive",url:`${eq}google_drive.svg`},{name:"Stripe",url:`${eq}stripe.svg`},{name:"Shopify",url:`${eq}shopify.svg`},{name:"Salesforce",url:`${eq}salesforce.svg`},{name:"HubSpot",url:`${eq}hubspot.svg`},{name:"Twilio",url:`${eq}twilio.svg`},{name:"Cloudflare",url:`${eq}cloudflare.svg`},{name:"Sentry",url:`${eq}sentry.svg`},{name:"PostgreSQL",url:`${eq}postgresql.svg`},{name:"Snowflake",url:`${eq}snowflake.svg`},{name:"Zapier",url:`${eq}zapier.svg`},{name:"Google",url:`${eq}google.svg`},{name:"GitLab",url:`${eq}gitlab.svg`}],e$=({value:e,onChange:s})=>{let[r,l]=(0,b.useState)(new Set);return(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-2",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Logo"}),(0,t.jsx)(g.Tooltip,{title:"Select a well-known logo or paste a URL to any image. The logo is shown on the admin and chat pages.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-blue-400 hover:text-blue-600 cursor-help"})})]}),e&&(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3 p-3 bg-gray-50 rounded-lg border border-gray-200",children:[(0,t.jsx)("img",{src:e,alt:"Selected logo",className:"w-10 h-10 object-contain rounded",onError:e=>{e.target.style.display="none"}}),(0,t.jsx)("div",{className:"flex-1 min-w-0",children:(0,t.jsx)("div",{className:"text-xs text-gray-500 truncate",children:e})}),(0,t.jsx)("button",{type:"button",onClick:()=>s?.(void 0),className:"text-xs text-gray-400 hover:text-red-500 cursor-pointer bg-transparent border-none",children:"✕"})]}),(0,t.jsx)("div",{className:"grid grid-cols-10 gap-1.5 mb-3",children:eV.map(a=>{let n=e===a.url;return r.has(a.url)?null:(0,t.jsx)(g.Tooltip,{title:a.name,children:(0,t.jsx)("button",{type:"button",onClick:()=>{var t;return t=a.url,void s?.(e===t?void 0:t)},className:`flex items-center justify-center p-2 rounded-lg border transition-all cursor-pointer ${n?"border-blue-500 bg-blue-50 shadow-sm":"border-gray-200 hover:border-blue-300 hover:bg-gray-50"}`,style:{width:40,height:40},children:(0,t.jsx)("img",{src:a.url,alt:a.name,className:"w-5 h-5 object-contain",onError:()=>{var e;return e=a.url,void l(t=>new Set(t).add(e))}})})},a.name)})}),(0,t.jsx)(H.Input,{prefix:(0,t.jsx)(eB.LinkOutlined,{className:"text-gray-400"}),placeholder:"Or paste a custom logo URL...",value:e&&!eV.some(t=>t.url===e)?e:"",onChange:e=>{let t=e.target.value.trim();s?.(t||void 0)},className:"rounded-lg",size:"small"})]})},eD=e=>{try{let t=e.indexOf("/mcp/");if(-1===t)return{token:null,baseUrl:e};let s=e.split("/mcp/");if(2!==s.length)return{token:null,baseUrl:e};let r=s[0]+"/mcp/",l=s[1];if(!l)return{token:null,baseUrl:e};return{token:l,baseUrl:r}}catch(t){return console.error("Error parsing MCP URL:",t),{token:null,baseUrl:e}}},eH=e=>{let{token:t}=eD(e);return{maskedUrl:(e=>{let{token:t,baseUrl:s}=eD(e);return t?s+"...":e})(e),hasToken:!!t}},eK=e=>e?/^https?:\/\/[^\s/$.?#].[^\s]*$/i.test(e)?Promise.resolve():Promise.reject("Please enter a valid URL (e.g., http://service-name.domain:1234/path or https://example.com)"):Promise.resolve(),eW=e=>e&&(e.includes("-")||e.includes(" "))?Promise.reject("Cannot contain '-' (hyphen) or spaces. Please use '_' (underscore) instead."):Promise.resolve();var eJ=e.i(122520),eY=e.i(165615),eG=e.i(434166);let eQ=({accessToken:e,getCredentials:t,getTemporaryPayload:s,onTokenReceived:r,onBeforeRedirect:l})=>{let[a,n]=(0,b.useState)("idle"),[i,o]=(0,b.useState)(null),[c,d]=(0,b.useState)(null),m=(0,b.useRef)(!1),u="litellm-mcp-oauth-flow-state",x="litellm-mcp-oauth-result",h="litellm-mcp-oauth-return-url",p=(e,t)=>{(0,eG.setSecureItem)(e,t)},g=e=>{try{return(0,eG.getSecureItem)(e)}catch(t){return console.warn(`Failed to get storage item ${e}`,t),null}},f=()=>{try{window.sessionStorage.removeItem(u),window.sessionStorage.removeItem(x),window.sessionStorage.removeItem(h),window.localStorage.removeItem(u),window.localStorage.removeItem(x),window.localStorage.removeItem(h)}catch(e){console.warn("Failed to clear OAuth storage",e)}},j=()=>{let e,t,s;return s=((t=(e=window.location.pathname||"").indexOf("/ui"))>=0?e.slice(0,t+3):"").replace(/\/+$/,""),`${window.location.origin}${s}/mcp/oauth/callback`},y=(0,b.useCallback)(async()=>{let r=t()||{};if(!e){o("Missing admin token"),C.default.error("Access token missing. Please re-authenticate and try again.");return}let a=s();if(!a||!a.url||!a.transport){let e="Please complete server URL and transport before starting OAuth.";o(e),C.default.error(e);return}try{n("authorizing"),o(null);let t=await (0,_.cacheTemporaryMcpServer)(e,a),s=t?.server_id?.trim();if(!s)throw Error("Temporary MCP server identifier missing. Please retry.");let i={};if(!(a.credentials?.client_id&&a.credentials?.client_secret)){let t=await (0,_.registerMcpOAuthClient)(e,s,{client_name:a.alias||a.server_name||s,grant_types:["authorization_code","refresh_token"],response_types:["code"],token_endpoint_auth_method:a.credentials&&a.credentials.client_secret?"client_secret_post":"none"});i={clientId:t?.client_id,clientSecret:t?.client_secret}}let c=(0,eY.generateCodeVerifier)(),d=await (0,eY.generateCodeChallenge)(c),m=crypto.randomUUID(),x=i.clientId||r.client_id,g=Array.isArray(r.scopes)?r.scopes.filter(e=>e&&e.trim().length>0).join(" "):void 0,f=(0,_.buildMcpOAuthAuthorizeUrl)({serverId:s,clientId:x,redirectUri:j(),state:m,codeChallenge:d,scope:g}),b={state:m,codeVerifier:c,clientId:x,clientSecret:i.clientSecret||r.client_secret,serverId:s,redirectUri:j()};if(l)try{l()}catch(e){console.error("Failed to prepare for OAuth redirect",e)}try{p(u,JSON.stringify(b)),p(h,window.location.href)}catch(e){throw Error("Unable to access browser storage for OAuth. Please enable storage and retry.")}window.location.href=f}catch(t){console.error("Failed to start OAuth flow",t),n("error");let e=(0,eJ.extractErrorMessage)(t);o(e),C.default.error(e)}},[e,t,s,l]),v=(0,b.useCallback)(async()=>{if(m.current)return;let t=null,s=null;try{let e=g(x);if(!e)return;m.current=!0,t=JSON.parse(e);let r=g(u);s=r?JSON.parse(r):null}catch(e){f(),m.current=!1,o("Failed to resume OAuth flow. Please retry."),n("error"),C.default.error("Failed to resume OAuth flow. Please retry.");return}if(!t){m.current=!1;return}try{window.sessionStorage.removeItem(x),window.localStorage.removeItem(x)}catch(e){}try{if(!s||!s.state||!s.codeVerifier||!s.serverId)throw Error("OAuth session state was lost. This can happen if you have strict browser privacy settings. Please try again and ensure cookies/storage is enabled.");if(!t.state||t.state!==s.state)throw Error("OAuth state mismatch. Please retry.");if(t.error)throw Error(t.error_description||t.error);if(!t.code)throw Error("Authorization code missing in callback.");n("exchanging");let l=await (0,_.exchangeMcpOAuthToken)({serverId:s.serverId,code:t.code,clientId:s.clientId,clientSecret:s.clientSecret,codeVerifier:s.codeVerifier,redirectUri:s.redirectUri,accessToken:e});r(l),d(l),n("success"),o(null),C.default.success("OAuth token retrieved successfully")}catch(t){let e=(0,eJ.extractErrorMessage)(t);o(e),n("error"),C.default.error(e)}finally{f(),setTimeout(()=>{m.current=!1},1e3)}},[r]);return(0,b.useEffect)(()=>{v()},[v]),{startOAuthFlow:y,status:a,error:i,tokenResponse:c}},eZ="../ui/assets/logos/mcp_logo.png",eX=[eo.AUTH_TYPE.API_KEY,eo.AUTH_TYPE.BEARER_TOKEN,eo.AUTH_TYPE.TOKEN,eo.AUTH_TYPE.BASIC],e0=[...eX,eo.AUTH_TYPE.OAUTH2,eo.AUTH_TYPE.AWS_SIGV4],e2="litellm-mcp-oauth-create-state",e1=e=>Array.isArray(e)?e.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value??""),e},{}):{},e5=({userRole:e,accessToken:r,onCreateSuccess:a,isModalVisible:n,setModalVisible:i,availableAccessGroups:o,prefillData:c,onBackToDiscovery:d})=>{let[m]=D.Form.useForm(),[u,x]=(0,b.useState)(!1),[f,j]=(0,b.useState)({}),[y,v]=(0,b.useState)({}),[N,w]=(0,b.useState)(null),[S,T]=(0,b.useState)(!1),[k,A]=(0,b.useState)([]),[I,P]=(0,b.useState)({}),[O,M]=(0,b.useState)({}),[F,E]=(0,b.useState)(""),[L,R]=(0,b.useState)([]),[z,U]=(0,b.useState)(""),[B,q]=(0,b.useState)(null),[V,$]=(0,b.useState)(void 0),[K,W]=(0,b.useState)(null),{tools:J,isLoadingTools:Y,toolsError:G,toolsErrorStackTrace:Q,canFetchTools:Z,fetchTools:X,clearTools:ee}=ek({accessToken:r,oauthAccessToken:B,formValues:y,enabled:!0}),et=y.auth_type,es=!!et&&eX.includes(et),er=et===eo.AUTH_TYPE.OAUTH2,ec=et===eo.AUTH_TYPE.AWS_SIGV4,ed=er&&y.oauth_flow_type===eo.OAUTH_FLOW.M2M,{startOAuthFlow:em,status:ex,error:eh,tokenResponse:ep}=eQ({accessToken:r,getCredentials:()=>m.getFieldValue("credentials"),getTemporaryPayload:()=>{let e=m.getFieldsValue(!0),t=e.transport||F,s=e.url||(t===eo.TRANSPORT.OPENAPI?e.spec_path:void 0);if(!s||!t)return null;let r=e1(e.static_headers);return{server_id:void 0,server_name:e.server_name,alias:e.alias,description:e.description,url:s,transport:t===eo.TRANSPORT.OPENAPI?"http":t,auth_type:eo.AUTH_TYPE.OAUTH2,credentials:e.credentials,authorization_url:e.authorization_url,token_url:e.token_url,registration_url:e.registration_url,mcp_access_groups:e.mcp_access_groups,static_headers:r,command:e.command,args:e.args,env:e.env}},onTokenReceived:e=>{if(q(e?.access_token??null),e?.access_token){let t={access_token:e.access_token,...e.refresh_token&&{refresh_token:e.refresh_token},...e.expires_in&&{expires_in:e.expires_in},...e.scope&&{scope:e.scope}};m.setFieldsValue({credentials:t}),C.default.success("OAuth authorization successful! Please click 'Create MCP Server' to save the configuration.")}},onBeforeRedirect:()=>{try{let e=m.getFieldsValue(!0);(0,eG.setSecureItem)(e2,JSON.stringify({modalVisible:n,formValues:e,transportType:F,costConfig:f,allowedTools:k,searchValue:z,aliasManuallyEdited:S,logoUrl:V}))}catch(e){console.warn("Failed to persist MCP create state",e)}}});b.default.useEffect(()=>{let e=(0,eG.getSecureItem)(e2);if(e)try{let t=JSON.parse(e);t.modalVisible&&i(!0);let s=t.formValues?.transport||t.transportType||"";s&&E(s),t.formValues&&w({values:t.formValues,transport:s}),t.costConfig&&j(t.costConfig),t.allowedTools&&A(t.allowedTools),t.searchValue&&U(t.searchValue),"boolean"==typeof t.aliasManuallyEdited&&T(t.aliasManuallyEdited),t.logoUrl&&$(t.logoUrl)}catch(e){console.error("Failed to restore MCP create state",e)}finally{window.sessionStorage.removeItem(e2)}},[m,i]),b.default.useEffect(()=>{N&&(F||N.transport,(!N.transport||F)&&(m.setFieldsValue(N.values),v(N.values),w(null)))},[N,m,F]),b.default.useEffect(()=>{if(!n||!c)return;let e=(c.name||"").replace(/[^a-zA-Z0-9_]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,""),t=c.transport||"";E(t);let s={server_name:e,alias:e,description:c.description||"",transport:t};if("stdio"===t){let e={};if(c.command&&(e.command=c.command),c.args&&c.args.length>0&&(e.args=c.args),c.env_vars&&c.env_vars.length>0){let t={};for(let e of c.env_vars)t[e.name]=e.description?`<${e.description}>`:"";e.env=t}Object.keys(e).length>0&&(s.stdio_config=JSON.stringify(e,null,2))}else c.url&&(s.url=c.url);m.setFieldsValue(s),v(s),T(!1)},[n,c,m]);let eg=async e=>{x(!0);try{let{static_headers:t,stdio_config:s,credentials:l,allow_all_keys:n,available_on_public_internet:o,delegate_auth_to_upstream:c,token_validation_json:d,...u}=e,h=u.mcp_access_groups,p=e1(t),g=l&&"object"==typeof l?Object.entries(l).reduce((e,[t,s])=>{if(null==s||""===s)return e;if("scopes"===t){if(Array.isArray(s)){let r=s.filter(e=>null!=e&&""!==e);r.length>0&&(e[t]=r)}}else e[t]=s;return e},{}):void 0,b={};if(s&&"stdio"===F)try{let e=JSON.parse(s),t=e;if(e.mcpServers&&"object"==typeof e.mcpServers){let s=Object.keys(e.mcpServers);if(s.length>0){let r=s[0];t=e.mcpServers[r],u.server_name||(u.server_name=r.replace(/-/g,"_"))}}b={command:t.command,args:t.args,env:t.env},console.log("Parsed stdio config:",b)}catch(e){C.default.fromBackend("Invalid JSON in stdio configuration");return}u.transport===eo.TRANSPORT.OPENAPI&&(u.transport="http");let y=null;if(d&&""!==d.trim())try{y=JSON.parse(d)}catch{C.default.fromBackend("Invalid JSON in Token Validation Rules"),x(!1);return}let v={...u,...b,stdio_config:void 0,mcp_info:{server_name:u.server_name||u.url,description:u.description,logo_url:V||void 0,mcp_server_cost_info:Object.keys(f).length>0?f:null},mcp_access_groups:h,alias:u.alias,allowed_tools:k.length>0?k:null,tool_name_to_display_name:Object.keys(I).length>0?I:null,tool_name_to_description:Object.keys(O).length>0?O:null,allow_all_keys:!!n,available_on_public_internet:!!o,delegate_auth_to_upstream:!!c,static_headers:p,...null!==y&&{token_validation:y}};if(v.static_headers=p,u.auth_type&&e0.includes(u.auth_type)&&g&&Object.keys(g).length>0&&(v.credentials=g),console.log(`Payload: ${JSON.stringify(v)}`),null!=r){let e=ej?await (0,_.createMCPServer)(r,v):await (0,_.registerMCPServer)(r,v);C.default.success(ej?"MCP Server created successfully":"MCP Server submitted for admin review"),m.resetFields(),j({}),ee(),A([]),T(!1),$(void 0),i(!1),a(e)}}catch(t){let e=t instanceof Error?t.message:String(t);C.default.fromBackend(ej?`Error creating MCP Server: ${e}`:`Error submitting MCP Server: ${e}`)}finally{x(!1)}},eb=()=>{m.resetFields(),j({}),ee(),A([]),T(!1),$(void 0),i(!1)};b.default.useEffect(()=>{if(!S&&y.server_name){let e=y.server_name.replace(/\s+/g,"_");m.setFieldsValue({alias:e}),v(t=>({...t,alias:e}))}},[y.server_name]),b.default.useEffect(()=>{n||v({})},[n]);let ej=(0,s.isAdminRole)(e);return(0,t.jsx)(h.Modal,{title:(0,t.jsxs)("div",{className:"flex items-center pb-4 border-b border-gray-100",style:{gap:12},children:[d&&(0,t.jsx)("button",{onClick:d,className:"text-sm text-blue-600 hover:text-blue-800 cursor-pointer bg-transparent border-none",style:{flexShrink:0},children:"←"}),(0,t.jsx)("img",{src:eZ,alt:"MCP Logo",className:"w-8 h-8 object-contain",style:{height:"20px",width:"20px",objectFit:"contain"}}),(0,t.jsx)("h2",{className:"text-xl font-semibold text-gray-900",children:ej?"Add New MCP Server":"Submit MCP Server for Review"})]}),open:n,width:1e3,onCancel:eb,footer:null,forceRender:!0,className:"top-8",styles:{body:{padding:"24px"},header:{padding:"24px 24px 0 24px",border:"none"}},children:(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsxs)(D.Form,{form:m,onFinish:eg,onValuesChange:(e,t)=>v(t),layout:"vertical",className:"space-y-6",children:[!ej&&(0,t.jsxs)("div",{className:"rounded-md bg-blue-50 border border-blue-200 px-4 py-3 text-sm text-blue-800",children:["Your submission will be sent for admin review before it becomes active."," ","Note: the request must be made with a team-scoped API key."]}),(0,t.jsxs)("div",{className:"grid grid-cols-1 gap-6",children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["MCP Server Name",(0,t.jsx)(g.Tooltip,{title:"Best practice: Use a descriptive name that indicates the server's purpose (e.g., 'GitHub_MCP', 'Email_Service'). Cannot contain spaces or hyphens; use underscores instead. Names must comply with SEP-986 and will be rejected if invalid (https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names).",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"server_name",rules:[{required:!1,message:"Please enter a server name"},{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(ei.TextInput,{placeholder:"e.g., GitHub_MCP, Zapier_MCP, etc.",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Alias",(0,t.jsx)(g.Tooltip,{title:"A short, unique identifier for this server. Defaults to the server name if not provided. Cannot contain spaces or hyphens; use underscores instead.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"alias",rules:[{required:!1},{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(ei.TextInput,{placeholder:"e.g., GitHub_MCP, Zapier_MCP, etc.",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500",onChange:()=>T(!0)})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Description"}),name:"description",rules:[{required:!1,message:"Please enter a server description"}],children:(0,t.jsx)(ei.TextInput,{placeholder:"Brief description of what this server does",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(e$,{value:V,onChange:$}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"GitHub / Source URL"}),name:"source_url",children:(0,t.jsx)(ei.TextInput,{placeholder:"https://github.com/org/mcp-server",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"Transport Type"}),name:"transport",rules:[{required:!0,message:"Please select a transport type"}],children:(0,t.jsxs)(p.Select,{placeholder:"Select transport",className:"rounded-lg",size:"large",onChange:e=>{E(e),"stdio"===e?m.setFieldsValue({url:void 0,spec_path:void 0,auth_type:void 0,credentials:void 0}):e===eo.TRANSPORT.OPENAPI?m.setFieldsValue({url:void 0,command:void 0,args:void 0,env:void 0}):m.setFieldsValue({spec_path:void 0,command:void 0,args:void 0,env:void 0})},value:F,children:[(0,t.jsx)(p.Select.Option,{value:"http",children:"Streamable HTTP (Recommended)"}),(0,t.jsx)(p.Select.Option,{value:"sse",children:"Server-Sent Events (SSE)"}),(0,t.jsx)(p.Select.Option,{value:"stdio",children:"Standard Input/Output (stdio)"}),(0,t.jsx)(p.Select.Option,{value:eo.TRANSPORT.OPENAPI,children:"OpenAPI Spec"})]})}),("http"===F||"sse"===F)&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsx)("span",{className:"text-sm font-medium text-gray-700",children:"MCP Server URL"}),name:"url",rules:[{required:!0,message:"Please enter a server URL"},{validator:(e,t)=>eK(t)}],children:(0,t.jsx)(H.Input,{placeholder:"https://your-mcp-server.com",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),F===eo.TRANSPORT.OPENAPI&&(0,t.jsx)(eU,{form:m,accessToken:n?r:null,onValuesChange:e=>v(t=>({...t,...e})),onKeyToolsChange:R,onLogoUrlChange:$,onOAuthDocsUrlChange:W}),F===eo.TRANSPORT.OPENAPI&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center gap-2",children:["BYOK (Bring Your Own Key)",(0,t.jsx)(g.Tooltip,{title:"When enabled, each user provides their own API key for this service. Keys are stored per-user and never shared.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"is_byok",valuePropName:"checked",children:(0,t.jsx)(el.Switch,{})}),(0,t.jsx)(D.Form.Item,{noStyle:!0,shouldUpdate:(e,t)=>e.is_byok!==t.is_byok||e.auth_type!==t.auth_type,children:({getFieldValue:e})=>e("is_byok")?(0,t.jsxs)(t.Fragment,{children:[e("auth_type")&&"none"!==e("auth_type")&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 rounded-lg text-sm text-blue-700 flex items-start gap-2",children:[(0,t.jsx)(en.InfoCircleOutlined,{className:"mt-0.5 flex-shrink-0"}),(0,t.jsxs)("span",{children:["User keys will be sent as:"," ",(0,t.jsxs)("code",{className:"font-mono bg-blue-100 px-1 rounded",children:["bearer_token"===e("auth_type")&&"Authorization: Bearer {key}","token"===e("auth_type")&&"Authorization: token {key}","api_key"===e("auth_type")&&"x-api-key: {key}","basic"===e("auth_type")&&"Authorization: Basic {key}","authorization"===e("auth_type")&&"Authorization: {key}"]}),!e("auth_type")&&"Set Authentication Type below to specify the format."]})]}),!e("auth_type")&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-yellow-50 rounded-lg text-sm text-yellow-700 flex items-start gap-2",children:[(0,t.jsx)(en.InfoCircleOutlined,{className:"mt-0.5 flex-shrink-0"}),(0,t.jsxs)("span",{children:["Set the ",(0,t.jsx)("strong",{children:"Authentication Type"})," below to specify how user keys are sent (e.g., Bearer Token, API Key header)."]})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700",children:["Access Description",(0,t.jsx)(g.Tooltip,{title:"List of permissions shown to users in the connection modal (e.g. 'Create and manage Jira issues')",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"byok_description",children:(0,t.jsx)(p.Select,{mode:"tags",placeholder:"Add access description items (press Enter after each)",className:"w-full",tokenSeparators:[","]})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700",children:["API Key Help URL",(0,t.jsx)(g.Tooltip,{title:"Optional link shown to users to help them find their API key",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"byok_api_key_help_url",children:(0,t.jsx)(H.Input,{placeholder:"https://docs.example.com/api-keys"})})]}):null})]}),"stdio"!==F&&""!==F&&(0,t.jsx)(ea.Collapse,{defaultActiveKey:["auth"],className:"mb-4",items:[{key:"auth",label:(0,t.jsx)("span",{className:"text-sm font-semibold text-gray-700",children:"Authentication"}),children:(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{name:"auth_type",rules:[{required:!0,message:"Please select an auth type"}],children:(0,t.jsxs)(p.Select,{placeholder:"Select auth type",className:"rounded-lg",size:"large",children:[(0,t.jsx)(p.Select.Option,{value:"none",children:"None"}),(0,t.jsx)(p.Select.Option,{value:"api_key",children:"API Key"}),(0,t.jsx)(p.Select.Option,{value:"bearer_token",children:"Bearer Token"}),(0,t.jsx)(p.Select.Option,{value:"token",children:"Token"}),(0,t.jsx)(p.Select.Option,{value:"basic",children:"Basic Auth"}),(0,t.jsx)(p.Select.Option,{value:"oauth2",children:"OAuth"}),(0,t.jsx)(p.Select.Option,{value:"aws_sigv4",children:"AWS SigV4 (Bedrock AgentCore MCPs)"})]})}),es&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Authentication Value",(0,t.jsx)(g.Tooltip,{title:"Token, password, or header value to send with each request for the selected auth type.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","auth_value"],rules:[{validator:(e,t)=>t&&"string"==typeof t&&""===t.trim()?Promise.reject(Error("Authentication value cannot be empty whitespace")):Promise.resolve()}],children:(0,t.jsx)(ei.TextInput,{type:"password",placeholder:"Enter token or secret",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),er&&(0,t.jsx)(eu,{isM2M:ed,initialFlowType:eo.OAUTH_FLOW.INTERACTIVE,docsUrl:K,oauthFlow:{startOAuthFlow:em,status:ex,error:eh,tokenResponse:ep}})]})}]}),"stdio"!==F&&""!==F&&ec&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-2",children:["For MCP servers hosted on AWS Bedrock AgentCore."," ",(0,t.jsx)("a",{href:"https://docs.litellm.ai/docs/mcp_aws_sigv4",target:"_blank",rel:"noopener noreferrer",className:"text-blue-500 hover:text-blue-700",children:"View docs →"})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Region",(0,t.jsx)(g.Tooltip,{title:"AWS region for SigV4 signing (e.g., us-east-1)",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_region_name"],rules:[{required:!0,message:"AWS region is required for SigV4 auth"}],children:(0,t.jsx)(H.Input,{placeholder:"us-east-1",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Service Name",(0,t.jsx)(g.Tooltip,{title:"AWS service name for SigV4 signing. Defaults to 'bedrock-agentcore'.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_service_name"],children:(0,t.jsx)(H.Input,{placeholder:"bedrock-agentcore",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Access Key ID",(0,t.jsx)(g.Tooltip,{title:"Optional. If not provided, falls back to the boto3 credential chain (IAM role, env vars, etc.).",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_access_key_id"],dependencies:[["credentials","aws_secret_access_key"]],rules:[({getFieldValue:e})=>({validator:(t,s)=>e(["credentials","aws_secret_access_key"])&&!s?Promise.reject(Error("Access Key ID is required when Secret Access Key is provided")):Promise.resolve()})],children:(0,t.jsx)(H.Input.Password,{placeholder:"AKIA... (optional — uses IAM role if blank)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Secret Access Key",(0,t.jsx)(g.Tooltip,{title:"Optional. Required if AWS Access Key ID is provided.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_secret_access_key"],dependencies:[["credentials","aws_access_key_id"]],rules:[({getFieldValue:e})=>({validator:(t,s)=>e(["credentials","aws_access_key_id"])&&!s?Promise.reject(Error("Secret Access Key is required when Access Key ID is provided")):Promise.resolve()})],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter secret key (optional — uses IAM role if blank)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Token",(0,t.jsx)(g.Tooltip,{title:"Optional. Only needed for temporary STS credentials.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_token"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter session token (optional)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Role ARN",(0,t.jsx)(g.Tooltip,{title:"Optional. IAM role ARN to assume via STS before signing. If set, LiteLLM calls sts:AssumeRole to get temporary credentials. Uses ambient credentials (IAM role, env vars) as the source identity unless explicit keys are also provided.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_role_name"],children:(0,t.jsx)(H.Input,{placeholder:"arn:aws:iam::123456789012:role/MyRole (optional)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Name",(0,t.jsx)(g.Tooltip,{title:"Optional. Session name for the AssumeRole call — appears in CloudTrail logs. Auto-generated if omitted.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_name"],children:(0,t.jsx)(H.Input,{placeholder:"litellm-prod (optional, auto-generated if blank)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})})]}),(0,t.jsx)(eO,{isVisible:"stdio"===F})]}),(0,t.jsx)("div",{className:"mt-8",children:(0,t.jsx)(eR,{availableAccessGroups:o,mcpServer:null,searchValue:z,setSearchValue:U,getAccessGroupOptions:()=>{let e=o.map(e=>({value:e,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-green-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:e})]})}));return z&&!o.some(e=>e.toLowerCase().includes(z.toLowerCase()))&&e.push({value:z,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-blue-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:z}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-1",children:"create new group"})]})}),e}})}),(0,t.jsx)("div",{className:"mt-8 pt-6 border-t border-gray-200",children:(0,t.jsx)(e_,{formValues:y,tools:J,isLoadingTools:Y,toolsError:G,toolsErrorStackTrace:Q,canFetchTools:Z,fetchTools:X})}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(eP,{accessToken:r,oauthAccessToken:B,formValues:y,allowedTools:k,existingAllowedTools:null,onAllowedToolsChange:A,toolNameToDisplayName:I,toolNameToDescription:O,onToolNameToDisplayNameChange:P,onToolNameToDescriptionChange:M,keyTools:L,externalTools:J,externalIsLoading:Y,externalError:G,externalCanFetch:Z})}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(ef,{value:f,onChange:j,tools:J.filter(e=>k.includes(e.name)),disabled:!1})}),(0,t.jsxs)("div",{className:"flex items-center justify-end space-x-3 pt-6 border-t border-gray-100",children:[(0,t.jsx)(l.Button,{variant:"secondary",onClick:eb,children:"Cancel"}),(0,t.jsx)(l.Button,{variant:"primary",loading:u,children:u?"Creating...":"Add MCP Server"})]})]})})})};var e4=e.i(175712),e6=e.i(118366),e3=e.i(475254);let e7=(0,e3.default)("code",[["path",{d:"m16 18 6-6-6-6",key:"eg8j8"}],["path",{d:"m8 6-6 6 6 6",key:"ppft3o"}]]);e.s(["Code",()=>e7],758472);let e8=(0,e3.default)("terminal",[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]]),e9=(0,e3.default)("globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);var te=e.i(634831),tt=e.i(438100);let ts=(0,e3.default)("zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);var tr=e.i(500330);let{Title:tl,Text:ta}=f.Typography,{Panel:tn}=ea.Collapse,ti=({icon:e,title:s,description:r,children:l,serverName:a,accessGroups:n=["dev-group"]})=>{let[i,o]=(0,b.useState)(!1);return(0,t.jsxs)(e4.Card,{className:"border border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3",children:[(0,t.jsx)("span",{className:"p-2 rounded-lg bg-gray-50",children:e}),(0,t.jsxs)("div",{children:[(0,t.jsx)(tl,{level:5,className:"mb-0",children:s}),(0,t.jsx)(ta,{className:"text-gray-600",children:r})]})]}),a&&("Implementation Example"===s||"Configuration"===s)&&(0,t.jsxs)(D.Form.Item,{className:"mb-4",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-2",children:[(0,t.jsx)(el.Switch,{size:"small",checked:i,onChange:o}),(0,t.jsxs)(ta,{className:"text-sm",children:["Limit tools to specific MCP servers or MCP groups by passing the ",(0,t.jsx)("code",{children:"x-mcp-servers"})," header"]})]}),i&&(0,t.jsx)(ej.Alert,{className:"mt-2",type:"info",showIcon:!0,message:"Two Options",description:(0,t.jsxs)("div",{children:[(0,t.jsxs)("p",{children:[(0,t.jsx)("strong",{children:"Option 1:"})," Get a specific server: ",(0,t.jsxs)("code",{children:['"',a.replace(/\s+/g,"_"),'"']})]}),(0,t.jsxs)("p",{children:[(0,t.jsx)("strong",{children:"Option 2:"})," Get a group of MCPs: ",(0,t.jsx)("code",{children:'"dev-group"'})]}),(0,t.jsxs)("p",{className:"mt-2 text-sm text-gray-600",children:["You can also mix both: ",(0,t.jsx)("code",{children:'"Server1,dev-group"'})]})]})})]}),b.default.Children.map(l,e=>{if(b.default.isValidElement(e)&&e.props.hasOwnProperty("code")&&e.props.hasOwnProperty("copyKey")){let t=e.props.code;if(t&&t.includes('"headers":'))return b.default.cloneElement(e,{code:t.replace(/"headers":\s*{[^}]*}/,`"headers": ${JSON.stringify((()=>{let e={"x-litellm-api-key":"Bearer YOUR_LITELLM_API_KEY"};if(i&&a){let t=[a.replace(/\s+/g,"_"),...n].join(",");e["x-mcp-servers"]=t}return e})(),null,8)}`)})}return e})]})},to=({currentServerAccessGroups:e=[]})=>{let s=(0,_.getProxyBaseUrl)(),[r,l]=(0,b.useState)({}),[u,x]=(0,b.useState)({openai:[],litellm:[],cursor:[],http:[]}),[h]=(0,b.useState)("Zapier_MCP"),p=async(e,t)=>{await (0,tr.copyToClipboard)(e)&&(l(e=>({...e,[t]:!0})),setTimeout(()=>{l(e=>({...e,[t]:!1}))},2e3))},g=({code:e,copyKey:s,title:l,className:a=""})=>(0,t.jsxs)("div",{className:"relative group",children:[l&&(0,t.jsxs)("div",{className:"flex items-center gap-2 mb-2",children:[(0,t.jsx)(e7,{size:16,className:"text-blue-600"}),(0,t.jsx)(ta,{strong:!0,className:"text-gray-700",children:l})]}),(0,t.jsxs)(e4.Card,{className:`bg-gray-50 border border-gray-200 relative ${a}`,children:[(0,t.jsx)(eb.Button,{type:"text",size:"small",icon:r[s]?(0,t.jsx)(k.CheckIcon,{size:12}):(0,t.jsx)(e6.CopyIcon,{size:12}),onClick:()=>p(e,s),className:`absolute top-2 right-2 z-10 transition-all duration-200 ${r[s]?"text-green-600 bg-green-50 border-green-200":"text-gray-500 hover:text-gray-700 hover:bg-gray-100"}`}),(0,t.jsx)("pre",{className:"text-sm overflow-x-auto pr-10 text-gray-800 font-mono leading-relaxed",children:e})]})]}),f=({step:e,title:s,children:r})=>(0,t.jsxs)("div",{className:"flex gap-4",children:[(0,t.jsx)("div",{className:"flex-shrink-0",children:(0,t.jsx)("div",{className:"w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold",children:e})}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsx)(ta,{strong:!0,className:"text-gray-800 block mb-2",children:s}),r]})]});return(0,t.jsx)("div",{children:(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(m.Title,{className:"text-3xl font-bold text-gray-900 mb-3",children:"Connect to your MCP client"}),(0,t.jsx)(d.Text,{className:"text-lg text-gray-600",children:"Use tools directly from any MCP client with LiteLLM MCP. Enable your AI assistant to perform real-world tasks through a simple, secure connection."})]}),(0,t.jsxs)(n.TabGroup,{className:"w-full",children:[(0,t.jsx)(i.TabList,{className:"flex justify-start mt-8 mb-6",children:(0,t.jsxs)("div",{className:"flex bg-gray-100 p-1 rounded-lg",children:[(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(e7,{size:18}),"OpenAI API"]})}),(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(ts,{size:18}),"LiteLLM Proxy"]})}),(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(e8,{size:18}),"Cursor"]})}),(0,t.jsx)(a.Tab,{className:"px-6 py-3 rounded-md transition-all duration-200",children:(0,t.jsxs)("span",{className:"flex items-center gap-2 font-medium",children:[(0,t.jsx)(e9,{size:18}),"Streamable HTTP"]})})]})}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsx)(o.TabPanel,{className:"mt-6",children:(0,t.jsx)(()=>(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsxs)("div",{className:"bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-100",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3",children:[(0,t.jsx)(e7,{className:"text-blue-600",size:24}),(0,t.jsx)(tl,{level:4,className:"mb-0 text-blue-900",children:"OpenAI Responses API Integration"})]}),(0,t.jsx)(ta,{className:"text-blue-700",children:"Connect OpenAI Responses API to your LiteLLM MCP server for seamless tool integration"})]}),(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsx)(ti,{icon:(0,t.jsx)(tt.KeyIcon,{className:"text-blue-600",size:16}),title:"API Key Setup",description:"Configure your OpenAI API key for authentication",children:(0,t.jsxs)(eM.Space,{direction:"vertical",size:"middle",className:"w-full",children:[(0,t.jsx)("div",{children:(0,t.jsxs)(ta,{children:["Get your API key from the"," ",(0,t.jsxs)("a",{href:"https://platform.openai.com/api-keys",target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:text-blue-700 inline-flex items-center gap-1",children:["OpenAI platform ",(0,t.jsx)(te.ExternalLinkIcon,{size:12})]})]})}),(0,t.jsx)(g,{title:"Environment Variable",code:'export OPENAI_API_KEY="sk-..."',copyKey:"openai-env"})]})}),(0,t.jsx)(ti,{icon:(0,t.jsx)(P.ServerIcon,{className:"text-blue-600",size:16}),title:"MCP Server Information",description:"Connection details for your LiteLLM MCP server",children:(0,t.jsx)(g,{title:"Server URL",code:`${s}/mcp`,copyKey:"openai-server-url"})}),(0,t.jsx)(ti,{icon:(0,t.jsx)(e7,{className:"text-blue-600",size:16}),title:"Implementation Example",description:"Complete cURL example for using the Responses API",serverName:"Zapier Gmail",accessGroups:["dev-group"],children:(0,t.jsx)(g,{code:`curl --location 'https://api.openai.com/v1/responses' \\ +>>>>>>>> origin/litellm_internal_staging:litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $OPENAI_API_KEY" \\ --data '{ @@ -66,9 +80,15 @@ } } } +<<<<<<<< HEAD:litellm/proxy/_experimental/out/_next/static/chunks/0279e5299e9f6e98.js +}`,copyKey:"cursor-config",className:"text-xs"})})]})]})]})]}),{})}),(0,t.jsx)(o.TabPanel,{className:"mt-6",children:(0,t.jsx)(()=>(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsxs)("div",{className:"bg-gradient-to-r from-green-50 to-teal-50 p-6 rounded-lg border border-green-100",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3",children:[(0,t.jsx)(e9,{className:"text-green-600",size:24}),(0,t.jsx)(tl,{level:4,className:"mb-0 text-green-900",children:"Streamable HTTP Transport"})]}),(0,t.jsx)(ta,{className:"text-green-700",children:"Connect to LiteLLM MCP using HTTP transport. Compatible with any MCP client that supports HTTP streaming."})]}),(0,t.jsx)(ti,{icon:(0,t.jsx)(e9,{className:"text-green-600",size:16}),title:"Universal MCP Connection",description:"Use this URL with any MCP client that supports HTTP transport",children:(0,t.jsxs)(eM.Space,{direction:"vertical",size:"middle",className:"w-full",children:[(0,t.jsx)("div",{children:(0,t.jsx)(ta,{children:"Each MCP client supports different transports. Refer to your client documentation to determine the appropriate transport method."})}),(0,t.jsx)(g,{title:"Server URL",code:`${s}/mcp`,copyKey:"http-server-url"}),(0,t.jsx)(g,{title:"Headers Configuration",code:JSON.stringify({"x-litellm-api-key":"Bearer YOUR_LITELLM_API_KEY"},null,2),copyKey:"http-headers"}),(0,t.jsx)("div",{className:"mt-4",children:(0,t.jsx)(eb.Button,{type:"link",className:"p-0 h-auto text-blue-600 hover:text-blue-700",href:"https://modelcontextprotocol.io/docs/concepts/transports",icon:(0,t.jsx)(te.ExternalLinkIcon,{size:14}),children:"Learn more about MCP transports"})})]})})]}),{})})]})]})]})})};var tc=e.i(752978),td=e.i(591935),tm=e.i(492030);let tu=({server:e,isLoadingHealth:s,isRechecking:r,onRecheck:l})=>{let[a,n]=(0,b.useState)(!1),i=e.status||"unknown",o=e.last_health_check,c=e.health_check_error;if(s||r)return(0,t.jsxs)("span",{className:"inline-flex items-center gap-1.5 text-xs text-gray-400 px-2 py-0.5 rounded-full bg-gray-50 border border-gray-100",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-gray-300 animate-pulse"}),"Checking"]});let d=!!l,m=(0,t.jsxs)("div",{className:"max-w-xs",children:[(0,t.jsxs)("div",{className:"font-semibold mb-1",children:["Health Status: ",i]}),o&&(0,t.jsxs)("div",{className:"text-xs mb-1",children:["Last Check: ",new Date(o).toLocaleString()]}),c&&(0,t.jsxs)("div",{className:"text-xs",children:[(0,t.jsx)("div",{className:"font-medium text-red-400 mb-1",children:"Error:"}),(0,t.jsx)("div",{className:"break-words",children:c})]}),!o&&!c&&(0,t.jsx)("div",{className:"text-xs text-gray-400",children:"No health check data available"}),d&&(0,t.jsx)("div",{className:"text-xs text-gray-400 mt-1",children:"Click to recheck"})]});return(0,t.jsx)(g.Tooltip,{title:m,placement:"top",children:(0,t.jsxs)("span",{className:`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${(e=>{switch(e){case"healthy":return"text-green-700 bg-green-50 border border-green-200";case"unhealthy":return"text-red-700 bg-red-50 border border-red-200";default:return"text-gray-600 bg-gray-50 border border-gray-200"}})(i)} ${d?"cursor-pointer hover:opacity-80":"cursor-default"}`,onMouseEnter:()=>n(!0),onMouseLeave:()=>n(!1),onClick:d?()=>l(e.server_id):void 0,children:[(0,t.jsx)("span",{children:a&&d?"↻":(e=>{switch(e){case"healthy":return"✓";case"unhealthy":return"✗";default:return"?"}})(i)}),a&&d?"Recheck":i.charAt(0).toUpperCase()+i.slice(1)]})})};var tx=e.i(530212),th=e.i(848725);let tp=b.forwardRef(function(e,t){return b.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),b.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"}))});var tg=e.i(350967),tf=e.i(954616);function tb(e){if(!e)return[];if(Array.isArray(e))return e.map(e=>tj(e)).filter(e=>void 0!==e);let t=tj(e);return void 0===t?[]:[t]}function tj(e,t){if(!e)return;let s=void 0!==t?t:e.default;if("object"===e.type){let t="object"!=typeof s||null===s||Array.isArray(s)?{}:{...s};return e.properties&&Object.entries(e.properties).forEach(([e,s])=>{t[e]=tj(s,t[e])}),t}if("array"===e.type){if(Array.isArray(s)){let t=e.items;if(!t)return s;if(0===s.length){let e=tb(t);return e.length?e:s}return Array.isArray(t)?s.map((e,s)=>tj(t[s]??t[t.length-1],e)):s.map(e=>tj(t,e))}return void 0!==s?s:tb(e.items)}if(void 0!==s)return s;switch(e.type){case"integer":case"number":return 0;case"boolean":return!1;default:return""}}let ty=e=>{let t=tj(e);if("object"===e.type||"array"===e.type){let s="array"===e.type?[]:{};return JSON.stringify(t??s,null,2)}return t};function tv({tool:e,onSubmit:s,isLoading:r,result:a,error:n,onClose:i}){let[o]=D.Form.useForm(),[c,d]=b.default.useState("formatted"),[m,u]=b.default.useState(null),[x,h]=b.default.useState(null),f=b.default.useMemo(()=>"string"==typeof e.inputSchema?{type:"object",properties:{input:{type:"string",description:"Input for this tool"}},required:["input"]}:e.inputSchema,[e.inputSchema]),j=b.default.useMemo(()=>f.properties&&f.properties.params&&"object"===f.properties.params.type&&f.properties.params.properties?{type:"object",properties:f.properties.params.properties,required:f.properties.params.required||[]}:f,[f]);b.default.useEffect(()=>{if(o.resetFields(),!j.properties)return;let e={};Object.entries(j.properties).forEach(([t,s])=>{e[t]=ty(s)}),o.setFieldsValue(e)},[o,j,e]),b.default.useEffect(()=>{m&&(a||n)&&h(Date.now()-m)},[a,n,m]);let y=async e=>{try{if(navigator.clipboard&&window.isSecureContext)return await navigator.clipboard.writeText(e),!0;{let t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.focus(),t.select();let s=document.execCommand("copy");if(document.body.removeChild(t),!s)throw Error("execCommand failed");return!0}}catch(e){return console.error("Copy failed:",e),!1}},v=async()=>{await y(JSON.stringify(a,null,2))?C.default.success("Result copied to clipboard"):C.default.fromBackend("Failed to copy result")},N=async()=>{await y(e.name)?C.default.success("Tool name copied to clipboard"):C.default.fromBackend("Failed to copy tool name")};return(0,t.jsxs)("div",{className:"space-y-4 h-full",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between pb-3 border-b border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-3",children:[e.mcp_info.logo_url&&(0,t.jsx)("img",{src:e.mcp_info.logo_url,alt:`${e.mcp_info.server_name} logo`,className:"w-6 h-6 object-contain"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2 mb-1",children:[(0,t.jsx)("h2",{className:"text-lg font-semibold text-gray-900",children:"Test Tool:"}),(0,t.jsxs)("div",{className:"group inline-flex items-center space-x-1 bg-slate-50 hover:bg-slate-100 px-3 py-1 rounded-md cursor-pointer transition-colors border border-slate-200",onClick:N,title:"Click to copy tool name",children:[(0,t.jsx)("span",{className:"font-mono text-slate-700 font-medium text-sm",children:e.name}),(0,t.jsx)("svg",{className:"w-3 h-3 text-slate-400 group-hover:text-slate-600 transition-colors",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"})})]})]}),(0,t.jsx)("p",{className:"text-xs text-gray-600",children:e.description}),(0,t.jsxs)("p",{className:"text-xs text-gray-500",children:["Provider: ",e.mcp_info.server_name]})]})]}),(0,t.jsx)(l.Button,{onClick:i,variant:"light",size:"sm",className:"text-gray-500 hover:text-gray-700",children:(0,t.jsx)("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]}),(0,t.jsxs)("div",{className:"grid grid-cols-2 gap-4 h-full",children:[(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)("div",{className:"border-b border-gray-100 px-4 py-2",children:(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:"Input Parameters"}),(0,t.jsx)(g.Tooltip,{title:"Configure the input parameters for this tool call",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-gray-400 hover:text-gray-600"})})]})}),(0,t.jsx)("div",{className:"p-4",children:(0,t.jsxs)(D.Form,{form:o,onFinish:e=>{u(Date.now()),h(null);let t={};Object.entries(e).forEach(([e,s])=>{let r=j.properties?.[e];if(r&&null!=s&&""!==s)switch(r.type){case"boolean":t[e]="true"===s||!0===s;break;case"number":case"integer":{let l=Number(s);t[e]=Number.isNaN(l)?s:"integer"===r.type?Math.trunc(l):l;break}case"object":case"array":try{let l="string"==typeof s?JSON.parse(s):s,a="object"===r.type&&null!==l&&"object"==typeof l&&!Array.isArray(l),n="array"===r.type&&Array.isArray(l);"object"===r.type&&a||"array"===r.type&&n?t[e]=l:t[e]=s}catch(r){t[e]=s}break;case"string":t[e]=String(s);break;default:t[e]=s}else null!=s&&""!==s&&(t[e]=s)}),s(f.properties&&f.properties.params&&"object"===f.properties.params.type&&f.properties.params.properties?{params:t}:t)},layout:"vertical",className:"space-y-3",children:["string"==typeof e.inputSchema?(0,t.jsx)("div",{className:"space-y-3",children:(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700",children:["Input ",(0,t.jsx)("span",{className:"text-red-500",children:"*"})]}),name:"input",rules:[{required:!0,message:"Please enter input for this tool"}],className:"mb-3",children:(0,t.jsx)(ei.TextInput,{placeholder:"Enter input for this tool",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})})}):void 0===j.properties?(0,t.jsx)("div",{className:"text-center py-6 bg-gray-50 rounded-lg border border-gray-200",children:(0,t.jsxs)("div",{className:"max-w-sm mx-auto",children:[(0,t.jsx)("h4",{className:"text-sm font-medium text-gray-900 mb-1",children:"No Parameters Required"}),(0,t.jsx)("p",{className:"text-xs text-gray-500",children:"This tool can be called without any input parameters."})]})}):(0,t.jsx)("div",{className:"space-y-3",children:Object.entries(j.properties).map(([s,r])=>{let l=ty(r),a=`${e.name}-${s}`;return(0,t.jsxs)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:[s," ",j.required?.includes(s)&&(0,t.jsx)("span",{className:"text-red-500",children:"*"}),r.description&&(0,t.jsx)(g.Tooltip,{title:r.description,children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-gray-400 hover:text-gray-600"})})]}),name:s,initialValue:l,rules:[{required:j.required?.includes(s),message:`Please enter ${s}`},..."object"===r.type||"array"===r.type?[{validator:(e,t)=>{if((null==t||""===t)&&!j.required?.includes(s))return Promise.resolve();try{let e="string"==typeof t?JSON.parse(t):t,s="object"===r.type&&null!==e&&"object"==typeof e&&!Array.isArray(e),l="array"===r.type&&Array.isArray(e);if("object"===r.type&&s||"array"===r.type&&l)return Promise.resolve();return Promise.reject(Error("object"===r.type?"Please enter a JSON object":"Please enter a JSON array"))}catch(e){return Promise.reject(Error("Invalid JSON"))}}}]:[]],className:"mb-3",children:["string"===r.type&&r.enum&&(0,t.jsxs)("select",{className:"w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors",defaultValue:l??"",children:[!j.required?.includes(s)&&(0,t.jsxs)("option",{value:"",children:["Select ",s]}),r.enum.map(e=>(0,t.jsx)("option",{value:e,children:e},e))]}),"string"===r.type&&!r.enum&&(0,t.jsx)(ei.TextInput,{placeholder:r.description||`Enter ${s}`,defaultValue:l??"",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"}),("number"===r.type||"integer"===r.type)&&(0,t.jsx)("input",{type:"number",step:"integer"===r.type?1:"any",placeholder:r.description||`Enter ${s}`,defaultValue:l??0,className:"w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"}),"boolean"===r.type&&(0,t.jsxs)(p.Select,{placeholder:`Select ${s}`,allowClear:!j.required?.includes(s),className:"w-full",children:[(0,t.jsx)(p.Select.Option,{value:!0,children:"True"}),(0,t.jsx)(p.Select.Option,{value:!1,children:"False"})]}),("object"===r.type||"array"===r.type)&&(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("textarea",{rows:"object"===r.type?6:4,placeholder:r.description||("object"===r.type?`Enter JSON object for ${s}`:`Enter JSON array for ${s}`),defaultValue:l??("object"===r.type?"{}":"[]"),spellCheck:!1,"data-testid":`textarea-${s}`,className:"w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"}),(0,t.jsx)("p",{className:"text-xs text-gray-500",children:"object"===r.type?"Provide a valid JSON object.":"Provide a valid JSON array."})]})]},a)})}),(0,t.jsx)("div",{className:"pt-3 border-t border-gray-100",children:(0,t.jsx)(l.Button,{onClick:()=>o.submit(),disabled:r,variant:"primary",className:"w-full",loading:r,children:r?"Calling Tool...":a||n?"Call Again":"Call Tool"})})]})})]}),(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)("div",{className:"border-b border-gray-100 px-4 py-2",children:(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:"Tool Result"})}),(0,t.jsx)("div",{className:"p-4",children:a||n||r?(0,t.jsxs)("div",{className:"space-y-3",children:[a&&!r&&!n&&(0,t.jsx)("div",{className:"p-2 bg-green-50 border border-green-200 rounded-lg",children:(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,t.jsx)("svg",{className:"h-4 w-4 text-green-500",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"})}),(0,t.jsx)("h4",{className:"text-xs font-medium text-green-900",children:"Tool executed successfully"}),null!==x&&(0,t.jsxs)("span",{className:"text-xs text-green-600 ml-1",children:["• ",(x/1e3).toFixed(2),"s"]})]}),(0,t.jsxs)("div",{className:"flex items-center space-x-1",children:[(0,t.jsxs)("div",{className:"flex bg-white rounded border border-green-300 p-0.5",children:[(0,t.jsx)("button",{onClick:()=>d("formatted"),className:`px-2 py-1 text-xs font-medium rounded transition-colors ${"formatted"===c?"bg-green-100 text-green-800":"text-green-600 hover:text-green-800"}`,children:"Formatted"}),(0,t.jsx)("button",{onClick:()=>d("json"),className:`px-2 py-1 text-xs font-medium rounded transition-colors ${"json"===c?"bg-green-100 text-green-800":"text-green-600 hover:text-green-800"}`,children:"JSON"})]}),(0,t.jsx)("button",{onClick:v,className:"p-1 hover:bg-green-100 rounded text-green-700",title:"Copy response",children:(0,t.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[(0,t.jsx)("rect",{x:"9",y:"9",width:"13",height:"13",rx:"2",ry:"2"}),(0,t.jsx)("path",{d:"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"})]})})]})]})}),(0,t.jsxs)("div",{className:"max-h-96 overflow-y-auto",children:[r&&(0,t.jsxs)("div",{className:"flex flex-col justify-center items-center h-48 text-gray-500",children:[(0,t.jsxs)("div",{className:"relative",children:[(0,t.jsx)("div",{className:"animate-spin rounded-full h-8 w-8 border-2 border-gray-200"}),(0,t.jsx)("div",{className:"animate-spin rounded-full h-8 w-8 border-2 border-blue-600 border-t-transparent absolute top-0"})]}),(0,t.jsx)("p",{className:"text-sm font-medium mt-3",children:"Calling tool..."}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-1",children:"Please wait while we process your request"})]}),n&&(0,t.jsx)("div",{className:"bg-red-50 border border-red-200 rounded-lg p-3",children:(0,t.jsxs)("div",{className:"flex items-start space-x-2",children:[(0,t.jsx)("div",{className:"flex-shrink-0",children:(0,t.jsx)("svg",{className:"h-4 w-4 text-red-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"})})}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2 mb-1",children:[(0,t.jsx)("h4",{className:"text-xs font-medium text-red-900",children:"Tool Call Failed"}),null!==x&&(0,t.jsxs)("span",{className:"text-xs text-red-600",children:["• ",(x/1e3).toFixed(2),"s"]})]}),(0,t.jsx)("div",{className:"bg-white border border-red-200 rounded p-2 max-h-48 overflow-y-auto",children:(0,t.jsx)("pre",{className:"text-xs whitespace-pre-wrap text-red-700 font-mono",children:n.message})})]})]})}),a&&!r&&!n&&(0,t.jsx)("div",{className:"space-y-3",children:"formatted"===c?a.map((e,s)=>(0,t.jsxs)("div",{className:"border border-gray-200 rounded-lg overflow-hidden",children:["text"===e.type&&(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bg-gray-50 px-3 py-1 border-b border-gray-200",children:(0,t.jsx)("span",{className:"text-xs font-medium text-gray-700 uppercase tracking-wide",children:"Text Response"})}),(0,t.jsx)("div",{className:"p-3",children:(0,t.jsx)("div",{className:"bg-white rounded border border-gray-200 max-h-64 overflow-y-auto",children:(0,t.jsx)("div",{className:"p-3 space-y-2",children:e.text.split("\n\n").map((e,s)=>{if(""===e.trim())return null;if(e.startsWith("##")){let r=e.replace(/^#+\s/,"");return(0,t.jsx)("div",{className:"border-b border-gray-200 pb-1 mb-2",children:(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:r})},s)}let r=/(https?:\/\/[^\s\)]+)/g;if(r.test(e)){let l=e.split(r);return(0,t.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded p-2",children:(0,t.jsx)("div",{className:"text-xs text-gray-700 leading-relaxed whitespace-pre-wrap",children:l.map((e,s)=>r.test(e)?(0,t.jsx)("a",{href:e,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:text-blue-800 underline break-all",children:e},s):e)})},s)}return e.includes("Score:")?(0,t.jsx)("div",{className:"bg-green-50 border-l-4 border-green-400 p-2 rounded-r",children:(0,t.jsx)("p",{className:"text-xs text-green-800 font-medium whitespace-pre-wrap",children:e})},s):(0,t.jsx)("div",{className:"bg-gray-50 rounded p-2 border border-gray-200",children:(0,t.jsx)("div",{className:"text-xs text-gray-700 leading-relaxed whitespace-pre-wrap font-mono",children:e})},s)}).filter(Boolean)})})})]}),"image"===e.type&&e.url&&(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bg-gray-50 px-3 py-1 border-b border-gray-200",children:(0,t.jsx)("span",{className:"text-xs font-medium text-gray-700 uppercase tracking-wide",children:"Image Response"})}),(0,t.jsx)("div",{className:"p-3",children:(0,t.jsx)("div",{className:"bg-gray-50 rounded p-3 border border-gray-200",children:(0,t.jsx)("img",{src:e.url,alt:"Tool result",className:"max-w-full h-auto rounded shadow-sm"})})})]}),"embedded_resource"===e.type&&(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bg-gray-50 px-3 py-1 border-b border-gray-200",children:(0,t.jsx)("span",{className:"text-xs font-medium text-gray-700 uppercase tracking-wide",children:"Embedded Resource"})}),(0,t.jsx)("div",{className:"p-3",children:(0,t.jsxs)("div",{className:"flex items-center space-x-2 p-3 bg-blue-50 border border-blue-200 rounded",children:[(0,t.jsx)("div",{className:"flex-shrink-0",children:(0,t.jsx)("svg",{className:"h-5 w-5 text-blue-500",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})})}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsxs)("p",{className:"text-xs font-medium text-blue-900",children:["Resource Type: ",e.resource_type]}),e.url&&(0,t.jsxs)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center text-xs text-blue-600 hover:text-blue-800 hover:underline mt-1 transition-colors",children:["View Resource",(0,t.jsxs)("svg",{className:"ml-1 h-3 w-3",fill:"currentColor",viewBox:"0 0 20 20",children:[(0,t.jsx)("path",{d:"M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"}),(0,t.jsx)("path",{d:"M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"})]})]})]})]})})]})]},s)):(0,t.jsx)("div",{className:"bg-white rounded border border-gray-200",children:(0,t.jsx)("div",{className:"p-3 overflow-auto max-h-80 bg-gray-50",children:(0,t.jsx)("pre",{className:"text-xs font-mono whitespace-pre-wrap break-all text-gray-800",children:JSON.stringify(a,null,2)})})})})]})]}):(0,t.jsx)("div",{className:"flex flex-col justify-center items-center h-48 text-gray-500",children:(0,t.jsxs)("div",{className:"text-center max-w-sm",children:[(0,t.jsx)("div",{className:"mb-3",children:(0,t.jsx)("svg",{className:"mx-auto h-12 w-12 text-gray-300",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:1,d:"M13 10V3L4 14h7v7l9-11h-7z"})})}),(0,t.jsx)("h4",{className:"text-sm font-medium text-gray-900 mb-1",children:"Ready to Call Tool"}),(0,t.jsx)("p",{className:"text-xs text-gray-500 leading-relaxed",children:'Configure the input parameters and click "Call Tool" to see the results here.'})]})})})]})]})]})}var tN=e.i(983561),t_=e.i(438957);let tw=({serverId:e,accessToken:s,auth_type:r,userRole:l,userID:a,serverAlias:n,extraHeaders:i})=>{let[o,c]=(0,b.useState)(null),[u,x]=(0,b.useState)(null),[h,p]=(0,b.useState)(null),[g,f]=(0,b.useState)(""),[j,v]=(0,b.useState)({}),[N,w]=(0,b.useState)(!1),S=i&&i.length>0,C=()=>{if(!n||!S)return;let e={};return Object.entries(j).forEach(([t,s])=>{s&&s.trim()&&(e[`x-mcp-${n}-${t.toLowerCase()}`]=s)}),Object.keys(e).length>0?e:void 0},{data:T,isLoading:k,error:A,refetch:I}=(0,y.useQuery)({queryKey:["mcpTools",e,j],queryFn:()=>{if(!s)throw Error("Access Token required");return(0,_.listMCPTools)(s,e,C())},enabled:!!s,staleTime:3e4}),{mutate:P,isPending:O}=(0,tf.useMutation)({mutationFn:async t=>{if(!s)throw Error("Access Token required");try{return await (0,_.callMCPTool)(s,e,t.tool.name,t.arguments,{customHeaders:C()})}catch(e){throw e}},onSuccess:e=>{x(e.content),p(null)},onError:e=>{p(e),x(null)}}),M=T?.tools||[],F=M.filter(e=>{let t=g.toLowerCase();return e.name.toLowerCase().includes(t)||e.description&&e.description.toLowerCase().includes(t)||e.mcp_info.server_name&&e.mcp_info.server_name.toLowerCase().includes(t)});return(0,t.jsx)("div",{className:"w-full h-screen p-4 bg-white",children:(0,t.jsx)(eg.Card,{className:"w-full rounded-xl shadow-md overflow-hidden",children:(0,t.jsxs)("div",{className:"flex h-auto w-full gap-4",children:[(0,t.jsxs)("div",{className:"w-1/4 p-4 bg-gray-50 flex flex-col",children:[(0,t.jsx)(m.Title,{className:"text-xl font-semibold mb-6 mt-2",children:"MCP Tools"}),(0,t.jsxs)("div",{className:"flex flex-col flex-1",children:[S&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-2",children:[(0,t.jsxs)("div",{className:"flex items-center",children:[(0,t.jsx)(t_.KeyOutlined,{className:"text-blue-600 mr-2"}),(0,t.jsx)(d.Text,{className:"text-sm font-medium text-blue-800",children:"Additional Headers"})]}),(0,t.jsx)(eb.Button,{size:"small",type:"link",onClick:()=>w(!N),className:"text-blue-700 p-0 h-auto",children:N?"Hide":"Configure"})]}),!N&&0===Object.keys(j).length&&(0,t.jsx)(d.Text,{className:"text-xs text-blue-700",children:'This server requires additional headers. Click "Configure" to provide values.'}),N&&(0,t.jsxs)("div",{className:"mt-3 space-y-2",children:[i?.map(e=>(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"block text-xs font-medium text-gray-700 mb-1",children:e}),(0,t.jsx)(H.Input,{size:"small",placeholder:`Enter ${e}`,value:j[e]||"",onChange:t=>{v({...j,[e]:t.target.value})},prefix:(0,t.jsx)(t_.KeyOutlined,{className:"text-gray-400"}),className:"rounded"})]},e)),(0,t.jsx)(eb.Button,{size:"small",type:"primary",onClick:()=>{I(),w(!1)},disabled:Object.values(j).every(e=>!e||!e.trim()),className:"w-full mt-2",children:"Load Tools"})]}),!N&&Object.keys(j).length>0&&(0,t.jsx)("div",{className:"mt-2",children:(0,t.jsxs)(d.Text,{className:"text-xs text-green-700 flex items-center",children:[(0,t.jsx)("span",{className:"inline-block w-2 h-2 bg-green-500 rounded-full mr-2"}),Object.keys(j).length," header(s) configured"]})})]}),(0,t.jsxs)("div",{className:"flex flex-col flex-1 min-h-0",children:[(0,t.jsxs)(d.Text,{className:"font-medium block mb-3 text-gray-700 flex items-center",children:[(0,t.jsx)(ep.ToolOutlined,{className:"mr-2"})," Available Tools",M.length>0&&(0,t.jsx)("span",{className:"ml-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full",children:M.length})]}),M.length>0&&(0,t.jsx)("div",{className:"mb-3",children:(0,t.jsx)(H.Input,{placeholder:"Search tools...",prefix:(0,t.jsx)(ew.SearchOutlined,{className:"text-gray-400"}),value:g,onChange:e=>f(e.target.value),allowClear:!0,className:"rounded-lg",size:"middle"})}),k&&(0,t.jsxs)("div",{className:"flex flex-col items-center justify-center py-8 bg-white border border-gray-200 rounded-lg",children:[(0,t.jsxs)("div",{className:"relative mb-3",children:[(0,t.jsx)("div",{className:"animate-spin rounded-full h-6 w-6 border-2 border-gray-200"}),(0,t.jsx)("div",{className:"animate-spin rounded-full h-6 w-6 border-2 border-blue-600 border-t-transparent absolute top-0"})]}),(0,t.jsx)("p",{className:"text-xs font-medium text-gray-700",children:"Loading tools..."})]}),T?.error&&!k&&!M.length&&(0,t.jsx)("div",{className:"p-3 text-xs text-red-800 rounded-lg bg-red-50 border border-red-200",children:(0,t.jsxs)("p",{className:"font-medium",children:["Error: ",T.message]})}),!k&&!T?.error&&(!M||0===M.length)&&(0,t.jsxs)("div",{className:"p-4 text-center bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)("div",{className:"mx-auto w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center mb-2",children:(0,t.jsx)("svg",{className:"w-4 h-4 text-gray-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 8.172V5L8 4z"})})}),(0,t.jsx)("p",{className:"text-xs font-medium text-gray-700 mb-1",children:"No tools available"}),(0,t.jsx)("p",{className:"text-xs text-gray-500",children:"No tools found for this server"})]}),!k&&!T?.error&&M.length>0&&(0,t.jsx)(t.Fragment,{children:0===F.length?(0,t.jsxs)("div",{className:"p-4 text-center bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)(ew.SearchOutlined,{className:"text-2xl text-gray-400 mb-2"}),(0,t.jsx)("p",{className:"text-xs font-medium text-gray-700 mb-1",children:"No tools found"}),(0,t.jsxs)("p",{className:"text-xs text-gray-500",children:['No tools match "',g,'"']})]}):(0,t.jsx)("div",{className:"space-y-2 flex-1 overflow-y-auto min-h-0 mcp-tools-scrollable",style:{maxHeight:"400px",scrollbarWidth:"auto",scrollbarColor:"#cbd5e0 #f7fafc"},children:F.map(e=>(0,t.jsxs)("div",{className:`border rounded-lg p-3 cursor-pointer transition-all hover:shadow-sm ${o?.name===e.name?"border-blue-500 bg-blue-50 ring-1 ring-blue-200":"border-gray-200 bg-white hover:border-gray-300"}`,onClick:()=>{c(e),x(null),p(null)},children:[(0,t.jsxs)("div",{className:"flex items-start space-x-2",children:[e.mcp_info.logo_url&&(0,t.jsx)("img",{src:e.mcp_info.logo_url,alt:`${e.mcp_info.server_name} logo`,className:"w-4 h-4 object-contain flex-shrink-0 mt-0.5"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("h4",{className:"font-mono text-xs font-medium text-gray-900 truncate",children:e.name}),(0,t.jsx)("p",{className:"text-xs text-gray-500 truncate",children:e.mcp_info.server_name}),(0,t.jsx)("p",{className:"text-xs text-gray-600 mt-1 line-clamp-2 leading-relaxed",children:e.description})]})]}),o?.name===e.name&&(0,t.jsx)("div",{className:"mt-2 pt-2 border-t border-blue-200",children:(0,t.jsxs)("div",{className:"flex items-center text-xs font-medium text-blue-700",children:[(0,t.jsx)("svg",{className:"w-3 h-3 mr-1",fill:"currentColor",viewBox:"0 0 20 20",children:(0,t.jsx)("path",{fillRule:"evenodd",d:"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",clipRule:"evenodd"})}),"Selected"]})})]},e.name))})})]})]})]}),(0,t.jsxs)("div",{className:"w-3/4 flex flex-col bg-white",children:[(0,t.jsx)("div",{className:"p-4 border-b border-gray-200 flex justify-between items-center",children:(0,t.jsx)(m.Title,{className:"text-xl font-semibold mb-0",children:"Tool Testing Playground"})}),(0,t.jsx)("div",{className:"flex-1 overflow-auto p-4",children:o?(0,t.jsx)("div",{className:"h-full",children:(0,t.jsx)(tv,{tool:o,onSubmit:e=>{P({tool:o,arguments:e})},result:u,error:h,isLoading:O,onClose:()=>c(null)})}):(0,t.jsxs)("div",{className:"h-full flex flex-col items-center justify-center text-gray-400",children:[(0,t.jsx)(tN.RobotOutlined,{style:{fontSize:"48px",marginBottom:"16px"}}),(0,t.jsx)(d.Text,{className:"text-lg font-medium text-gray-600 mb-2",children:"Select a Tool to Test"}),(0,t.jsx)(d.Text,{className:"text-center text-gray-500 max-w-md",children:"Choose a tool from the left sidebar to start testing its functionality with custom inputs."})]})})]})]})})})},tS=[eo.AUTH_TYPE.API_KEY,eo.AUTH_TYPE.BEARER_TOKEN,eo.AUTH_TYPE.TOKEN,eo.AUTH_TYPE.BASIC],tC=[...tS,eo.AUTH_TYPE.OAUTH2,eo.AUTH_TYPE.AWS_SIGV4],tT="litellm-mcp-oauth-edit-state",tk=({mcpServer:e,accessToken:s,onCancel:r,onSuccess:d,availableAccessGroups:m})=>{let[u]=D.Form.useForm(),[x,h]=(0,b.useState)({}),[f,j]=(0,b.useState)([]),[y,v]=(0,b.useState)(!1),[N,w]=(0,b.useState)(null),[S,T]=(0,b.useState)(""),[k,A]=(0,b.useState)(!1),[I,P]=(0,b.useState)([]),[O,M]=(0,b.useState)({}),[F,E]=(0,b.useState)({}),[L,R]=(0,b.useState)(null),[U,z]=(0,b.useState)(e.mcp_info?.logo_url||void 0),B=D.Form.useWatch("auth_type",u),q=D.Form.useWatch("transport",u),V="stdio"===q,$=q===eo.TRANSPORT.OPENAPI,K=!!B&&tS.includes(B),W=B===eo.AUTH_TYPE.OAUTH2,J=B===eo.AUTH_TYPE.AWS_SIGV4,Y=D.Form.useWatch("oauth_flow_type",u),G=W&&Y===eo.OAUTH_FLOW.M2M,[Q,Z]=(0,b.useState)(null),X=D.Form.useWatch("url",u),ee=D.Form.useWatch("spec_path",u),et=D.Form.useWatch("server_name",u),es=D.Form.useWatch("auth_type",u),er=D.Form.useWatch("static_headers",u),el=D.Form.useWatch("credentials",u),ea=D.Form.useWatch("authorization_url",u),ei=D.Form.useWatch("token_url",u),ed=D.Form.useWatch("registration_url",u),{startOAuthFlow:em,status:eu,error:ex,tokenResponse:eh}=eQ({accessToken:s,getCredentials:()=>u.getFieldValue("credentials"),getTemporaryPayload:()=>{let t=u.getFieldsValue(!0),s=t.url||e.url,r=t.transport||e.transport;if(!s||!r)return null;let l=Array.isArray(t.static_headers)?t.static_headers.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value??""),e},{}):{};return{server_id:e.server_id,server_name:t.server_name||e.server_name||e.alias,alias:t.alias||e.alias,description:t.description||e.description,url:s,transport:r,auth_type:eo.AUTH_TYPE.OAUTH2,credentials:t.credentials,mcp_access_groups:t.mcp_access_groups||e.mcp_access_groups,static_headers:l,command:t.command,args:t.args,env:t.env}},onTokenReceived:e=>{if(Z(e?.access_token??null),e?.access_token){let t={access_token:e.access_token,...e.refresh_token&&{refresh_token:e.refresh_token},...e.expires_in&&{expires_in:e.expires_in},...e.scope&&{scope:e.scope}};u.setFieldsValue({credentials:t}),C.default.success("OAuth authorization successful! Please click 'Update MCP Server' to save the credentials.")}},onBeforeRedirect:()=>{try{let t=u.getFieldsValue(!0);(0,eG.setSecureItem)(tT,JSON.stringify({serverId:e.server_id,formValues:t,costConfig:x,allowedTools:I,searchValue:S,aliasManuallyEdited:k}))}catch(e){console.warn("Failed to persist MCP edit state",e)}}}),ep=b.default.useMemo(()=>e.static_headers?Object.entries(e.static_headers).map(([e,t])=>({header:e,value:null!=t?String(t):""})):[],[e.static_headers]),eg=b.default.useMemo(()=>{let t=e.env??void 0;if(!t||0===Object.keys(t).length)return"";try{return JSON.stringify(t,null,2)}catch{return""}},[e.env]),ej=b.default.useMemo(()=>e.spec_path&&"stdio"!==e.transport?eo.TRANSPORT.OPENAPI:e.transport,[e]),ey=b.default.useMemo(()=>({...e,transport:ej,static_headers:ep,extra_headers:e.extra_headers||[],oauth_flow_type:e.token_url?eo.OAUTH_FLOW.M2M:eo.OAUTH_FLOW.INTERACTIVE,token_validation_json:e.token_validation?JSON.stringify(e.token_validation,null,2):void 0}),[e,ej,ep,eg]);(0,b.useEffect)(()=>{e.mcp_info?.mcp_server_cost_info&&h(e.mcp_info.mcp_server_cost_info)},[e]),(0,b.useEffect)(()=>{e.allowed_tools&&P(e.allowed_tools),M(e.tool_name_to_display_name??{}),E(e.tool_name_to_description??{})},[e]),(0,b.useEffect)(()=>{let t=(0,eG.getSecureItem)(tT);if(t)try{let s=JSON.parse(t);if(!s||s.serverId!==e.server_id)return;s.formValues&&R({...e,...s.formValues}),s.costConfig&&h(s.costConfig),s.allowedTools&&P(s.allowedTools),s.searchValue&&T(s.searchValue),"boolean"==typeof s.aliasManuallyEdited&&A(s.aliasManuallyEdited)}catch(e){console.error("Failed to restore MCP edit state",e)}finally{window.sessionStorage.removeItem(tT)}},[u,e]),(0,b.useEffect)(()=>{if(!L)return;let t=L.transport||e.transport;t&&t!==u.getFieldValue("transport")?u.setFieldsValue({transport:t}):(u.setFieldsValue(L),R(null))},[L,u,e.transport]),(0,b.useEffect)(()=>{if(e.mcp_access_groups){let t=e.mcp_access_groups.map(e=>"string"==typeof e?e:e.name||String(e));u.setFieldValue("mcp_access_groups",t)}},[e]),(0,b.useEffect)(()=>{e.server_id&&""!==e.server_id.trim()&&ev()},[e,s]);let ev=async()=>{if(s&&e.server_id){v(!0),w(null);try{let t=await (0,_.listMCPTools)(s,e.server_id);t.tools&&!t.error?j(t.tools):(console.error("Failed to fetch tools:",t.message),j([]),w(t.message||"Failed to load tools"))}catch(e){console.error("Tools fetch error:",e),j([]),w(e instanceof Error?e.message:"Failed to load tools")}finally{v(!1)}}},eN=async t=>{if(s)try{let{static_headers:r,credentials:l,stdio_config:a,env_json:n,command:i,args:o,allow_all_keys:c,available_on_public_internet:m,delegate_auth_to_upstream:u,token_validation_json:h,...p}=t,g=(p.mcp_access_groups||[]).map(e=>"string"==typeof e?e:e.name||String(e)),f=Array.isArray(r)?r.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value??""),e},{}):{},b=l&&"object"==typeof l?Object.entries(l).reduce((e,[t,s])=>{if(null==s||""===s)return e;if("scopes"===t){if(Array.isArray(s)){let r=s.filter(e=>null!=e&&""!==e);r.length>0&&(e[t]=r)}}else e[t]=s;return e},{}):void 0,j={};if("stdio"===p.transport)if(a)try{let e=JSON.parse(a),t=e;if(e?.mcpServers&&"object"==typeof e.mcpServers){let s=Object.keys(e.mcpServers);s.length>0&&(t=e.mcpServers[s[0]])}let s=Array.isArray(t?.args)?t.args.map(e=>String(e)).filter(e=>""!==e.trim()):[],r=t?.env&&"object"==typeof t.env&&!Array.isArray(t.env)?Object.entries(t.env).reduce((e,[t,s])=>(null==t||""===String(t).trim()||(e[String(t)]=null==s?"":String(s)),e),{}):{};if(!(j={command:t?.command?String(t.command):void 0,args:s,env:r}).command)return void C.default.fromBackend("Stdio configuration must include a command")}catch{C.default.fromBackend("Invalid JSON in stdio configuration");return}else{let e={};if(n)try{let t=JSON.parse(n);t&&"object"==typeof t&&!Array.isArray(t)&&(e=Object.entries(t).reduce((e,[t,s])=>(null==t||""===String(t).trim()||(e[String(t)]=null==s?"":String(s)),e),{}))}catch{C.default.fromBackend("Invalid JSON in stdio env configuration");return}let t=Array.isArray(o)?o.map(e=>String(e)).filter(e=>""!==e.trim()):[],s=i?String(i).trim():"";if(!s)return void C.default.fromBackend("Stdio transport requires a command");j={command:s,args:t,env:e}}p.transport===eo.TRANSPORT.OPENAPI&&(p.transport="http");let y=null;if(h&&""!==h.trim())try{y=JSON.parse(h)}catch{C.default.fromBackend("Invalid JSON in Token Validation Rules");return}let v=p.server_name||p.url||e.server_name||e.url||p.alias||e.alias||"unknown",N={...p,...j,stdio_config:void 0,env_json:void 0,server_id:e.server_id,mcp_info:{server_name:v,description:p.description,logo_url:U||void 0,mcp_server_cost_info:Object.keys(x).length>0?x:null},mcp_access_groups:g,alias:p.alias,extra_headers:p.extra_headers||[],allowed_tools:I.length>0?I:null,tool_name_to_display_name:Object.keys(O).length>0?O:null,tool_name_to_description:Object.keys(F).length>0?F:null,disallowed_tools:p.disallowed_tools||[],static_headers:f,allow_all_keys:!!(c??e.allow_all_keys),available_on_public_internet:!!(m??e.available_on_public_internet),delegate_auth_to_upstream:p.auth_type===eo.AUTH_TYPE.OAUTH2&&!!(u??e.delegate_auth_to_upstream),...null!==y||e.token_validation?{token_validation:y}:{}};p.auth_type&&tC.includes(p.auth_type)&&b&&Object.keys(b).length>0&&(N.credentials=b);let w=await (0,_.updateMCPServer)(s,N);C.default.success("MCP Server updated successfully"),d(w)}catch(e){C.default.fromBackend("Failed to update MCP Server"+(e?.message?`: ${e.message}`:""))}};return(0,t.jsxs)(n.TabGroup,{children:[(0,t.jsxs)(i.TabList,{className:"grid w-full grid-cols-2",children:[(0,t.jsx)(a.Tab,{children:"Server Configuration"}),(0,t.jsx)(a.Tab,{children:"Cost Configuration"})]}),(0,t.jsxs)(c.TabPanels,{className:"mt-6",children:[(0,t.jsx)(o.TabPanel,{children:(0,t.jsxs)(D.Form,{form:u,onFinish:eN,initialValues:ey,layout:"vertical",children:[(0,t.jsx)(D.Form.Item,{label:"MCP Server Name",name:"server_name",rules:[{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(H.Input,{className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:"Alias",name:"alias",rules:[{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(H.Input,{onChange:()=>A(!0),className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:"Description",name:"description",children:(0,t.jsx)(H.Input,{className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(e$,{value:U,onChange:z}),(0,t.jsx)(D.Form.Item,{label:"Transport Type",name:"transport",rules:[{required:!0}],children:(0,t.jsxs)(p.Select,{onChange:e=>{"stdio"===e?u.setFieldsValue({url:void 0,spec_path:void 0,auth_type:void 0,credentials:void 0,authorization_url:void 0,token_url:void 0,registration_url:void 0}):e===eo.TRANSPORT.OPENAPI?u.setFieldsValue({url:void 0,command:void 0,args:void 0,env_json:void 0,stdio_config:void 0}):u.setFieldsValue({spec_path:void 0,command:void 0,args:void 0,env_json:void 0,stdio_config:void 0})},children:[(0,t.jsx)(p.Select.Option,{value:"http",children:"Streamable HTTP (Recommended)"}),(0,t.jsx)(p.Select.Option,{value:"sse",children:"Server-Sent Events (SSE)"}),(0,t.jsx)(p.Select.Option,{value:"stdio",children:"Standard Input/Output (stdio)"}),(0,t.jsx)(p.Select.Option,{value:eo.TRANSPORT.OPENAPI,children:"OpenAPI Spec"})]})}),!V&&!$&&(0,t.jsx)(D.Form.Item,{label:"MCP Server URL",name:"url",rules:[{required:!0,message:"Please enter a server URL"},{validator:(e,t)=>eK(t)}],children:(0,t.jsx)(H.Input,{placeholder:"https://your-mcp-server.com",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),$&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OpenAPI Spec URL",(0,t.jsx)(g.Tooltip,{title:"URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"spec_path",rules:[{required:!0,message:"Please enter an OpenAPI spec URL"}],children:(0,t.jsx)(H.Input,{placeholder:"https://petstore3.swagger.io/api/v3/openapi.json",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),!V&&(0,t.jsx)(D.Form.Item,{label:"Authentication",name:"auth_type",rules:[{required:!0}],children:(0,t.jsxs)(p.Select,{children:[(0,t.jsx)(p.Select.Option,{value:"none",children:"None"}),(0,t.jsx)(p.Select.Option,{value:"api_key",children:"API Key"}),(0,t.jsx)(p.Select.Option,{value:"bearer_token",children:"Bearer Token"}),(0,t.jsx)(p.Select.Option,{value:"token",children:"Token"}),(0,t.jsx)(p.Select.Option,{value:"basic",children:"Basic Auth"}),(0,t.jsx)(p.Select.Option,{value:"oauth2",children:"OAuth"}),(0,t.jsx)(p.Select.Option,{value:"aws_sigv4",children:"AWS SigV4 (Bedrock AgentCore MCPs)"})]})}),V&&(0,t.jsxs)("div",{className:"rounded-lg border border-gray-200 p-4 space-y-4",children:[(0,t.jsx)("p",{className:"text-sm text-gray-600",children:"Configure the stdio transport used to launch the MCP server process. You can either fill in the fields below or paste a JSON configuration."}),(0,t.jsx)(D.Form.Item,{label:"Command",name:"command",rules:[{required:!0,message:"Please enter a command for stdio transport"}],children:(0,t.jsx)(H.Input,{placeholder:"e.g., npx",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:"Args",name:"args",children:(0,t.jsx)(p.Select,{mode:"tags",size:"large",tokenSeparators:[","],placeholder:"Add args (press enter or comma)",className:"rounded-lg"})}),(0,t.jsx)(D.Form.Item,{label:"Environment (JSON object)",name:"env_json",rules:[{validator:(e,t)=>{if(!t)return Promise.resolve();try{let e=JSON.parse(t);if(e&&"object"==typeof e&&!Array.isArray(e))return Promise.resolve();return Promise.reject(Error("Env must be a JSON object"))}catch{return Promise.reject(Error("Please enter valid JSON"))}}}],children:(0,t.jsx)(H.Input.TextArea,{rows:6,className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 font-mono text-sm",placeholder:`{ + "KEY": "value" +}`})}),(0,t.jsx)(eO,{isVisible:!0,required:!1})]}),!V&&K&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Authentication Value",(0,t.jsx)(g.Tooltip,{title:"Token, password, or header value to send with each request for the selected auth type.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","auth_value"],rules:[{validator:(e,t)=>t&&"string"==typeof t&&""===t.trim()?Promise.reject(Error("Authentication value cannot be empty")):Promise.resolve()}],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter token or secret (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),!V&&W&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OAuth Client ID (optional)",(0,t.jsx)(g.Tooltip,{title:"Provide only if your MCP server cannot handle dynamic client registration.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","client_id"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter OAuth client ID (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OAuth Client Secret (optional)",(0,t.jsx)(g.Tooltip,{title:"Provide only if your MCP server cannot handle dynamic client registration.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","client_secret"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter OAuth client secret (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OAuth Scopes (optional)",(0,t.jsx)(g.Tooltip,{title:"Add scopes to override the default scope list used for this MCP server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","scopes"],children:(0,t.jsx)(p.Select,{mode:"tags",tokenSeparators:[","],placeholder:"Add scopes",className:"rounded-lg",size:"large"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Authorization URL Override (optional)",(0,t.jsx)(g.Tooltip,{title:"Optional override for the authorization endpoint.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"authorization_url",children:(0,t.jsx)(H.Input,{placeholder:"https://example.com/oauth/authorize",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Token URL Override (optional)",(0,t.jsx)(g.Tooltip,{title:"Optional override for the token endpoint.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"token_url",children:(0,t.jsx)(H.Input,{placeholder:"https://example.com/oauth/token",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Registration URL Override (optional)",(0,t.jsx)(g.Tooltip,{title:"Optional override for the dynamic client registration endpoint.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"registration_url",children:(0,t.jsx)(H.Input,{placeholder:"https://example.com/oauth/register",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),!G&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Token Validation Rules (optional)",(0,t.jsx)(g.Tooltip,{title:'JSON object of key-value rules checked against the OAuth token response before storing. Supports dot-notation for nested fields (e.g. {"organization": "my-org", "team.id": "123"}). Tokens that fail validation are rejected with HTTP 403.',children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"token_validation_json",rules:[{validator:(e,t)=>{if(!t||""===t.trim())return Promise.resolve();try{return JSON.parse(t),Promise.resolve()}catch{return Promise.reject(Error("Must be valid JSON"))}}}],children:(0,t.jsx)(H.Input.TextArea,{placeholder:'{\n "organization": "my-org",\n "team.id": "123"\n}',rows:4,className:"font-mono text-sm rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Token Storage TTL (seconds, optional)",(0,t.jsx)(g.Tooltip,{title:"How long to cache each user's OAuth access token in Redis before evicting it (regardless of the token's own expires_in). Leave blank to derive the TTL from the token's expires_in, or fall back to the 12-hour default.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"token_storage_ttl_seconds",children:(0,t.jsx)(ec.InputNumber,{min:1,placeholder:"e.g. 3600",style:{width:"100%"},className:"rounded-lg"})})]}),(0,t.jsxs)("div",{className:"rounded-lg border border-dashed border-gray-300 p-4 space-y-2",children:[(0,t.jsx)("p",{className:"text-sm text-gray-600",children:"Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value."}),(0,t.jsx)(l.Button,{variant:"secondary",onClick:em,disabled:"authorizing"===eu||"exchanging"===eu,children:"authorizing"===eu?"Waiting for authorization...":"exchanging"===eu?"Exchanging authorization code...":"Authorize & Fetch Token"}),ex&&(0,t.jsx)("p",{className:"text-sm text-red-500",children:ex}),"success"===eu&&eh?.access_token&&(0,t.jsxs)("p",{className:"text-sm text-green-600",children:["Token fetched. Expires in ",eh.expires_in??"?"," seconds."]})]})]}),!V&&J&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-2",children:["For MCP servers hosted on AWS Bedrock AgentCore."," ",(0,t.jsx)("a",{href:"https://docs.litellm.ai/docs/mcp_aws_sigv4",target:"_blank",rel:"noopener noreferrer",className:"text-blue-500 hover:text-blue-700",children:"View docs →"})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Region",(0,t.jsx)(g.Tooltip,{title:"AWS region for SigV4 signing (e.g., us-east-1)",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_region_name"],rules:[],children:(0,t.jsx)(H.Input,{placeholder:"us-east-1 (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Service Name",(0,t.jsx)(g.Tooltip,{title:"AWS service name for SigV4 signing. Defaults to 'bedrock-agentcore'.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_service_name"],children:(0,t.jsx)(H.Input,{placeholder:"bedrock-agentcore (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Access Key ID",(0,t.jsx)(g.Tooltip,{title:"Optional. If not provided, falls back to the boto3 credential chain (IAM role, env vars, etc.).",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_access_key_id"],rules:[],children:(0,t.jsx)(H.Input.Password,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Secret Access Key",(0,t.jsx)(g.Tooltip,{title:"Optional. Required if AWS Access Key ID is provided.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_secret_access_key"],rules:[],children:(0,t.jsx)(H.Input.Password,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Token",(0,t.jsx)(g.Tooltip,{title:"Optional. Only needed for temporary STS credentials.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_token"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Role ARN",(0,t.jsx)(g.Tooltip,{title:"Optional. IAM role ARN to assume via STS before signing. If set, LiteLLM calls sts:AssumeRole to get temporary credentials.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_role_name"],children:(0,t.jsx)(H.Input,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Name",(0,t.jsx)(g.Tooltip,{title:"Optional. Session name for the AssumeRole call — appears in CloudTrail logs. Auto-generated if omitted.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_name"],children:(0,t.jsx)(H.Input,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})})]}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(eR,{availableAccessGroups:m,mcpServer:e,searchValue:S,setSearchValue:T,getAccessGroupOptions:()=>{let e=m.map(e=>({value:e,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-green-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:e})]})}));return S&&!m.some(e=>e.toLowerCase().includes(S.toLowerCase()))&&e.push({value:S,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-blue-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:S}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-1",children:"create new group"})]})}),e}})}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(eP,{accessToken:s,oauthAccessToken:Q,formValues:{server_id:e.server_id,server_name:et??e.server_name,url:X??e.url,spec_path:ee??e.spec_path,transport:q??e.transport,auth_type:es??e.auth_type,mcp_info:e.mcp_info,oauth_flow_type:ei??e.token_url?eo.OAUTH_FLOW.M2M:eo.OAUTH_FLOW.INTERACTIVE,static_headers:er??e.static_headers,credentials:el,authorization_url:ea??e.authorization_url,token_url:ei??e.token_url,registration_url:ed??e.registration_url},allowedTools:I,existingAllowedTools:e.allowed_tools||null,onAllowedToolsChange:P,toolNameToDisplayName:O,toolNameToDescription:F,onToolNameToDisplayNameChange:M,onToolNameToDescriptionChange:E})}),(0,t.jsxs)("div",{className:"flex justify-end gap-2",children:[(0,t.jsx)(eb.Button,{onClick:r,children:"Cancel"}),(0,t.jsx)(l.Button,{type:"submit",children:"Save Changes"})]})]})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsxs)("div",{className:"space-y-6",children:[(0,t.jsx)(ef,{value:x,onChange:h,tools:f,disabled:y}),(0,t.jsxs)("div",{className:"flex justify-end gap-2",children:[(0,t.jsx)(eb.Button,{onClick:r,children:"Cancel"}),(0,t.jsx)(l.Button,{onClick:()=>u.submit(),children:"Save Changes"})]})]})})]})]})},tA=({costConfig:e})=>{let s=e?.default_cost_per_query!==void 0&&e?.default_cost_per_query!==null,r=e?.tool_name_to_cost_per_query&&Object.keys(e.tool_name_to_cost_per_query).length>0;return s||r?(0,t.jsx)("div",{className:"mt-6 pt-6 border-t border-gray-200",children:(0,t.jsxs)("div",{className:"space-y-4",children:[s&&e?.default_cost_per_query!==void 0&&e?.default_cost_per_query!==null&&(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"font-medium",children:"Default Cost per Query"}),(0,t.jsxs)("div",{className:"text-green-600 font-mono",children:["$",e.default_cost_per_query.toFixed(4)]})]}),r&&e?.tool_name_to_cost_per_query&&(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"font-medium",children:"Tool-Specific Costs"}),(0,t.jsx)("div",{className:"mt-2 space-y-2",children:Object.entries(e.tool_name_to_cost_per_query).map(([e,s])=>null!=s&&(0,t.jsxs)("div",{className:"flex justify-between items-center p-3 bg-gray-50 rounded-lg",children:[(0,t.jsx)(d.Text,{className:"font-medium",children:e}),(0,t.jsxs)(d.Text,{className:"text-green-600 font-mono",children:["$",s.toFixed(4)," per query"]})]},e))})]}),(0,t.jsxs)("div",{className:"mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg",children:[(0,t.jsx)(d.Text,{className:"text-blue-800 font-medium",children:"Cost Summary:"}),(0,t.jsxs)("div",{className:"mt-2 space-y-1",children:[s&&e?.default_cost_per_query!==void 0&&e?.default_cost_per_query!==null&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• Default cost: $",e.default_cost_per_query.toFixed(4)," per query"]}),r&&e?.tool_name_to_cost_per_query&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• ",Object.keys(e.tool_name_to_cost_per_query).length," tool(s) with custom pricing"]})]})]})]})}):(0,t.jsx)("div",{className:"mt-6 pt-6 border-t border-gray-200",children:(0,t.jsx)("div",{className:"space-y-4",children:(0,t.jsx)("div",{className:"p-4 bg-gray-50 border border-gray-200 rounded-lg",children:(0,t.jsx)(d.Text,{className:"text-gray-600",children:"No cost configuration set for this server. Tool calls will be charged at $0.00 per tool call."})})})})},tI=({mcpServer:e,onBack:s,isEditing:r,isProxyAdmin:u,accessToken:x,userRole:h,userID:p,availableAccessGroups:g})=>{let[f,j]=(0,b.useState)(r),[y,v]=(0,b.useState)(!1),[N,_]=(0,b.useState)({}),[w,S]=(0,b.useState)(0),C=e.url??"",{maskedUrl:T,hasToken:A}=C?eH(C):{maskedUrl:"—",hasToken:!1},I=(e,t)=>e?A?t?e:T:e:"—",P=async(e,t)=>{await (0,tr.copyToClipboard)(e)&&(_(e=>({...e,[t]:!0})),setTimeout(()=>{_(e=>({...e,[t]:!1}))},2e3))},O=e=>{let s=e.toUpperCase();return(0,t.jsx)("span",{className:"inline-flex items-center text-sm font-medium px-2.5 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:s})},M=e=>(0,t.jsx)("span",{className:"inline-flex items-center text-sm font-medium px-2.5 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:e});return(0,t.jsxs)("div",{className:"p-4 max-w-full",children:[(0,t.jsxs)("div",{className:"mb-6",children:[(0,t.jsx)(l.Button,{icon:tx.ArrowLeftIcon,variant:"light",className:"mb-4",onClick:s,children:"Back to All Servers"}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(m.Title,{className:"text-2xl",children:e.server_name||e.alias||"Unnamed Server"}),(0,t.jsx)(eb.Button,{type:"text",size:"small",icon:N["mcp-server_name"]?(0,t.jsx)(k.CheckIcon,{size:12}):(0,t.jsx)(e6.CopyIcon,{size:12}),onClick:()=>P(e.server_name||e.alias,"mcp-server_name"),className:`transition-all duration-200 ${N["mcp-server_name"]?"text-green-600 bg-green-50 border-green-200":"text-gray-400 hover:text-gray-600 hover:bg-gray-100"}`}),e.alias&&e.server_name&&e.alias!==e.server_name&&(0,t.jsx)("span",{className:"ml-2 inline-flex items-center text-xs font-medium px-2 py-0.5 rounded bg-gray-100 text-gray-600 border border-gray-200 font-mono",children:e.alias})]}),(0,t.jsxs)("div",{className:"flex items-center gap-1.5 mt-1",children:[(0,t.jsx)(d.Text,{className:"text-gray-400 font-mono text-xs",children:e.server_id}),(0,t.jsx)(eb.Button,{type:"text",size:"small",icon:N["mcp-server-id"]?(0,t.jsx)(k.CheckIcon,{size:10}):(0,t.jsx)(e6.CopyIcon,{size:10}),onClick:()=>P(e.server_id,"mcp-server-id"),className:`transition-all duration-200 ${N["mcp-server-id"]?"text-green-600 bg-green-50 border-green-200":"text-gray-300 hover:text-gray-500 hover:bg-gray-50"}`})]}),e.description&&(0,t.jsx)(d.Text,{className:"text-gray-500 mt-2",children:e.description})]}),(0,t.jsxs)(n.TabGroup,{index:w,onIndexChange:S,children:[(0,t.jsx)(i.TabList,{className:"mb-4",children:[(0,t.jsx)(a.Tab,{children:"Overview"},"overview"),(0,t.jsx)(a.Tab,{children:"MCP Tools"},"tools"),...u?[(0,t.jsx)(a.Tab,{children:"Settings"},"settings")]:[]]}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsxs)(o.TabPanel,{children:[(0,t.jsxs)(tg.Grid,{numItems:1,numItemsSm:2,numItemsLg:3,className:"gap-4",children:[(0,t.jsxs)(eg.Card,{className:"p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Transport"}),(0,t.jsx)("div",{className:"mt-3",children:O((0,eo.handleTransport)(e.transport??void 0,e.spec_path??void 0))})]}),(0,t.jsxs)(eg.Card,{className:"p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Authentication"}),(0,t.jsx)("div",{className:"mt-3",children:M((0,eo.handleAuth)(e.auth_type??void 0))})]}),(0,t.jsxs)(eg.Card,{className:"p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Host URL"}),(0,t.jsxs)("div",{className:"mt-3 flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"break-all overflow-wrap-anywhere font-mono text-sm",children:I(e.url,y)}),A&&u&&(0,t.jsx)("button",{onClick:()=>v(!y),className:"p-1 hover:bg-gray-100 rounded flex-shrink-0",children:(0,t.jsx)(tc.Icon,{icon:y?tp:th.EyeIcon,size:"sm",className:"text-gray-500"})})]})]})]}),(0,t.jsxs)(eg.Card,{className:"mt-4 p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Cost Configuration"}),(0,t.jsx)("div",{className:"mt-3",children:(0,t.jsx)(tA,{costConfig:e.mcp_info?.mcp_server_cost_info})})]})]}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tw,{serverId:e.server_id,accessToken:x,auth_type:e.auth_type,userRole:h,userID:p,serverAlias:e.alias,extraHeaders:e.extra_headers})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsxs)(eg.Card,{children:[(0,t.jsxs)("div",{className:"flex justify-between items-center mb-4",children:[(0,t.jsx)(m.Title,{children:"MCP Server Settings"}),f?null:(0,t.jsx)(l.Button,{variant:"light",onClick:()=>j(!0),children:"Edit Settings"})]}),f?(0,t.jsx)(tk,{mcpServer:e,accessToken:x,onCancel:()=>j(!1),onSuccess:e=>{j(!1),s()},availableAccessGroups:g}):(0,t.jsxs)("div",{className:"divide-y divide-gray-100",children:[(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Server Name"}),(0,t.jsx)("div",{className:"col-span-2 text-sm text-gray-900",children:e.server_name||(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Alias"}),(0,t.jsx)("div",{className:"col-span-2 text-sm font-mono text-gray-900",children:e.alias||(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Description"}),(0,t.jsx)("div",{className:"col-span-2 text-sm text-gray-900",children:e.description||(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"URL"}),(0,t.jsxs)("div",{className:"col-span-2 text-sm font-mono text-gray-900 break-all flex items-center gap-2",children:[I(e.url,y),A&&(0,t.jsx)("button",{onClick:()=>v(!y),className:"p-1 hover:bg-gray-100 rounded flex-shrink-0",children:(0,t.jsx)(tc.Icon,{icon:y?tp:th.EyeIcon,size:"sm",className:"text-gray-500"})})]})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Transport"}),(0,t.jsx)("div",{className:"col-span-2",children:O((0,eo.handleTransport)(e.transport,e.spec_path))})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Authentication"}),(0,t.jsx)("div",{className:"col-span-2",children:M((0,eo.handleAuth)(e.auth_type))})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Extra Headers"}),(0,t.jsx)("div",{className:"col-span-2 text-sm text-gray-900",children:e.extra_headers&&e.extra_headers.length>0?e.extra_headers.join(", "):(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Allow All Keys"}),(0,t.jsx)("div",{className:"col-span-2",children:e.allow_all_keys?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Enabled"]}):(0,t.jsx)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-gray-50 text-gray-600 rounded-full border border-gray-200 text-xs font-medium",children:"Disabled"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Network Access"}),(0,t.jsx)("div",{className:"col-span-2",children:e.available_on_public_internet?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Public"]}):(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-orange-500"}),"Internal only"]})})]}),"oauth2"===(0,eo.handleAuth)(e.auth_type)&&(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Delegate Auth to Upstream"}),(0,t.jsx)("div",{className:"col-span-2",children:e.delegate_auth_to_upstream?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Enabled (PKCE passthrough)"]}):(0,t.jsx)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-gray-50 text-gray-600 rounded-full border border-gray-200 text-xs font-medium",children:"Disabled"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Access Groups"}),(0,t.jsx)("div",{className:"col-span-2",children:e.mcp_access_groups&&e.mcp_access_groups.length>0?(0,t.jsx)("div",{className:"flex flex-wrap gap-1.5",children:e.mcp_access_groups.map((e,s)=>(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200",children:"string"==typeof e?e:e?.name??""},s))}):(0,t.jsx)("span",{className:"text-sm text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Allowed Tools"}),(0,t.jsx)("div",{className:"col-span-2",children:e.allowed_tools&&e.allowed_tools.length>0?(0,t.jsx)("div",{className:"flex flex-wrap gap-1.5",children:e.allowed_tools.map((e,s)=>(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-mono font-medium px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200",children:e},s))}):(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded bg-green-50 text-green-700 border border-green-200",children:"All tools enabled"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Cost"}),(0,t.jsx)("div",{className:"col-span-2",children:(0,t.jsx)(tA,{costConfig:e.mcp_info?.mcp_server_cost_info})})]})]})]})})]})]})]})},tP=(0,N.createQueryKeys)("mcpSemanticFilterSettings"),tO=(0,N.createQueryKeys)("mcpSemanticFilterSettings");var tM=e.i(178654),tF=e.i(621192),tE=e.i(981339),tL=e.i(850627),tR=e.i(987432),tU=e.i(689020),tz=e.i(245094),tB=e.i(788191),tq=e.i(653496),tV=e.i(992619);function t$({accessToken:e,testQuery:s,setTestQuery:r,testModel:l,setTestModel:a,isTesting:n,onTest:i,filterEnabled:o,testResult:c,curlCommand:d}){return(0,t.jsx)(e4.Card,{title:"Test Configuration",style:{marginBottom:16},children:(0,t.jsx)(tq.Tabs,{defaultActiveKey:"test",items:[{key:"test",label:"Test",children:(0,t.jsxs)(eM.Space,{direction:"vertical",style:{width:"100%"},size:"large",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)(f.Typography.Text,{strong:!0,style:{display:"block",marginBottom:8},children:[(0,t.jsx)(tB.PlayCircleOutlined,{})," Test Query"]}),(0,t.jsx)(H.Input.TextArea,{placeholder:"Enter a test query to see which tools would be selected...",value:s,onChange:e=>r(e.target.value),rows:4,disabled:n})]}),(0,t.jsx)("div",{children:(0,t.jsx)(tV.default,{accessToken:e||"",value:l,onChange:a,disabled:n,showLabel:!0,labelText:"Select Model"})}),(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tB.PlayCircleOutlined,{}),onClick:i,loading:n,disabled:!s||!l||!o,block:!0,children:"Test Filter"}),!o&&(0,t.jsx)(ej.Alert,{type:"warning",message:"Semantic filtering is disabled",description:"Enable semantic filtering and save settings to test the filter.",showIcon:!0}),c&&(0,t.jsxs)("div",{children:[(0,t.jsx)(f.Typography.Title,{level:5,children:"Results"}),(0,t.jsx)(ej.Alert,{type:"success",message:`${c.selectedTools} tools selected`,description:`Filtered from ${c.totalTools} available tools`,showIcon:!0,style:{marginBottom:16}}),(0,t.jsxs)("div",{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,style:{display:"block",marginBottom:8},children:"Selected Tools:"}),(0,t.jsx)("ul",{style:{paddingLeft:20,margin:0},children:c.tools.map((e,s)=>(0,t.jsx)("li",{style:{marginBottom:4},children:(0,t.jsx)(f.Typography.Text,{children:e})},s))})]})]})]})},{key:"api",label:"API Usage",children:(0,t.jsxs)("div",{children:[(0,t.jsxs)(eM.Space,{style:{marginBottom:8},children:[(0,t.jsx)(tz.CodeOutlined,{}),(0,t.jsx)(f.Typography.Text,{strong:!0,children:"API Usage"})]}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block",marginBottom:8},children:"Use this curl command to test the semantic filter with your current configuration."}),(0,t.jsx)(f.Typography.Text,{strong:!0,style:{display:"block",marginBottom:8},children:"Response headers to check:"}),(0,t.jsxs)("ul",{style:{paddingLeft:20,margin:"0 0 12px 0"},children:[(0,t.jsxs)("li",{children:[(0,t.jsx)(f.Typography.Text,{children:"x-litellm-semantic-filter: shows total tools → selected tools"}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block"},children:"Example: 10→3"})]}),(0,t.jsxs)("li",{children:[(0,t.jsx)(f.Typography.Text,{children:"x-litellm-semantic-filter-tools: CSV of selected tool names"}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block"},children:"Example: wikipedia-fetch,github-search,slack-post"})]})]}),(0,t.jsx)("pre",{style:{background:"#f5f5f5",padding:12,borderRadius:4,overflow:"auto",fontSize:12,margin:0},children:d})]})}]})})}let tD=async({accessToken:e,testModel:t,testQuery:s,setIsTesting:r,setTestResult:l})=>{if(!s||!t||!e)return void C.default.error("Please enter a query and select a model");r(!0),l(null);try{let{headers:r}=await (0,_.testMCPSemanticFilter)(e,t,s),a=(e=>{if(!e.filter)return null;let[t,s]=e.filter.split("->").map(Number);return{totalTools:t,selectedTools:s,tools:e.tools?e.tools.split(",").map(e=>e.trim()):[]}})(r);if(!a)return void C.default.warning("Semantic filter is not enabled or no tools were filtered");l(a),C.default.success("Semantic filter test completed successfully")}catch(e){console.error("Test failed:",e),C.default.error("Failed to test semantic filter")}finally{r(!1)}};function tH({accessToken:e}){var s;let l,{data:a,isLoading:n,isError:i,error:o}=(()=>{let{accessToken:e}=(0,w.default)();return(0,y.useQuery)({queryKey:tP.list({}),queryFn:async()=>await (0,_.getMCPSemanticFilterSettings)(e),enabled:!!e,staleTime:36e5,gcTime:36e5})})(),{mutate:c,isPending:d,error:m}=(s=e||"",l=(0,v.useQueryClient)(),(0,tf.useMutation)({mutationFn:async e=>{if(!s)throw Error("Access token is required");return(0,_.updateMCPSemanticFilterSettings)(s,e)},onSuccess:()=>{l.invalidateQueries({queryKey:tO.all})}})),[u]=D.Form.useForm(),[x,h]=(0,b.useState)(!1),[j,N]=(0,b.useState)(!1),[S,T]=(0,b.useState)([]),[k,A]=(0,b.useState)(!0),[I,P]=(0,b.useState)(""),[O,M]=(0,b.useState)("gpt-4o"),[F,E]=(0,b.useState)(null),[L,R]=(0,b.useState)(!1),U=a?.field_schema,z=a?.values??{};(0,b.useEffect)(()=>{(async()=>{if(e)try{A(!0);let t=(await (0,tU.fetchAvailableModels)(e)).filter(e=>"embedding"===e.mode);T(t)}catch(e){console.error("Error fetching embedding models:",e)}finally{A(!1)}})()},[e]),(0,b.useEffect)(()=>{z&&(u.setFieldsValue({enabled:z.enabled??!1,embedding_model:z.embedding_model??"text-embedding-3-small",top_k:z.top_k??10,similarity_threshold:z.similarity_threshold??.3}),N(!1))},[z,u]);let B=async()=>{try{let e=await u.validateFields();c(e,{onSuccess:()=>{N(!1),h(!0),setTimeout(()=>h(!1),3e3),C.default.success("Settings updated successfully. Changes will be applied across all pods within 10 seconds.")},onError:e=>{C.default.fromBackend(e)}})}catch(e){console.error("Form validation failed:",e)}},q=async()=>{e&&await tD({accessToken:e,testModel:O,testQuery:I,setIsTesting:R,setTestResult:E})};return e?(0,t.jsx)("div",{style:{width:"100%"},children:n?(0,t.jsx)(tE.Skeleton,{active:!0}):i?(0,t.jsx)(ej.Alert,{type:"error",message:"Could not load MCP Semantic Filter settings",description:o instanceof Error?o.message:void 0,style:{marginBottom:24}}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(ej.Alert,{type:"info",message:"Semantic Tool Filtering",description:"Filter MCP tools semantically based on query relevance. This reduces context window size and improves tool selection accuracy. Click 'Save Settings' to apply changes across all pods (takes effect within 10 seconds).",showIcon:!0,style:{marginBottom:24}}),x&&(0,t.jsx)(ej.Alert,{type:"success",message:"Settings saved successfully",icon:(0,t.jsx)(ey.CheckCircleOutlined,{}),showIcon:!0,closable:!0,style:{marginBottom:16}}),m&&(0,t.jsx)(ej.Alert,{type:"error",message:"Could not update settings",description:m instanceof Error?m.message:void 0,style:{marginBottom:16}}),(0,t.jsxs)(tF.Row,{gutter:24,children:[(0,t.jsx)(tM.Col,{xs:24,lg:12,children:(0,t.jsxs)(D.Form,{form:u,layout:"vertical",disabled:d,onValuesChange:()=>{N(!0)},children:[(0,t.jsxs)(e4.Card,{style:{marginBottom:16},children:[(0,t.jsx)(D.Form.Item,{name:"enabled",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Enable Semantic Filtering"}),(0,t.jsx)(g.Tooltip,{title:"When enabled, only the most relevant MCP tools will be included in requests based on semantic similarity",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),valuePropName:"checked",children:(0,t.jsx)(el.Switch,{disabled:d})}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block",marginTop:-16,marginBottom:16},children:U?.properties?.enabled?.description})]}),(0,t.jsxs)(e4.Card,{title:"Configuration",style:{marginBottom:16},children:[(0,t.jsx)(D.Form.Item,{name:"embedding_model",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Embedding Model"}),(0,t.jsx)(g.Tooltip,{title:"The model used to generate embeddings for semantic matching",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),children:(0,t.jsx)(p.Select,{options:S.map(e=>({label:e.model_group,value:e.model_group})),placeholder:k?"Loading models...":"Select embedding model",showSearch:!0,disabled:d||k,loading:k,notFoundContent:k?"Loading...":"No embedding models available"})}),(0,t.jsx)(D.Form.Item,{name:"top_k",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Top K Results"}),(0,t.jsx)(g.Tooltip,{title:"Maximum number of tools to return after filtering",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),children:(0,t.jsx)(ec.InputNumber,{min:1,max:100,style:{width:"100%"},disabled:d})}),(0,t.jsx)(D.Form.Item,{name:"similarity_threshold",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Similarity Threshold"}),(0,t.jsx)(g.Tooltip,{title:"Minimum similarity score (0-1) for a tool to be included",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),children:(0,t.jsx)(tL.Slider,{min:0,max:1,step:.05,marks:{0:"0.0",.3:"0.3",.5:"0.5",.7:"0.7",1:"1.0"},disabled:d})})]}),(0,t.jsx)("div",{style:{display:"flex",justifyContent:"flex-end",gap:8},children:(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tR.SaveOutlined,{}),onClick:B,loading:d,disabled:!j,children:"Save Settings"})})]})}),(0,t.jsx)(tM.Col,{xs:24,lg:12,children:(0,t.jsx)(t$,{accessToken:e,testQuery:I,setTestQuery:P,testModel:O,setTestModel:M,isTesting:L,onTest:q,filterEnabled:!!z.enabled,testResult:F,curlCommand:`curl --location 'http://localhost:4000/v1/responses' \\ +======== }`,copyKey:"cursor-config",className:"text-xs"})})]})]})]})]}),{})}),(0,t.jsx)(o.TabPanel,{className:"mt-6",children:(0,t.jsx)(()=>(0,t.jsxs)(eM.Space,{direction:"vertical",size:"large",className:"w-full",children:[(0,t.jsxs)("div",{className:"bg-gradient-to-r from-green-50 to-teal-50 p-6 rounded-lg border border-green-100",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 mb-3",children:[(0,t.jsx)(e9,{className:"text-green-600",size:24}),(0,t.jsx)(tl,{level:4,className:"mb-0 text-green-900",children:"Streamable HTTP Transport"})]}),(0,t.jsx)(ta,{className:"text-green-700",children:"Connect to LiteLLM MCP using HTTP transport. Compatible with any MCP client that supports HTTP streaming."})]}),(0,t.jsx)(ti,{icon:(0,t.jsx)(e9,{className:"text-green-600",size:16}),title:"Universal MCP Connection",description:"Use this URL with any MCP client that supports HTTP transport",children:(0,t.jsxs)(eM.Space,{direction:"vertical",size:"middle",className:"w-full",children:[(0,t.jsx)("div",{children:(0,t.jsx)(ta,{children:"Each MCP client supports different transports. Refer to your client documentation to determine the appropriate transport method."})}),(0,t.jsx)(g,{title:"Server URL",code:`${s}/mcp`,copyKey:"http-server-url"}),(0,t.jsx)(g,{title:"Headers Configuration",code:JSON.stringify({"x-litellm-api-key":"Bearer YOUR_LITELLM_API_KEY"},null,2),copyKey:"http-headers"}),(0,t.jsx)("div",{className:"mt-4",children:(0,t.jsx)(eb.Button,{type:"link",className:"p-0 h-auto text-blue-600 hover:text-blue-700",href:"https://modelcontextprotocol.io/docs/concepts/transports",icon:(0,t.jsx)(te.ExternalLinkIcon,{size:14}),children:"Learn more about MCP transports"})})]})})]}),{})})]})]})]})})};var tc=e.i(752978),td=e.i(591935),tm=e.i(492030);let tu=({server:e,isLoadingHealth:s,isRechecking:r,onRecheck:l})=>{let[a,n]=(0,b.useState)(!1),i=e.status||"unknown",o=e.last_health_check,c=e.health_check_error;if(s||r)return(0,t.jsxs)("span",{className:"inline-flex items-center gap-1.5 text-xs text-gray-400 px-2 py-0.5 rounded-full bg-gray-50 border border-gray-100",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-gray-300 animate-pulse"}),"Checking"]});let d=!!l,m=(0,t.jsxs)("div",{className:"max-w-xs",children:[(0,t.jsxs)("div",{className:"font-semibold mb-1",children:["Health Status: ",i]}),o&&(0,t.jsxs)("div",{className:"text-xs mb-1",children:["Last Check: ",new Date(o).toLocaleString()]}),c&&(0,t.jsxs)("div",{className:"text-xs",children:[(0,t.jsx)("div",{className:"font-medium text-red-400 mb-1",children:"Error:"}),(0,t.jsx)("div",{className:"break-words",children:c})]}),!o&&!c&&(0,t.jsx)("div",{className:"text-xs text-gray-400",children:"No health check data available"}),d&&(0,t.jsx)("div",{className:"text-xs text-gray-400 mt-1",children:"Click to recheck"})]});return(0,t.jsx)(g.Tooltip,{title:m,placement:"top",children:(0,t.jsxs)("span",{className:`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${(e=>{switch(e){case"healthy":return"text-green-700 bg-green-50 border border-green-200";case"unhealthy":return"text-red-700 bg-red-50 border border-red-200";default:return"text-gray-600 bg-gray-50 border border-gray-200"}})(i)} ${d?"cursor-pointer hover:opacity-80":"cursor-default"}`,onMouseEnter:()=>n(!0),onMouseLeave:()=>n(!1),onClick:d?()=>l(e.server_id):void 0,children:[(0,t.jsx)("span",{children:a&&d?"↻":(e=>{switch(e){case"healthy":return"✓";case"unhealthy":return"✗";default:return"?"}})(i)}),a&&d?"Recheck":i.charAt(0).toUpperCase()+i.slice(1)]})})};var tx=e.i(530212),th=e.i(848725);let tp=b.forwardRef(function(e,t){return b.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),b.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"}))});var tg=e.i(350967),tf=e.i(954616);function tb(e){if(!e)return[];if(Array.isArray(e))return e.map(e=>tj(e)).filter(e=>void 0!==e);let t=tj(e);return void 0===t?[]:[t]}function tj(e,t){if(!e)return;let s=void 0!==t?t:e.default;if("object"===e.type){let t="object"!=typeof s||null===s||Array.isArray(s)?{}:{...s};return e.properties&&Object.entries(e.properties).forEach(([e,s])=>{t[e]=tj(s,t[e])}),t}if("array"===e.type){if(Array.isArray(s)){let t=e.items;if(!t)return s;if(0===s.length){let e=tb(t);return e.length?e:s}return Array.isArray(t)?s.map((e,s)=>tj(t[s]??t[t.length-1],e)):s.map(e=>tj(t,e))}return void 0!==s?s:tb(e.items)}if(void 0!==s)return s;switch(e.type){case"integer":case"number":return 0;case"boolean":return!1;default:return""}}let ty=e=>{let t=tj(e);if("object"===e.type||"array"===e.type){let s="array"===e.type?[]:{};return JSON.stringify(t??s,null,2)}return t};function tv({tool:e,onSubmit:s,isLoading:r,result:a,error:n,onClose:i}){let[o]=D.Form.useForm(),[c,d]=b.default.useState("formatted"),[m,u]=b.default.useState(null),[x,h]=b.default.useState(null),f=b.default.useMemo(()=>"string"==typeof e.inputSchema?{type:"object",properties:{input:{type:"string",description:"Input for this tool"}},required:["input"]}:e.inputSchema,[e.inputSchema]),j=b.default.useMemo(()=>f.properties&&f.properties.params&&"object"===f.properties.params.type&&f.properties.params.properties?{type:"object",properties:f.properties.params.properties,required:f.properties.params.required||[]}:f,[f]);b.default.useEffect(()=>{if(o.resetFields(),!j.properties)return;let e={};Object.entries(j.properties).forEach(([t,s])=>{e[t]=ty(s)}),o.setFieldsValue(e)},[o,j,e]),b.default.useEffect(()=>{m&&(a||n)&&h(Date.now()-m)},[a,n,m]);let y=async e=>{try{if(navigator.clipboard&&window.isSecureContext)return await navigator.clipboard.writeText(e),!0;{let t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.focus(),t.select();let s=document.execCommand("copy");if(document.body.removeChild(t),!s)throw Error("execCommand failed");return!0}}catch(e){return console.error("Copy failed:",e),!1}},v=async()=>{await y(JSON.stringify(a,null,2))?C.default.success("Result copied to clipboard"):C.default.fromBackend("Failed to copy result")},N=async()=>{await y(e.name)?C.default.success("Tool name copied to clipboard"):C.default.fromBackend("Failed to copy tool name")};return(0,t.jsxs)("div",{className:"space-y-4 h-full",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between pb-3 border-b border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-3",children:[e.mcp_info.logo_url&&(0,t.jsx)("img",{src:e.mcp_info.logo_url,alt:`${e.mcp_info.server_name} logo`,className:"w-6 h-6 object-contain"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2 mb-1",children:[(0,t.jsx)("h2",{className:"text-lg font-semibold text-gray-900",children:"Test Tool:"}),(0,t.jsxs)("div",{className:"group inline-flex items-center space-x-1 bg-slate-50 hover:bg-slate-100 px-3 py-1 rounded-md cursor-pointer transition-colors border border-slate-200",onClick:N,title:"Click to copy tool name",children:[(0,t.jsx)("span",{className:"font-mono text-slate-700 font-medium text-sm",children:e.name}),(0,t.jsx)("svg",{className:"w-3 h-3 text-slate-400 group-hover:text-slate-600 transition-colors",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"})})]})]}),(0,t.jsx)("p",{className:"text-xs text-gray-600",children:e.description}),(0,t.jsxs)("p",{className:"text-xs text-gray-500",children:["Provider: ",e.mcp_info.server_name]})]})]}),(0,t.jsx)(l.Button,{onClick:i,variant:"light",size:"sm",className:"text-gray-500 hover:text-gray-700",children:(0,t.jsx)("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]}),(0,t.jsxs)("div",{className:"grid grid-cols-2 gap-4 h-full",children:[(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)("div",{className:"border-b border-gray-100 px-4 py-2",children:(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:"Input Parameters"}),(0,t.jsx)(g.Tooltip,{title:"Configure the input parameters for this tool call",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"text-gray-400 hover:text-gray-600"})})]})}),(0,t.jsx)("div",{className:"p-4",children:(0,t.jsxs)(D.Form,{form:o,onFinish:e=>{u(Date.now()),h(null);let t={};Object.entries(e).forEach(([e,s])=>{let r=j.properties?.[e];if(r&&null!=s&&""!==s)switch(r.type){case"boolean":t[e]="true"===s||!0===s;break;case"number":case"integer":{let l=Number(s);t[e]=Number.isNaN(l)?s:"integer"===r.type?Math.trunc(l):l;break}case"object":case"array":try{let l="string"==typeof s?JSON.parse(s):s,a="object"===r.type&&null!==l&&"object"==typeof l&&!Array.isArray(l),n="array"===r.type&&Array.isArray(l);"object"===r.type&&a||"array"===r.type&&n?t[e]=l:t[e]=s}catch(r){t[e]=s}break;case"string":t[e]=String(s);break;default:t[e]=s}else null!=s&&""!==s&&(t[e]=s)}),s(f.properties&&f.properties.params&&"object"===f.properties.params.type&&f.properties.params.properties?{params:t}:t)},layout:"vertical",className:"space-y-3",children:["string"==typeof e.inputSchema?(0,t.jsx)("div",{className:"space-y-3",children:(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700",children:["Input ",(0,t.jsx)("span",{className:"text-red-500",children:"*"})]}),name:"input",rules:[{required:!0,message:"Please enter input for this tool"}],className:"mb-3",children:(0,t.jsx)(ei.TextInput,{placeholder:"Enter input for this tool",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})})}):void 0===j.properties?(0,t.jsx)("div",{className:"text-center py-6 bg-gray-50 rounded-lg border border-gray-200",children:(0,t.jsxs)("div",{className:"max-w-sm mx-auto",children:[(0,t.jsx)("h4",{className:"text-sm font-medium text-gray-900 mb-1",children:"No Parameters Required"}),(0,t.jsx)("p",{className:"text-xs text-gray-500",children:"This tool can be called without any input parameters."})]})}):(0,t.jsx)("div",{className:"space-y-3",children:Object.entries(j.properties).map(([s,r])=>{let l=ty(r),a=`${e.name}-${s}`;return(0,t.jsxs)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:[s," ",j.required?.includes(s)&&(0,t.jsx)("span",{className:"text-red-500",children:"*"}),r.description&&(0,t.jsx)(g.Tooltip,{title:r.description,children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-gray-400 hover:text-gray-600"})})]}),name:s,initialValue:l,rules:[{required:j.required?.includes(s),message:`Please enter ${s}`},..."object"===r.type||"array"===r.type?[{validator:(e,t)=>{if((null==t||""===t)&&!j.required?.includes(s))return Promise.resolve();try{let e="string"==typeof t?JSON.parse(t):t,s="object"===r.type&&null!==e&&"object"==typeof e&&!Array.isArray(e),l="array"===r.type&&Array.isArray(e);if("object"===r.type&&s||"array"===r.type&&l)return Promise.resolve();return Promise.reject(Error("object"===r.type?"Please enter a JSON object":"Please enter a JSON array"))}catch(e){return Promise.reject(Error("Invalid JSON"))}}}]:[]],className:"mb-3",children:["string"===r.type&&r.enum&&(0,t.jsxs)("select",{className:"w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors",defaultValue:l??"",children:[!j.required?.includes(s)&&(0,t.jsxs)("option",{value:"",children:["Select ",s]}),r.enum.map(e=>(0,t.jsx)("option",{value:e,children:e},e))]}),"string"===r.type&&!r.enum&&(0,t.jsx)(ei.TextInput,{placeholder:r.description||`Enter ${s}`,defaultValue:l??"",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"}),("number"===r.type||"integer"===r.type)&&(0,t.jsx)("input",{type:"number",step:"integer"===r.type?1:"any",placeholder:r.description||`Enter ${s}`,defaultValue:l??0,className:"w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"}),"boolean"===r.type&&(0,t.jsxs)(p.Select,{placeholder:`Select ${s}`,allowClear:!j.required?.includes(s),className:"w-full",children:[(0,t.jsx)(p.Select.Option,{value:!0,children:"True"}),(0,t.jsx)(p.Select.Option,{value:!1,children:"False"})]}),("object"===r.type||"array"===r.type)&&(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("textarea",{rows:"object"===r.type?6:4,placeholder:r.description||("object"===r.type?`Enter JSON object for ${s}`:`Enter JSON array for ${s}`),defaultValue:l??("object"===r.type?"{}":"[]"),spellCheck:!1,"data-testid":`textarea-${s}`,className:"w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"}),(0,t.jsx)("p",{className:"text-xs text-gray-500",children:"object"===r.type?"Provide a valid JSON object.":"Provide a valid JSON array."})]})]},a)})}),(0,t.jsx)("div",{className:"pt-3 border-t border-gray-100",children:(0,t.jsx)(l.Button,{onClick:()=>o.submit(),disabled:r,variant:"primary",className:"w-full",loading:r,children:r?"Calling Tool...":a||n?"Call Again":"Call Tool"})})]})})]}),(0,t.jsxs)("div",{className:"bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)("div",{className:"border-b border-gray-100 px-4 py-2",children:(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:"Tool Result"})}),(0,t.jsx)("div",{className:"p-4",children:a||n||r?(0,t.jsxs)("div",{className:"space-y-3",children:[a&&!r&&!n&&(0,t.jsx)("div",{className:"p-2 bg-green-50 border border-green-200 rounded-lg",children:(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,t.jsx)("svg",{className:"h-4 w-4 text-green-500",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"})}),(0,t.jsx)("h4",{className:"text-xs font-medium text-green-900",children:"Tool executed successfully"}),null!==x&&(0,t.jsxs)("span",{className:"text-xs text-green-600 ml-1",children:["• ",(x/1e3).toFixed(2),"s"]})]}),(0,t.jsxs)("div",{className:"flex items-center space-x-1",children:[(0,t.jsxs)("div",{className:"flex bg-white rounded border border-green-300 p-0.5",children:[(0,t.jsx)("button",{onClick:()=>d("formatted"),className:`px-2 py-1 text-xs font-medium rounded transition-colors ${"formatted"===c?"bg-green-100 text-green-800":"text-green-600 hover:text-green-800"}`,children:"Formatted"}),(0,t.jsx)("button",{onClick:()=>d("json"),className:`px-2 py-1 text-xs font-medium rounded transition-colors ${"json"===c?"bg-green-100 text-green-800":"text-green-600 hover:text-green-800"}`,children:"JSON"})]}),(0,t.jsx)("button",{onClick:v,className:"p-1 hover:bg-green-100 rounded text-green-700",title:"Copy response",children:(0,t.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[(0,t.jsx)("rect",{x:"9",y:"9",width:"13",height:"13",rx:"2",ry:"2"}),(0,t.jsx)("path",{d:"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"})]})})]})]})}),(0,t.jsxs)("div",{className:"max-h-96 overflow-y-auto",children:[r&&(0,t.jsxs)("div",{className:"flex flex-col justify-center items-center h-48 text-gray-500",children:[(0,t.jsxs)("div",{className:"relative",children:[(0,t.jsx)("div",{className:"animate-spin rounded-full h-8 w-8 border-2 border-gray-200"}),(0,t.jsx)("div",{className:"animate-spin rounded-full h-8 w-8 border-2 border-blue-600 border-t-transparent absolute top-0"})]}),(0,t.jsx)("p",{className:"text-sm font-medium mt-3",children:"Calling tool..."}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-1",children:"Please wait while we process your request"})]}),n&&(0,t.jsx)("div",{className:"bg-red-50 border border-red-200 rounded-lg p-3",children:(0,t.jsxs)("div",{className:"flex items-start space-x-2",children:[(0,t.jsx)("div",{className:"flex-shrink-0",children:(0,t.jsx)("svg",{className:"h-4 w-4 text-red-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"})})}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-2 mb-1",children:[(0,t.jsx)("h4",{className:"text-xs font-medium text-red-900",children:"Tool Call Failed"}),null!==x&&(0,t.jsxs)("span",{className:"text-xs text-red-600",children:["• ",(x/1e3).toFixed(2),"s"]})]}),(0,t.jsx)("div",{className:"bg-white border border-red-200 rounded p-2 max-h-48 overflow-y-auto",children:(0,t.jsx)("pre",{className:"text-xs whitespace-pre-wrap text-red-700 font-mono",children:n.message})})]})]})}),a&&!r&&!n&&(0,t.jsx)("div",{className:"space-y-3",children:"formatted"===c?a.map((e,s)=>(0,t.jsxs)("div",{className:"border border-gray-200 rounded-lg overflow-hidden",children:["text"===e.type&&(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bg-gray-50 px-3 py-1 border-b border-gray-200",children:(0,t.jsx)("span",{className:"text-xs font-medium text-gray-700 uppercase tracking-wide",children:"Text Response"})}),(0,t.jsx)("div",{className:"p-3",children:(0,t.jsx)("div",{className:"bg-white rounded border border-gray-200 max-h-64 overflow-y-auto",children:(0,t.jsx)("div",{className:"p-3 space-y-2",children:e.text.split("\n\n").map((e,s)=>{if(""===e.trim())return null;if(e.startsWith("##")){let r=e.replace(/^#+\s/,"");return(0,t.jsx)("div",{className:"border-b border-gray-200 pb-1 mb-2",children:(0,t.jsx)("h3",{className:"text-sm font-semibold text-gray-900",children:r})},s)}let r=/(https?:\/\/[^\s\)]+)/g;if(r.test(e)){let l=e.split(r);return(0,t.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded p-2",children:(0,t.jsx)("div",{className:"text-xs text-gray-700 leading-relaxed whitespace-pre-wrap",children:l.map((e,s)=>r.test(e)?(0,t.jsx)("a",{href:e,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:text-blue-800 underline break-all",children:e},s):e)})},s)}return e.includes("Score:")?(0,t.jsx)("div",{className:"bg-green-50 border-l-4 border-green-400 p-2 rounded-r",children:(0,t.jsx)("p",{className:"text-xs text-green-800 font-medium whitespace-pre-wrap",children:e})},s):(0,t.jsx)("div",{className:"bg-gray-50 rounded p-2 border border-gray-200",children:(0,t.jsx)("div",{className:"text-xs text-gray-700 leading-relaxed whitespace-pre-wrap font-mono",children:e})},s)}).filter(Boolean)})})})]}),"image"===e.type&&e.url&&(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bg-gray-50 px-3 py-1 border-b border-gray-200",children:(0,t.jsx)("span",{className:"text-xs font-medium text-gray-700 uppercase tracking-wide",children:"Image Response"})}),(0,t.jsx)("div",{className:"p-3",children:(0,t.jsx)("div",{className:"bg-gray-50 rounded p-3 border border-gray-200",children:(0,t.jsx)("img",{src:e.url,alt:"Tool result",className:"max-w-full h-auto rounded shadow-sm"})})})]}),"embedded_resource"===e.type&&(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bg-gray-50 px-3 py-1 border-b border-gray-200",children:(0,t.jsx)("span",{className:"text-xs font-medium text-gray-700 uppercase tracking-wide",children:"Embedded Resource"})}),(0,t.jsx)("div",{className:"p-3",children:(0,t.jsxs)("div",{className:"flex items-center space-x-2 p-3 bg-blue-50 border border-blue-200 rounded",children:[(0,t.jsx)("div",{className:"flex-shrink-0",children:(0,t.jsx)("svg",{className:"h-5 w-5 text-blue-500",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})})}),(0,t.jsxs)("div",{className:"flex-1",children:[(0,t.jsxs)("p",{className:"text-xs font-medium text-blue-900",children:["Resource Type: ",e.resource_type]}),e.url&&(0,t.jsxs)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center text-xs text-blue-600 hover:text-blue-800 hover:underline mt-1 transition-colors",children:["View Resource",(0,t.jsxs)("svg",{className:"ml-1 h-3 w-3",fill:"currentColor",viewBox:"0 0 20 20",children:[(0,t.jsx)("path",{d:"M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"}),(0,t.jsx)("path",{d:"M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"})]})]})]})]})})]})]},s)):(0,t.jsx)("div",{className:"bg-white rounded border border-gray-200",children:(0,t.jsx)("div",{className:"p-3 overflow-auto max-h-80 bg-gray-50",children:(0,t.jsx)("pre",{className:"text-xs font-mono whitespace-pre-wrap break-all text-gray-800",children:JSON.stringify(a,null,2)})})})})]})]}):(0,t.jsx)("div",{className:"flex flex-col justify-center items-center h-48 text-gray-500",children:(0,t.jsxs)("div",{className:"text-center max-w-sm",children:[(0,t.jsx)("div",{className:"mb-3",children:(0,t.jsx)("svg",{className:"mx-auto h-12 w-12 text-gray-300",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:1,d:"M13 10V3L4 14h7v7l9-11h-7z"})})}),(0,t.jsx)("h4",{className:"text-sm font-medium text-gray-900 mb-1",children:"Ready to Call Tool"}),(0,t.jsx)("p",{className:"text-xs text-gray-500 leading-relaxed",children:'Configure the input parameters and click "Call Tool" to see the results here.'})]})})})]})]})]})}var tN=e.i(983561),t_=e.i(438957);let tw=({serverId:e,accessToken:s,auth_type:r,userRole:l,userID:a,serverAlias:n,extraHeaders:i})=>{let[o,c]=(0,b.useState)(null),[u,x]=(0,b.useState)(null),[h,p]=(0,b.useState)(null),[g,f]=(0,b.useState)(""),[j,v]=(0,b.useState)({}),[N,w]=(0,b.useState)(!1),S=i&&i.length>0,C=()=>{if(!n||!S)return;let e={};return Object.entries(j).forEach(([t,s])=>{s&&s.trim()&&(e[`x-mcp-${n}-${t.toLowerCase()}`]=s)}),Object.keys(e).length>0?e:void 0},{data:T,isLoading:k,error:A,refetch:I}=(0,y.useQuery)({queryKey:["mcpTools",e,j],queryFn:()=>{if(!s)throw Error("Access Token required");return(0,_.listMCPTools)(s,e,C())},enabled:!!s,staleTime:3e4}),{mutate:P,isPending:O}=(0,tf.useMutation)({mutationFn:async t=>{if(!s)throw Error("Access Token required");try{return await (0,_.callMCPTool)(s,e,t.tool.name,t.arguments,{customHeaders:C()})}catch(e){throw e}},onSuccess:e=>{x(e.content),p(null)},onError:e=>{p(e),x(null)}}),M=T?.tools||[],F=M.filter(e=>{let t=g.toLowerCase();return e.name.toLowerCase().includes(t)||e.description&&e.description.toLowerCase().includes(t)||e.mcp_info.server_name&&e.mcp_info.server_name.toLowerCase().includes(t)});return(0,t.jsx)("div",{className:"w-full h-screen p-4 bg-white",children:(0,t.jsx)(eg.Card,{className:"w-full rounded-xl shadow-md overflow-hidden",children:(0,t.jsxs)("div",{className:"flex h-auto w-full gap-4",children:[(0,t.jsxs)("div",{className:"w-1/4 p-4 bg-gray-50 flex flex-col",children:[(0,t.jsx)(m.Title,{className:"text-xl font-semibold mb-6 mt-2",children:"MCP Tools"}),(0,t.jsxs)("div",{className:"flex flex-col flex-1",children:[S&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-2",children:[(0,t.jsxs)("div",{className:"flex items-center",children:[(0,t.jsx)(t_.KeyOutlined,{className:"text-blue-600 mr-2"}),(0,t.jsx)(d.Text,{className:"text-sm font-medium text-blue-800",children:"Additional Headers"})]}),(0,t.jsx)(eb.Button,{size:"small",type:"link",onClick:()=>w(!N),className:"text-blue-700 p-0 h-auto",children:N?"Hide":"Configure"})]}),!N&&0===Object.keys(j).length&&(0,t.jsx)(d.Text,{className:"text-xs text-blue-700",children:'This server requires additional headers. Click "Configure" to provide values.'}),N&&(0,t.jsxs)("div",{className:"mt-3 space-y-2",children:[i?.map(e=>(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"block text-xs font-medium text-gray-700 mb-1",children:e}),(0,t.jsx)(H.Input,{size:"small",placeholder:`Enter ${e}`,value:j[e]||"",onChange:t=>{v({...j,[e]:t.target.value})},prefix:(0,t.jsx)(t_.KeyOutlined,{className:"text-gray-400"}),className:"rounded"})]},e)),(0,t.jsx)(eb.Button,{size:"small",type:"primary",onClick:()=>{I(),w(!1)},disabled:Object.values(j).every(e=>!e||!e.trim()),className:"w-full mt-2",children:"Load Tools"})]}),!N&&Object.keys(j).length>0&&(0,t.jsx)("div",{className:"mt-2",children:(0,t.jsxs)(d.Text,{className:"text-xs text-green-700 flex items-center",children:[(0,t.jsx)("span",{className:"inline-block w-2 h-2 bg-green-500 rounded-full mr-2"}),Object.keys(j).length," header(s) configured"]})})]}),(0,t.jsxs)("div",{className:"flex flex-col flex-1 min-h-0",children:[(0,t.jsxs)(d.Text,{className:"font-medium block mb-3 text-gray-700 flex items-center",children:[(0,t.jsx)(ep.ToolOutlined,{className:"mr-2"})," Available Tools",M.length>0&&(0,t.jsx)("span",{className:"ml-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full",children:M.length})]}),M.length>0&&(0,t.jsx)("div",{className:"mb-3",children:(0,t.jsx)(H.Input,{placeholder:"Search tools...",prefix:(0,t.jsx)(ew.SearchOutlined,{className:"text-gray-400"}),value:g,onChange:e=>f(e.target.value),allowClear:!0,className:"rounded-lg",size:"middle"})}),k&&(0,t.jsxs)("div",{className:"flex flex-col items-center justify-center py-8 bg-white border border-gray-200 rounded-lg",children:[(0,t.jsxs)("div",{className:"relative mb-3",children:[(0,t.jsx)("div",{className:"animate-spin rounded-full h-6 w-6 border-2 border-gray-200"}),(0,t.jsx)("div",{className:"animate-spin rounded-full h-6 w-6 border-2 border-blue-600 border-t-transparent absolute top-0"})]}),(0,t.jsx)("p",{className:"text-xs font-medium text-gray-700",children:"Loading tools..."})]}),T?.error&&!k&&!M.length&&(0,t.jsx)("div",{className:"p-3 text-xs text-red-800 rounded-lg bg-red-50 border border-red-200",children:(0,t.jsxs)("p",{className:"font-medium",children:["Error: ",T.message]})}),!k&&!T?.error&&(!M||0===M.length)&&(0,t.jsxs)("div",{className:"p-4 text-center bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)("div",{className:"mx-auto w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center mb-2",children:(0,t.jsx)("svg",{className:"w-4 h-4 text-gray-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 8.172V5L8 4z"})})}),(0,t.jsx)("p",{className:"text-xs font-medium text-gray-700 mb-1",children:"No tools available"}),(0,t.jsx)("p",{className:"text-xs text-gray-500",children:"No tools found for this server"})]}),!k&&!T?.error&&M.length>0&&(0,t.jsx)(t.Fragment,{children:0===F.length?(0,t.jsxs)("div",{className:"p-4 text-center bg-white border border-gray-200 rounded-lg",children:[(0,t.jsx)(ew.SearchOutlined,{className:"text-2xl text-gray-400 mb-2"}),(0,t.jsx)("p",{className:"text-xs font-medium text-gray-700 mb-1",children:"No tools found"}),(0,t.jsxs)("p",{className:"text-xs text-gray-500",children:['No tools match "',g,'"']})]}):(0,t.jsx)("div",{className:"space-y-2 flex-1 overflow-y-auto min-h-0 mcp-tools-scrollable",style:{maxHeight:"400px",scrollbarWidth:"auto",scrollbarColor:"#cbd5e0 #f7fafc"},children:F.map(e=>(0,t.jsxs)("div",{className:`border rounded-lg p-3 cursor-pointer transition-all hover:shadow-sm ${o?.name===e.name?"border-blue-500 bg-blue-50 ring-1 ring-blue-200":"border-gray-200 bg-white hover:border-gray-300"}`,onClick:()=>{c(e),x(null),p(null)},children:[(0,t.jsxs)("div",{className:"flex items-start space-x-2",children:[e.mcp_info.logo_url&&(0,t.jsx)("img",{src:e.mcp_info.logo_url,alt:`${e.mcp_info.server_name} logo`,className:"w-4 h-4 object-contain flex-shrink-0 mt-0.5"}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsx)("h4",{className:"font-mono text-xs font-medium text-gray-900 truncate",children:e.name}),(0,t.jsx)("p",{className:"text-xs text-gray-500 truncate",children:e.mcp_info.server_name}),(0,t.jsx)("p",{className:"text-xs text-gray-600 mt-1 line-clamp-2 leading-relaxed",children:e.description})]})]}),o?.name===e.name&&(0,t.jsx)("div",{className:"mt-2 pt-2 border-t border-blue-200",children:(0,t.jsxs)("div",{className:"flex items-center text-xs font-medium text-blue-700",children:[(0,t.jsx)("svg",{className:"w-3 h-3 mr-1",fill:"currentColor",viewBox:"0 0 20 20",children:(0,t.jsx)("path",{fillRule:"evenodd",d:"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",clipRule:"evenodd"})}),"Selected"]})})]},e.name))})})]})]})]}),(0,t.jsxs)("div",{className:"w-3/4 flex flex-col bg-white",children:[(0,t.jsx)("div",{className:"p-4 border-b border-gray-200 flex justify-between items-center",children:(0,t.jsx)(m.Title,{className:"text-xl font-semibold mb-0",children:"Tool Testing Playground"})}),(0,t.jsx)("div",{className:"flex-1 overflow-auto p-4",children:o?(0,t.jsx)("div",{className:"h-full",children:(0,t.jsx)(tv,{tool:o,onSubmit:e=>{P({tool:o,arguments:e})},result:u,error:h,isLoading:O,onClose:()=>c(null)})}):(0,t.jsxs)("div",{className:"h-full flex flex-col items-center justify-center text-gray-400",children:[(0,t.jsx)(tN.RobotOutlined,{style:{fontSize:"48px",marginBottom:"16px"}}),(0,t.jsx)(d.Text,{className:"text-lg font-medium text-gray-600 mb-2",children:"Select a Tool to Test"}),(0,t.jsx)(d.Text,{className:"text-center text-gray-500 max-w-md",children:"Choose a tool from the left sidebar to start testing its functionality with custom inputs."})]})})]})]})})})},tS=[eo.AUTH_TYPE.API_KEY,eo.AUTH_TYPE.BEARER_TOKEN,eo.AUTH_TYPE.TOKEN,eo.AUTH_TYPE.BASIC],tC=[...tS,eo.AUTH_TYPE.OAUTH2,eo.AUTH_TYPE.AWS_SIGV4],tT="litellm-mcp-oauth-edit-state",tk=({mcpServer:e,accessToken:s,onCancel:r,onSuccess:d,availableAccessGroups:m})=>{let[u]=D.Form.useForm(),[x,h]=(0,b.useState)({}),[f,j]=(0,b.useState)([]),[y,v]=(0,b.useState)(!1),[N,w]=(0,b.useState)(null),[S,T]=(0,b.useState)(""),[k,A]=(0,b.useState)(!1),[I,P]=(0,b.useState)([]),[O,M]=(0,b.useState)({}),[F,E]=(0,b.useState)({}),[L,R]=(0,b.useState)(null),[z,U]=(0,b.useState)(e.mcp_info?.logo_url||void 0),B=D.Form.useWatch("auth_type",u),q=D.Form.useWatch("transport",u),V="stdio"===q,$=q===eo.TRANSPORT.OPENAPI,K=!!B&&tS.includes(B),W=B===eo.AUTH_TYPE.OAUTH2,J=B===eo.AUTH_TYPE.AWS_SIGV4,Y=D.Form.useWatch("oauth_flow_type",u),G=W&&Y===eo.OAUTH_FLOW.M2M,[Q,Z]=(0,b.useState)(null),X=D.Form.useWatch("url",u),ee=D.Form.useWatch("spec_path",u),et=D.Form.useWatch("server_name",u),es=D.Form.useWatch("auth_type",u),er=D.Form.useWatch("static_headers",u),el=D.Form.useWatch("credentials",u),ea=D.Form.useWatch("authorization_url",u),ei=D.Form.useWatch("token_url",u),ed=D.Form.useWatch("registration_url",u),{startOAuthFlow:em,status:eu,error:ex,tokenResponse:eh}=eQ({accessToken:s,getCredentials:()=>u.getFieldValue("credentials"),getTemporaryPayload:()=>{let t=u.getFieldsValue(!0),s=t.url||e.url,r=t.transport||e.transport;if(!s||!r)return null;let l=Array.isArray(t.static_headers)?t.static_headers.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value??""),e},{}):{};return{server_id:e.server_id,server_name:t.server_name||e.server_name||e.alias,alias:t.alias||e.alias,description:t.description||e.description,url:s,transport:r,auth_type:eo.AUTH_TYPE.OAUTH2,credentials:t.credentials,mcp_access_groups:t.mcp_access_groups||e.mcp_access_groups,static_headers:l,command:t.command,args:t.args,env:t.env}},onTokenReceived:e=>{if(Z(e?.access_token??null),e?.access_token){let t={access_token:e.access_token,...e.refresh_token&&{refresh_token:e.refresh_token},...e.expires_in&&{expires_in:e.expires_in},...e.scope&&{scope:e.scope}};u.setFieldsValue({credentials:t}),C.default.success("OAuth authorization successful! Please click 'Update MCP Server' to save the credentials.")}},onBeforeRedirect:()=>{try{let t=u.getFieldsValue(!0);(0,eG.setSecureItem)(tT,JSON.stringify({serverId:e.server_id,formValues:t,costConfig:x,allowedTools:I,searchValue:S,aliasManuallyEdited:k}))}catch(e){console.warn("Failed to persist MCP edit state",e)}}}),ep=b.default.useMemo(()=>e.static_headers?Object.entries(e.static_headers).map(([e,t])=>({header:e,value:null!=t?String(t):""})):[],[e.static_headers]),eg=b.default.useMemo(()=>{let t=e.env??void 0;if(!t||0===Object.keys(t).length)return"";try{return JSON.stringify(t,null,2)}catch{return""}},[e.env]),ej=b.default.useMemo(()=>e.spec_path&&"stdio"!==e.transport?eo.TRANSPORT.OPENAPI:e.transport,[e]),ey=b.default.useMemo(()=>({...e,transport:ej,static_headers:ep,extra_headers:e.extra_headers||[],oauth_flow_type:e.token_url?eo.OAUTH_FLOW.M2M:eo.OAUTH_FLOW.INTERACTIVE,token_validation_json:e.token_validation?JSON.stringify(e.token_validation,null,2):void 0}),[e,ej,ep,eg]);(0,b.useEffect)(()=>{e.mcp_info?.mcp_server_cost_info&&h(e.mcp_info.mcp_server_cost_info)},[e]),(0,b.useEffect)(()=>{e.allowed_tools&&P(e.allowed_tools),M(e.tool_name_to_display_name??{}),E(e.tool_name_to_description??{})},[e]),(0,b.useEffect)(()=>{let t=(0,eG.getSecureItem)(tT);if(t)try{let s=JSON.parse(t);if(!s||s.serverId!==e.server_id)return;s.formValues&&R({...e,...s.formValues}),s.costConfig&&h(s.costConfig),s.allowedTools&&P(s.allowedTools),s.searchValue&&T(s.searchValue),"boolean"==typeof s.aliasManuallyEdited&&A(s.aliasManuallyEdited)}catch(e){console.error("Failed to restore MCP edit state",e)}finally{window.sessionStorage.removeItem(tT)}},[u,e]),(0,b.useEffect)(()=>{if(!L)return;let t=L.transport||e.transport;t&&t!==u.getFieldValue("transport")?u.setFieldsValue({transport:t}):(u.setFieldsValue(L),R(null))},[L,u,e.transport]),(0,b.useEffect)(()=>{if(e.mcp_access_groups){let t=e.mcp_access_groups.map(e=>"string"==typeof e?e:e.name||String(e));u.setFieldValue("mcp_access_groups",t)}},[e]),(0,b.useEffect)(()=>{e.server_id&&""!==e.server_id.trim()&&ev()},[e,s]);let ev=async()=>{if(s&&e.server_id){v(!0),w(null);try{let t=await (0,_.listMCPTools)(s,e.server_id);t.tools&&!t.error?j(t.tools):(console.error("Failed to fetch tools:",t.message),j([]),w(t.message||"Failed to load tools"))}catch(e){console.error("Tools fetch error:",e),j([]),w(e instanceof Error?e.message:"Failed to load tools")}finally{v(!1)}}},eN=async t=>{if(s)try{let{static_headers:r,credentials:l,stdio_config:a,env_json:n,command:i,args:o,allow_all_keys:c,available_on_public_internet:m,delegate_auth_to_upstream:u,token_validation_json:h,...p}=t,g=(p.mcp_access_groups||[]).map(e=>"string"==typeof e?e:e.name||String(e)),f=Array.isArray(r)?r.reduce((e,t)=>{let s=t?.header?.trim();return s&&(e[s]=t?.value??""),e},{}):{},b=l&&"object"==typeof l?Object.entries(l).reduce((e,[t,s])=>{if(null==s||""===s)return e;if("scopes"===t){if(Array.isArray(s)){let r=s.filter(e=>null!=e&&""!==e);r.length>0&&(e[t]=r)}}else e[t]=s;return e},{}):void 0,j={};if("stdio"===p.transport)if(a)try{let e=JSON.parse(a),t=e;if(e?.mcpServers&&"object"==typeof e.mcpServers){let s=Object.keys(e.mcpServers);s.length>0&&(t=e.mcpServers[s[0]])}let s=Array.isArray(t?.args)?t.args.map(e=>String(e)).filter(e=>""!==e.trim()):[],r=t?.env&&"object"==typeof t.env&&!Array.isArray(t.env)?Object.entries(t.env).reduce((e,[t,s])=>(null==t||""===String(t).trim()||(e[String(t)]=null==s?"":String(s)),e),{}):{};if(!(j={command:t?.command?String(t.command):void 0,args:s,env:r}).command)return void C.default.fromBackend("Stdio configuration must include a command")}catch{C.default.fromBackend("Invalid JSON in stdio configuration");return}else{let e={};if(n)try{let t=JSON.parse(n);t&&"object"==typeof t&&!Array.isArray(t)&&(e=Object.entries(t).reduce((e,[t,s])=>(null==t||""===String(t).trim()||(e[String(t)]=null==s?"":String(s)),e),{}))}catch{C.default.fromBackend("Invalid JSON in stdio env configuration");return}let t=Array.isArray(o)?o.map(e=>String(e)).filter(e=>""!==e.trim()):[],s=i?String(i).trim():"";if(!s)return void C.default.fromBackend("Stdio transport requires a command");j={command:s,args:t,env:e}}p.transport===eo.TRANSPORT.OPENAPI&&(p.transport="http");let y=null;if(h&&""!==h.trim())try{y=JSON.parse(h)}catch{C.default.fromBackend("Invalid JSON in Token Validation Rules");return}let v=p.server_name||p.url||e.server_name||e.url||p.alias||e.alias||"unknown",N={...p,...j,stdio_config:void 0,env_json:void 0,server_id:e.server_id,mcp_info:{server_name:v,description:p.description,logo_url:z||void 0,mcp_server_cost_info:Object.keys(x).length>0?x:null},mcp_access_groups:g,alias:p.alias,extra_headers:p.extra_headers||[],allowed_tools:I.length>0?I:null,tool_name_to_display_name:Object.keys(O).length>0?O:null,tool_name_to_description:Object.keys(F).length>0?F:null,disallowed_tools:p.disallowed_tools||[],static_headers:f,allow_all_keys:!!(c??e.allow_all_keys),available_on_public_internet:!!(m??e.available_on_public_internet),delegate_auth_to_upstream:p.auth_type===eo.AUTH_TYPE.OAUTH2&&!!(u??e.delegate_auth_to_upstream),...null!==y||e.token_validation?{token_validation:y}:{}};p.auth_type&&tC.includes(p.auth_type)&&b&&Object.keys(b).length>0&&(N.credentials=b);let w=await (0,_.updateMCPServer)(s,N);C.default.success("MCP Server updated successfully"),d(w)}catch(e){C.default.fromBackend("Failed to update MCP Server"+(e?.message?`: ${e.message}`:""))}};return(0,t.jsxs)(n.TabGroup,{children:[(0,t.jsxs)(i.TabList,{className:"grid w-full grid-cols-2",children:[(0,t.jsx)(a.Tab,{children:"Server Configuration"}),(0,t.jsx)(a.Tab,{children:"Cost Configuration"})]}),(0,t.jsxs)(c.TabPanels,{className:"mt-6",children:[(0,t.jsx)(o.TabPanel,{children:(0,t.jsxs)(D.Form,{form:u,onFinish:eN,initialValues:ey,layout:"vertical",children:[(0,t.jsx)(D.Form.Item,{label:"MCP Server Name",name:"server_name",rules:[{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(H.Input,{className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:"Alias",name:"alias",rules:[{validator:(e,t)=>eW(t)}],children:(0,t.jsx)(H.Input,{onChange:()=>A(!0),className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:"Description",name:"description",children:(0,t.jsx)(H.Input,{className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(e$,{value:z,onChange:U}),(0,t.jsx)(D.Form.Item,{label:"Transport Type",name:"transport",rules:[{required:!0}],children:(0,t.jsxs)(p.Select,{onChange:e=>{"stdio"===e?u.setFieldsValue({url:void 0,spec_path:void 0,auth_type:void 0,credentials:void 0,authorization_url:void 0,token_url:void 0,registration_url:void 0}):e===eo.TRANSPORT.OPENAPI?u.setFieldsValue({url:void 0,command:void 0,args:void 0,env_json:void 0,stdio_config:void 0}):u.setFieldsValue({spec_path:void 0,command:void 0,args:void 0,env_json:void 0,stdio_config:void 0})},children:[(0,t.jsx)(p.Select.Option,{value:"http",children:"Streamable HTTP (Recommended)"}),(0,t.jsx)(p.Select.Option,{value:"sse",children:"Server-Sent Events (SSE)"}),(0,t.jsx)(p.Select.Option,{value:"stdio",children:"Standard Input/Output (stdio)"}),(0,t.jsx)(p.Select.Option,{value:eo.TRANSPORT.OPENAPI,children:"OpenAPI Spec"})]})}),!V&&!$&&(0,t.jsx)(D.Form.Item,{label:"MCP Server URL",name:"url",rules:[{required:!0,message:"Please enter a server URL"},{validator:(e,t)=>eK(t)}],children:(0,t.jsx)(H.Input,{placeholder:"https://your-mcp-server.com",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),$&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OpenAPI Spec URL",(0,t.jsx)(g.Tooltip,{title:"URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"spec_path",rules:[{required:!0,message:"Please enter an OpenAPI spec URL"}],children:(0,t.jsx)(H.Input,{placeholder:"https://petstore3.swagger.io/api/v3/openapi.json",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),!V&&(0,t.jsx)(D.Form.Item,{label:"Authentication",name:"auth_type",rules:[{required:!0}],children:(0,t.jsxs)(p.Select,{children:[(0,t.jsx)(p.Select.Option,{value:"none",children:"None"}),(0,t.jsx)(p.Select.Option,{value:"api_key",children:"API Key"}),(0,t.jsx)(p.Select.Option,{value:"bearer_token",children:"Bearer Token"}),(0,t.jsx)(p.Select.Option,{value:"token",children:"Token"}),(0,t.jsx)(p.Select.Option,{value:"basic",children:"Basic Auth"}),(0,t.jsx)(p.Select.Option,{value:"oauth2",children:"OAuth"}),(0,t.jsx)(p.Select.Option,{value:"aws_sigv4",children:"AWS SigV4 (Bedrock AgentCore MCPs)"})]})}),V&&(0,t.jsxs)("div",{className:"rounded-lg border border-gray-200 p-4 space-y-4",children:[(0,t.jsx)("p",{className:"text-sm text-gray-600",children:"Configure the stdio transport used to launch the MCP server process. You can either fill in the fields below or paste a JSON configuration."}),(0,t.jsx)(D.Form.Item,{label:"Command",name:"command",rules:[{required:!0,message:"Please enter a command for stdio transport"}],children:(0,t.jsx)(H.Input,{placeholder:"e.g., npx",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:"Args",name:"args",children:(0,t.jsx)(p.Select,{mode:"tags",size:"large",tokenSeparators:[","],placeholder:"Add args (press enter or comma)",className:"rounded-lg"})}),(0,t.jsx)(D.Form.Item,{label:"Environment (JSON object)",name:"env_json",rules:[{validator:(e,t)=>{if(!t)return Promise.resolve();try{let e=JSON.parse(t);if(e&&"object"==typeof e&&!Array.isArray(e))return Promise.resolve();return Promise.reject(Error("Env must be a JSON object"))}catch{return Promise.reject(Error("Please enter valid JSON"))}}}],children:(0,t.jsx)(H.Input.TextArea,{rows:6,className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 font-mono text-sm",placeholder:`{ "KEY": "value" }`})}),(0,t.jsx)(eO,{isVisible:!0,required:!1})]}),!V&&K&&(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Authentication Value",(0,t.jsx)(g.Tooltip,{title:"Token, password, or header value to send with each request for the selected auth type.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","auth_value"],rules:[{validator:(e,t)=>t&&"string"==typeof t&&""===t.trim()?Promise.reject(Error("Authentication value cannot be empty")):Promise.resolve()}],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter token or secret (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),!V&&W&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OAuth Client ID (optional)",(0,t.jsx)(g.Tooltip,{title:"Provide only if your MCP server cannot handle dynamic client registration.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","client_id"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter OAuth client ID (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OAuth Client Secret (optional)",(0,t.jsx)(g.Tooltip,{title:"Provide only if your MCP server cannot handle dynamic client registration.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","client_secret"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Enter OAuth client secret (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["OAuth Scopes (optional)",(0,t.jsx)(g.Tooltip,{title:"Add scopes to override the default scope list used for this MCP server.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","scopes"],children:(0,t.jsx)(p.Select,{mode:"tags",tokenSeparators:[","],placeholder:"Add scopes",className:"rounded-lg",size:"large"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Authorization URL Override (optional)",(0,t.jsx)(g.Tooltip,{title:"Optional override for the authorization endpoint.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"authorization_url",children:(0,t.jsx)(H.Input,{placeholder:"https://example.com/oauth/authorize",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Token URL Override (optional)",(0,t.jsx)(g.Tooltip,{title:"Optional override for the token endpoint.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"token_url",children:(0,t.jsx)(H.Input,{placeholder:"https://example.com/oauth/token",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Registration URL Override (optional)",(0,t.jsx)(g.Tooltip,{title:"Optional override for the dynamic client registration endpoint.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"registration_url",children:(0,t.jsx)(H.Input,{placeholder:"https://example.com/oauth/register",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),!G&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Token Validation Rules (optional)",(0,t.jsx)(g.Tooltip,{title:'JSON object of key-value rules checked against the OAuth token response before storing. Supports dot-notation for nested fields (e.g. {"organization": "my-org", "team.id": "123"}). Tokens that fail validation are rejected with HTTP 403.',children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"token_validation_json",rules:[{validator:(e,t)=>{if(!t||""===t.trim())return Promise.resolve();try{return JSON.parse(t),Promise.resolve()}catch{return Promise.reject(Error("Must be valid JSON"))}}}],children:(0,t.jsx)(H.Input.TextArea,{placeholder:'{\n "organization": "my-org",\n "team.id": "123"\n}',rows:4,className:"font-mono text-sm rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["Token Storage TTL (seconds, optional)",(0,t.jsx)(g.Tooltip,{title:"How long to cache each user's OAuth access token in Redis before evicting it (regardless of the token's own expires_in). Leave blank to derive the TTL from the token's expires_in, or fall back to the 12-hour default.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:"token_storage_ttl_seconds",children:(0,t.jsx)(ec.InputNumber,{min:1,placeholder:"e.g. 3600",style:{width:"100%"},className:"rounded-lg"})})]}),(0,t.jsxs)("div",{className:"rounded-lg border border-dashed border-gray-300 p-4 space-y-2",children:[(0,t.jsx)("p",{className:"text-sm text-gray-600",children:"Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value."}),(0,t.jsx)(l.Button,{variant:"secondary",onClick:em,disabled:"authorizing"===eu||"exchanging"===eu,children:"authorizing"===eu?"Waiting for authorization...":"exchanging"===eu?"Exchanging authorization code...":"Authorize & Fetch Token"}),ex&&(0,t.jsx)("p",{className:"text-sm text-red-500",children:ex}),"success"===eu&&eh?.access_token&&(0,t.jsxs)("p",{className:"text-sm text-green-600",children:["Token fetched. Expires in ",eh.expires_in??"?"," seconds."]})]})]}),!V&&J&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"text-sm text-gray-500 mb-2",children:["For MCP servers hosted on AWS Bedrock AgentCore."," ",(0,t.jsx)("a",{href:"https://docs.litellm.ai/docs/mcp_aws_sigv4",target:"_blank",rel:"noopener noreferrer",className:"text-blue-500 hover:text-blue-700",children:"View docs →"})]}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Region",(0,t.jsx)(g.Tooltip,{title:"AWS region for SigV4 signing (e.g., us-east-1)",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_region_name"],rules:[],children:(0,t.jsx)(H.Input,{placeholder:"us-east-1 (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Service Name",(0,t.jsx)(g.Tooltip,{title:"AWS service name for SigV4 signing. Defaults to 'bedrock-agentcore'.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_service_name"],children:(0,t.jsx)(H.Input,{placeholder:"bedrock-agentcore (leave blank to keep existing)",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Access Key ID",(0,t.jsx)(g.Tooltip,{title:"Optional. If not provided, falls back to the boto3 credential chain (IAM role, env vars, etc.).",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_access_key_id"],rules:[],children:(0,t.jsx)(H.Input.Password,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Secret Access Key",(0,t.jsx)(g.Tooltip,{title:"Optional. Required if AWS Access Key ID is provided.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_secret_access_key"],rules:[],children:(0,t.jsx)(H.Input.Password,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Token",(0,t.jsx)(g.Tooltip,{title:"Optional. Only needed for temporary STS credentials.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_token"],children:(0,t.jsx)(H.Input.Password,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Role ARN",(0,t.jsx)(g.Tooltip,{title:"Optional. IAM role ARN to assume via STS before signing. If set, LiteLLM calls sts:AssumeRole to get temporary credentials.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_role_name"],children:(0,t.jsx)(H.Input,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})}),(0,t.jsx)(D.Form.Item,{label:(0,t.jsxs)("span",{className:"text-sm font-medium text-gray-700 flex items-center",children:["AWS Session Name",(0,t.jsx)(g.Tooltip,{title:"Optional. Session name for the AssumeRole call — appears in CloudTrail logs. Auto-generated if omitted.",children:(0,t.jsx)(en.InfoCircleOutlined,{className:"ml-2 text-blue-400 hover:text-blue-600 cursor-help"})})]}),name:["credentials","aws_session_name"],children:(0,t.jsx)(H.Input,{placeholder:"Leave blank to keep existing",className:"rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"})})]}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(eR,{availableAccessGroups:m,mcpServer:e,searchValue:S,setSearchValue:T,getAccessGroupOptions:()=>{let e=m.map(e=>({value:e,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-green-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:e})]})}));return S&&!m.some(e=>e.toLowerCase().includes(S.toLowerCase()))&&e.push({value:S,label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:"w-2 h-2 bg-blue-500 rounded-full"}),(0,t.jsx)("span",{className:"font-medium",children:S}),(0,t.jsx)("span",{className:"text-gray-400 text-xs ml-1",children:"create new group"})]})}),e}})}),(0,t.jsx)("div",{className:"mt-6",children:(0,t.jsx)(eP,{accessToken:s,oauthAccessToken:Q,formValues:{server_id:e.server_id,server_name:et??e.server_name,url:X??e.url,spec_path:ee??e.spec_path,transport:q??e.transport,auth_type:es??e.auth_type,mcp_info:e.mcp_info,oauth_flow_type:ei??e.token_url?eo.OAUTH_FLOW.M2M:eo.OAUTH_FLOW.INTERACTIVE,static_headers:er??e.static_headers,credentials:el,authorization_url:ea??e.authorization_url,token_url:ei??e.token_url,registration_url:ed??e.registration_url},allowedTools:I,existingAllowedTools:e.allowed_tools||null,onAllowedToolsChange:P,toolNameToDisplayName:O,toolNameToDescription:F,onToolNameToDisplayNameChange:M,onToolNameToDescriptionChange:E})}),(0,t.jsxs)("div",{className:"flex justify-end gap-2",children:[(0,t.jsx)(eb.Button,{onClick:r,children:"Cancel"}),(0,t.jsx)(l.Button,{type:"submit",children:"Save Changes"})]})]})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsxs)("div",{className:"space-y-6",children:[(0,t.jsx)(ef,{value:x,onChange:h,tools:f,disabled:y}),(0,t.jsxs)("div",{className:"flex justify-end gap-2",children:[(0,t.jsx)(eb.Button,{onClick:r,children:"Cancel"}),(0,t.jsx)(l.Button,{onClick:()=>u.submit(),children:"Save Changes"})]})]})})]})]})},tA=({costConfig:e})=>{let s=e?.default_cost_per_query!==void 0&&e?.default_cost_per_query!==null,r=e?.tool_name_to_cost_per_query&&Object.keys(e.tool_name_to_cost_per_query).length>0;return s||r?(0,t.jsx)("div",{className:"mt-6 pt-6 border-t border-gray-200",children:(0,t.jsxs)("div",{className:"space-y-4",children:[s&&e?.default_cost_per_query!==void 0&&e?.default_cost_per_query!==null&&(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"font-medium",children:"Default Cost per Query"}),(0,t.jsxs)("div",{className:"text-green-600 font-mono",children:["$",e.default_cost_per_query.toFixed(4)]})]}),r&&e?.tool_name_to_cost_per_query&&(0,t.jsxs)("div",{children:[(0,t.jsx)(d.Text,{className:"font-medium",children:"Tool-Specific Costs"}),(0,t.jsx)("div",{className:"mt-2 space-y-2",children:Object.entries(e.tool_name_to_cost_per_query).map(([e,s])=>null!=s&&(0,t.jsxs)("div",{className:"flex justify-between items-center p-3 bg-gray-50 rounded-lg",children:[(0,t.jsx)(d.Text,{className:"font-medium",children:e}),(0,t.jsxs)(d.Text,{className:"text-green-600 font-mono",children:["$",s.toFixed(4)," per query"]})]},e))})]}),(0,t.jsxs)("div",{className:"mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg",children:[(0,t.jsx)(d.Text,{className:"text-blue-800 font-medium",children:"Cost Summary:"}),(0,t.jsxs)("div",{className:"mt-2 space-y-1",children:[s&&e?.default_cost_per_query!==void 0&&e?.default_cost_per_query!==null&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• Default cost: $",e.default_cost_per_query.toFixed(4)," per query"]}),r&&e?.tool_name_to_cost_per_query&&(0,t.jsxs)(d.Text,{className:"text-blue-700",children:["• ",Object.keys(e.tool_name_to_cost_per_query).length," tool(s) with custom pricing"]})]})]})]})}):(0,t.jsx)("div",{className:"mt-6 pt-6 border-t border-gray-200",children:(0,t.jsx)("div",{className:"space-y-4",children:(0,t.jsx)("div",{className:"p-4 bg-gray-50 border border-gray-200 rounded-lg",children:(0,t.jsx)(d.Text,{className:"text-gray-600",children:"No cost configuration set for this server. Tool calls will be charged at $0.00 per tool call."})})})})},tI=({mcpServer:e,onBack:s,isEditing:r,isProxyAdmin:u,accessToken:x,userRole:h,userID:p,availableAccessGroups:g})=>{let[f,j]=(0,b.useState)(r),[y,v]=(0,b.useState)(!1),[N,_]=(0,b.useState)({}),[w,S]=(0,b.useState)(0),C=e.url??"",{maskedUrl:T,hasToken:A}=C?eH(C):{maskedUrl:"—",hasToken:!1},I=(e,t)=>e?A?t?e:T:e:"—",P=async(e,t)=>{await (0,tr.copyToClipboard)(e)&&(_(e=>({...e,[t]:!0})),setTimeout(()=>{_(e=>({...e,[t]:!1}))},2e3))},O=e=>{let s=e.toUpperCase();return(0,t.jsx)("span",{className:"inline-flex items-center text-sm font-medium px-2.5 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:s})},M=e=>(0,t.jsx)("span",{className:"inline-flex items-center text-sm font-medium px-2.5 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:e});return(0,t.jsxs)("div",{className:"p-4 max-w-full",children:[(0,t.jsxs)("div",{className:"mb-6",children:[(0,t.jsx)(l.Button,{icon:tx.ArrowLeftIcon,variant:"light",className:"mb-4",onClick:s,children:"Back to All Servers"}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(m.Title,{className:"text-2xl",children:e.server_name||e.alias||"Unnamed Server"}),(0,t.jsx)(eb.Button,{type:"text",size:"small",icon:N["mcp-server_name"]?(0,t.jsx)(k.CheckIcon,{size:12}):(0,t.jsx)(e6.CopyIcon,{size:12}),onClick:()=>P(e.server_name||e.alias,"mcp-server_name"),className:`transition-all duration-200 ${N["mcp-server_name"]?"text-green-600 bg-green-50 border-green-200":"text-gray-400 hover:text-gray-600 hover:bg-gray-100"}`}),e.alias&&e.server_name&&e.alias!==e.server_name&&(0,t.jsx)("span",{className:"ml-2 inline-flex items-center text-xs font-medium px-2 py-0.5 rounded bg-gray-100 text-gray-600 border border-gray-200 font-mono",children:e.alias})]}),(0,t.jsxs)("div",{className:"flex items-center gap-1.5 mt-1",children:[(0,t.jsx)(d.Text,{className:"text-gray-400 font-mono text-xs",children:e.server_id}),(0,t.jsx)(eb.Button,{type:"text",size:"small",icon:N["mcp-server-id"]?(0,t.jsx)(k.CheckIcon,{size:10}):(0,t.jsx)(e6.CopyIcon,{size:10}),onClick:()=>P(e.server_id,"mcp-server-id"),className:`transition-all duration-200 ${N["mcp-server-id"]?"text-green-600 bg-green-50 border-green-200":"text-gray-300 hover:text-gray-500 hover:bg-gray-50"}`})]}),e.description&&(0,t.jsx)(d.Text,{className:"text-gray-500 mt-2",children:e.description})]}),(0,t.jsxs)(n.TabGroup,{index:w,onIndexChange:S,children:[(0,t.jsx)(i.TabList,{className:"mb-4",children:[(0,t.jsx)(a.Tab,{children:"Overview"},"overview"),(0,t.jsx)(a.Tab,{children:"MCP Tools"},"tools"),...u?[(0,t.jsx)(a.Tab,{children:"Settings"},"settings")]:[]]}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsxs)(o.TabPanel,{children:[(0,t.jsxs)(tg.Grid,{numItems:1,numItemsSm:2,numItemsLg:3,className:"gap-4",children:[(0,t.jsxs)(eg.Card,{className:"p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Transport"}),(0,t.jsx)("div",{className:"mt-3",children:O((0,eo.handleTransport)(e.transport??void 0,e.spec_path??void 0))})]}),(0,t.jsxs)(eg.Card,{className:"p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Authentication"}),(0,t.jsx)("div",{className:"mt-3",children:M((0,eo.handleAuth)(e.auth_type??void 0))})]}),(0,t.jsxs)(eg.Card,{className:"p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Host URL"}),(0,t.jsxs)("div",{className:"mt-3 flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"break-all overflow-wrap-anywhere font-mono text-sm",children:I(e.url,y)}),A&&u&&(0,t.jsx)("button",{onClick:()=>v(!y),className:"p-1 hover:bg-gray-100 rounded flex-shrink-0",children:(0,t.jsx)(tc.Icon,{icon:y?tp:th.EyeIcon,size:"sm",className:"text-gray-500"})})]})]})]}),(0,t.jsxs)(eg.Card,{className:"mt-4 p-4",children:[(0,t.jsx)(d.Text,{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:"Cost Configuration"}),(0,t.jsx)("div",{className:"mt-3",children:(0,t.jsx)(tA,{costConfig:e.mcp_info?.mcp_server_cost_info})})]})]}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tw,{serverId:e.server_id,accessToken:x,auth_type:e.auth_type,userRole:h,userID:p,serverAlias:e.alias,extraHeaders:e.extra_headers})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsxs)(eg.Card,{children:[(0,t.jsxs)("div",{className:"flex justify-between items-center mb-4",children:[(0,t.jsx)(m.Title,{children:"MCP Server Settings"}),f?null:(0,t.jsx)(l.Button,{variant:"light",onClick:()=>j(!0),children:"Edit Settings"})]}),f?(0,t.jsx)(tk,{mcpServer:e,accessToken:x,onCancel:()=>j(!1),onSuccess:e=>{j(!1),s()},availableAccessGroups:g}):(0,t.jsxs)("div",{className:"divide-y divide-gray-100",children:[(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Server Name"}),(0,t.jsx)("div",{className:"col-span-2 text-sm text-gray-900",children:e.server_name||(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Alias"}),(0,t.jsx)("div",{className:"col-span-2 text-sm font-mono text-gray-900",children:e.alias||(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Description"}),(0,t.jsx)("div",{className:"col-span-2 text-sm text-gray-900",children:e.description||(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"URL"}),(0,t.jsxs)("div",{className:"col-span-2 text-sm font-mono text-gray-900 break-all flex items-center gap-2",children:[I(e.url,y),A&&(0,t.jsx)("button",{onClick:()=>v(!y),className:"p-1 hover:bg-gray-100 rounded flex-shrink-0",children:(0,t.jsx)(tc.Icon,{icon:y?tp:th.EyeIcon,size:"sm",className:"text-gray-500"})})]})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Transport"}),(0,t.jsx)("div",{className:"col-span-2",children:O((0,eo.handleTransport)(e.transport,e.spec_path))})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Authentication"}),(0,t.jsx)("div",{className:"col-span-2",children:M((0,eo.handleAuth)(e.auth_type))})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Extra Headers"}),(0,t.jsx)("div",{className:"col-span-2 text-sm text-gray-900",children:e.extra_headers&&e.extra_headers.length>0?e.extra_headers.join(", "):(0,t.jsx)("span",{className:"text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Allow All Keys"}),(0,t.jsx)("div",{className:"col-span-2",children:e.allow_all_keys?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Enabled"]}):(0,t.jsx)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-gray-50 text-gray-600 rounded-full border border-gray-200 text-xs font-medium",children:"Disabled"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Network Access"}),(0,t.jsx)("div",{className:"col-span-2",children:e.available_on_public_internet?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Public"]}):(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-orange-500"}),"Internal only"]})})]}),"oauth2"===(0,eo.handleAuth)(e.auth_type)&&(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Delegate Auth to Upstream"}),(0,t.jsx)("div",{className:"col-span-2",children:e.delegate_auth_to_upstream?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Enabled (PKCE passthrough)"]}):(0,t.jsx)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-gray-50 text-gray-600 rounded-full border border-gray-200 text-xs font-medium",children:"Disabled"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Access Groups"}),(0,t.jsx)("div",{className:"col-span-2",children:e.mcp_access_groups&&e.mcp_access_groups.length>0?(0,t.jsx)("div",{className:"flex flex-wrap gap-1.5",children:e.mcp_access_groups.map((e,s)=>(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200",children:"string"==typeof e?e:e?.name??""},s))}):(0,t.jsx)("span",{className:"text-sm text-gray-400",children:"—"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Allowed Tools"}),(0,t.jsx)("div",{className:"col-span-2",children:e.allowed_tools&&e.allowed_tools.length>0?(0,t.jsx)("div",{className:"flex flex-wrap gap-1.5",children:e.allowed_tools.map((e,s)=>(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-mono font-medium px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200",children:e},s))}):(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded bg-green-50 text-green-700 border border-green-200",children:"All tools enabled"})})]}),(0,t.jsxs)("div",{className:"py-3 grid grid-cols-3 gap-4",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-500",children:"Cost"}),(0,t.jsx)("div",{className:"col-span-2",children:(0,t.jsx)(tA,{costConfig:e.mcp_info?.mcp_server_cost_info})})]})]})]})})]})]})]})},tP=(0,N.createQueryKeys)("mcpSemanticFilterSettings"),tO=(0,N.createQueryKeys)("mcpSemanticFilterSettings");var tM=e.i(178654),tF=e.i(621192),tE=e.i(981339),tL=e.i(850627),tR=e.i(987432),tz=e.i(689020),tU=e.i(245094),tB=e.i(788191),tq=e.i(653496),tV=e.i(992619);function t$({accessToken:e,testQuery:s,setTestQuery:r,testModel:l,setTestModel:a,isTesting:n,onTest:i,filterEnabled:o,testResult:c,curlCommand:d}){return(0,t.jsx)(e4.Card,{title:"Test Configuration",style:{marginBottom:16},children:(0,t.jsx)(tq.Tabs,{defaultActiveKey:"test",items:[{key:"test",label:"Test",children:(0,t.jsxs)(eM.Space,{direction:"vertical",style:{width:"100%"},size:"large",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)(f.Typography.Text,{strong:!0,style:{display:"block",marginBottom:8},children:[(0,t.jsx)(tB.PlayCircleOutlined,{})," Test Query"]}),(0,t.jsx)(H.Input.TextArea,{placeholder:"Enter a test query to see which tools would be selected...",value:s,onChange:e=>r(e.target.value),rows:4,disabled:n})]}),(0,t.jsx)("div",{children:(0,t.jsx)(tV.default,{accessToken:e||"",value:l,onChange:a,disabled:n,showLabel:!0,labelText:"Select Model"})}),(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tB.PlayCircleOutlined,{}),onClick:i,loading:n,disabled:!s||!l||!o,block:!0,children:"Test Filter"}),!o&&(0,t.jsx)(ej.Alert,{type:"warning",message:"Semantic filtering is disabled",description:"Enable semantic filtering and save settings to test the filter.",showIcon:!0}),c&&(0,t.jsxs)("div",{children:[(0,t.jsx)(f.Typography.Title,{level:5,children:"Results"}),(0,t.jsx)(ej.Alert,{type:"success",message:`${c.selectedTools} tools selected`,description:`Filtered from ${c.totalTools} available tools`,showIcon:!0,style:{marginBottom:16}}),(0,t.jsxs)("div",{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,style:{display:"block",marginBottom:8},children:"Selected Tools:"}),(0,t.jsx)("ul",{style:{paddingLeft:20,margin:0},children:c.tools.map((e,s)=>(0,t.jsx)("li",{style:{marginBottom:4},children:(0,t.jsx)(f.Typography.Text,{children:e})},s))})]})]})]})},{key:"api",label:"API Usage",children:(0,t.jsxs)("div",{children:[(0,t.jsxs)(eM.Space,{style:{marginBottom:8},children:[(0,t.jsx)(tU.CodeOutlined,{}),(0,t.jsx)(f.Typography.Text,{strong:!0,children:"API Usage"})]}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block",marginBottom:8},children:"Use this curl command to test the semantic filter with your current configuration."}),(0,t.jsx)(f.Typography.Text,{strong:!0,style:{display:"block",marginBottom:8},children:"Response headers to check:"}),(0,t.jsxs)("ul",{style:{paddingLeft:20,margin:"0 0 12px 0"},children:[(0,t.jsxs)("li",{children:[(0,t.jsx)(f.Typography.Text,{children:"x-litellm-semantic-filter: shows total tools → selected tools"}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block"},children:"Example: 10→3"})]}),(0,t.jsxs)("li",{children:[(0,t.jsx)(f.Typography.Text,{children:"x-litellm-semantic-filter-tools: CSV of selected tool names"}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block"},children:"Example: wikipedia-fetch,github-search,slack-post"})]})]}),(0,t.jsx)("pre",{style:{background:"#f5f5f5",padding:12,borderRadius:4,overflow:"auto",fontSize:12,margin:0},children:d})]})}]})})}let tD=async({accessToken:e,testModel:t,testQuery:s,setIsTesting:r,setTestResult:l})=>{if(!s||!t||!e)return void C.default.error("Please enter a query and select a model");r(!0),l(null);try{let{headers:r}=await (0,_.testMCPSemanticFilter)(e,t,s),a=(e=>{if(!e.filter)return null;let[t,s]=e.filter.split("->").map(Number);return{totalTools:t,selectedTools:s,tools:e.tools?e.tools.split(",").map(e=>e.trim()):[]}})(r);if(!a)return void C.default.warning("Semantic filter is not enabled or no tools were filtered");l(a),C.default.success("Semantic filter test completed successfully")}catch(e){console.error("Test failed:",e),C.default.error("Failed to test semantic filter")}finally{r(!1)}};function tH({accessToken:e}){var s;let l,{data:a,isLoading:n,isError:i,error:o}=(()=>{let{accessToken:e}=(0,w.default)();return(0,y.useQuery)({queryKey:tP.list({}),queryFn:async()=>await (0,_.getMCPSemanticFilterSettings)(e),enabled:!!e,staleTime:36e5,gcTime:36e5})})(),{mutate:c,isPending:d,error:m}=(s=e||"",l=(0,v.useQueryClient)(),(0,tf.useMutation)({mutationFn:async e=>{if(!s)throw Error("Access token is required");return(0,_.updateMCPSemanticFilterSettings)(s,e)},onSuccess:()=>{l.invalidateQueries({queryKey:tO.all})}})),[u]=D.Form.useForm(),[x,h]=(0,b.useState)(!1),[j,N]=(0,b.useState)(!1),[S,T]=(0,b.useState)([]),[k,A]=(0,b.useState)(!0),[I,P]=(0,b.useState)(""),[O,M]=(0,b.useState)("gpt-4o"),[F,E]=(0,b.useState)(null),[L,R]=(0,b.useState)(!1),z=a?.field_schema,U=a?.values??{};(0,b.useEffect)(()=>{(async()=>{if(e)try{A(!0);let t=(await (0,tz.fetchAvailableModels)(e)).filter(e=>"embedding"===e.mode);T(t)}catch(e){console.error("Error fetching embedding models:",e)}finally{A(!1)}})()},[e]),(0,b.useEffect)(()=>{U&&(u.setFieldsValue({enabled:U.enabled??!1,embedding_model:U.embedding_model??"text-embedding-3-small",top_k:U.top_k??10,similarity_threshold:U.similarity_threshold??.3}),N(!1))},[U,u]);let B=async()=>{try{let e=await u.validateFields();c(e,{onSuccess:()=>{N(!1),h(!0),setTimeout(()=>h(!1),3e3),C.default.success("Settings updated successfully. Changes will be applied across all pods within 10 seconds.")},onError:e=>{C.default.fromBackend(e)}})}catch(e){console.error("Form validation failed:",e)}},q=async()=>{e&&await tD({accessToken:e,testModel:O,testQuery:I,setIsTesting:R,setTestResult:E})};return e?(0,t.jsx)("div",{style:{width:"100%"},children:n?(0,t.jsx)(tE.Skeleton,{active:!0}):i?(0,t.jsx)(ej.Alert,{type:"error",message:"Could not load MCP Semantic Filter settings",description:o instanceof Error?o.message:void 0,style:{marginBottom:24}}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(ej.Alert,{type:"info",message:"Semantic Tool Filtering",description:"Filter MCP tools semantically based on query relevance. This reduces context window size and improves tool selection accuracy. Click 'Save Settings' to apply changes across all pods (takes effect within 10 seconds).",showIcon:!0,style:{marginBottom:24}}),x&&(0,t.jsx)(ej.Alert,{type:"success",message:"Settings saved successfully",icon:(0,t.jsx)(ey.CheckCircleOutlined,{}),showIcon:!0,closable:!0,style:{marginBottom:16}}),m&&(0,t.jsx)(ej.Alert,{type:"error",message:"Could not update settings",description:m instanceof Error?m.message:void 0,style:{marginBottom:16}}),(0,t.jsxs)(tF.Row,{gutter:24,children:[(0,t.jsx)(tM.Col,{xs:24,lg:12,children:(0,t.jsxs)(D.Form,{form:u,layout:"vertical",disabled:d,onValuesChange:()=>{N(!0)},children:[(0,t.jsxs)(e4.Card,{style:{marginBottom:16},children:[(0,t.jsx)(D.Form.Item,{name:"enabled",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Enable Semantic Filtering"}),(0,t.jsx)(g.Tooltip,{title:"When enabled, only the most relevant MCP tools will be included in requests based on semantic similarity",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),valuePropName:"checked",children:(0,t.jsx)(el.Switch,{disabled:d})}),(0,t.jsx)(f.Typography.Text,{type:"secondary",style:{display:"block",marginTop:-16,marginBottom:16},children:z?.properties?.enabled?.description})]}),(0,t.jsxs)(e4.Card,{title:"Configuration",style:{marginBottom:16},children:[(0,t.jsx)(D.Form.Item,{name:"embedding_model",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Embedding Model"}),(0,t.jsx)(g.Tooltip,{title:"The model used to generate embeddings for semantic matching",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),children:(0,t.jsx)(p.Select,{options:S.map(e=>({label:e.model_group,value:e.model_group})),placeholder:k?"Loading models...":"Select embedding model",showSearch:!0,disabled:d||k,loading:k,notFoundContent:k?"Loading...":"No embedding models available"})}),(0,t.jsx)(D.Form.Item,{name:"top_k",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Top K Results"}),(0,t.jsx)(g.Tooltip,{title:"Maximum number of tools to return after filtering",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),children:(0,t.jsx)(ec.InputNumber,{min:1,max:100,style:{width:"100%"},disabled:d})}),(0,t.jsx)(D.Form.Item,{name:"similarity_threshold",label:(0,t.jsxs)(eM.Space,{children:[(0,t.jsx)(f.Typography.Text,{strong:!0,children:"Similarity Threshold"}),(0,t.jsx)(g.Tooltip,{title:"Minimum similarity score (0-1) for a tool to be included",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{color:"#8c8c8c"}})})]}),children:(0,t.jsx)(tL.Slider,{min:0,max:1,step:.05,marks:{0:"0.0",.3:"0.3",.5:"0.5",.7:"0.7",1:"1.0"},disabled:d})})]}),(0,t.jsx)("div",{style:{display:"flex",justifyContent:"flex-end",gap:8},children:(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tR.SaveOutlined,{}),onClick:B,loading:d,disabled:!j,children:"Save Settings"})})]})}),(0,t.jsx)(tM.Col,{xs:24,lg:12,children:(0,t.jsx)(t$,{accessToken:e,testQuery:I,setTestQuery:P,testModel:O,setTestModel:M,isTesting:L,onTest:q,filterEnabled:!!U.enabled,testResult:F,curlCommand:`curl --location 'http://localhost:4000/v1/responses' \\ +>>>>>>>> origin/litellm_internal_staging:litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js --header 'Content-Type: application/json' \\ --header 'Authorization: Bearer sk-1234' \\ --data '{ @@ -88,4 +108,8 @@ } ], "tool_choice": "required" -}'`})})]})]})}):(0,t.jsx)("div",{className:"p-6 text-center text-gray-500",children:"Please log in to configure semantic filter settings."})}var tK=e.i(262218);let{Text:tW}=f.Typography,tJ=({accessToken:e})=>{let s,[r,l]=(0,b.useState)(!0),[a,n]=(0,b.useState)(!1),[i,o]=(0,b.useState)([]),[c,d]=(0,b.useState)(null);(0,b.useEffect)(()=>{m(),u()},[e]);let m=async()=>{if(e){l(!0);try{for(let t of(await (0,_.getGeneralSettingsCall)(e)))"mcp_internal_ip_ranges"===t.field_name&&t.field_value&&o(t.field_value)}catch(e){console.error("Failed to load MCP network settings:",e)}finally{l(!1)}}},u=async()=>{if(!e)return;let t=await (0,_.fetchMCPClientIp)(e);t&&d(t)},x=async()=>{if(e){n(!0);try{i.length>0?await (0,_.updateConfigFieldSetting)(e,"mcp_internal_ip_ranges",i):await (0,_.deleteConfigFieldSetting)(e,"mcp_internal_ip_ranges")}catch(e){console.error("Failed to save MCP network settings:",e)}finally{n(!1)}}};if(r)return(0,t.jsx)("div",{className:"flex justify-center py-12",children:(0,t.jsx)(W.Spin,{})});let h=c?4!==(s=c.split(".")).length?c+"/32":`${s[0]}.${s[1]}.${s[2]}.0/24`:null;return(0,t.jsxs)("div",{className:"space-y-6 p-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(tW,{className:"text-lg font-semibold",children:"Private IP Ranges"}),(0,t.jsx)("p",{className:"text-sm text-gray-500 mt-1",children:'Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. Callers from any other IP can only see servers marked "Available on Public Internet".'})]}),(0,t.jsxs)(e4.Card,{children:[c&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 rounded-lg",children:[(0,t.jsxs)(tW,{className:"text-sm text-blue-700",children:["Your current IP: ",(0,t.jsx)("span",{className:"font-mono font-medium",children:c})]}),h&&!i.includes(h)&&(0,t.jsxs)("div",{className:"mt-1",children:[(0,t.jsx)(tW,{className:"text-sm text-blue-600",children:"Suggested range: "}),(0,t.jsx)(tK.Tag,{className:"cursor-pointer font-mono",color:"blue",icon:(0,t.jsx)(eE.PlusOutlined,{}),onClick:()=>{!i.includes(h)&&o([...i,h])},children:h})]})]}),(0,t.jsx)("div",{className:"flex items-center mb-2",children:(0,t.jsx)(tW,{className:"font-medium",children:"Your Private Network Ranges"})}),(0,t.jsx)(p.Select,{mode:"tags",value:i,onChange:o,placeholder:"Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8",tokenSeparators:[","],className:"w-full",size:"large",allowClear:!0}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-2",children:"Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP ranges are used."})]}),(0,t.jsx)("div",{className:"flex justify-end",children:(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tR.SaveOutlined,{}),onClick:x,loading:a,children:"Save"})})]})},{Search:tY}=H.Input,{Text:tG}=f.Typography,tQ=["#3B82F6","#10B981","#F59E0B","#EF4444","#8B5CF6","#EC4899","#06B6D4","#84CC16"],tZ=({isVisible:e,onClose:s,onSelectServer:r,onCustomServer:l,accessToken:a})=>{let[n,i]=(0,b.useState)([]),[o,c]=(0,b.useState)([]),[d,m]=(0,b.useState)(!1),[u,x]=(0,b.useState)(null),[p,g]=(0,b.useState)(""),[f,j]=(0,b.useState)("All");(0,b.useEffect)(()=>{e&&a&&(m(!0),x(null),(0,_.fetchDiscoverableMCPServers)(a).then(e=>{i(e.servers||[]),c(e.categories||[])}).catch(e=>{x(e.message||"Failed to load MCP servers")}).finally(()=>{m(!1)}))},[e,a]),(0,b.useEffect)(()=>{e&&(g(""),j("All"))},[e]);let y=(0,b.useMemo)(()=>{let e=n;if("All"!==f&&(e=e.filter(e=>e.category===f)),p.trim()){let t=p.toLowerCase();e=e.filter(e=>e.name.toLowerCase().includes(t)||e.title.toLowerCase().includes(t)||e.description.toLowerCase().includes(t))}return e},[n,f,p]),v=(0,b.useMemo)(()=>{let e={};for(let t of y){let s=t.category||"Other";e[s]||(e[s]=[]),e[s].push(t)}return e},[y]);return(0,t.jsxs)(h.Modal,{title:(0,t.jsxs)("div",{className:"flex items-center justify-between pb-4 border-b border-gray-100",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-3",children:[(0,t.jsx)("img",{src:eZ,alt:"MCP Logo",className:"w-8 h-8 object-contain",style:{height:"20px",width:"20px",marginRight:"8px",objectFit:"contain"}}),(0,t.jsx)("h2",{className:"text-xl font-semibold text-gray-900",children:"Add MCP Server"})]}),(0,t.jsx)("button",{onClick:l,className:"text-sm text-blue-600 hover:text-blue-800 cursor-pointer bg-transparent border-none font-medium",children:"+ Custom Server"})]}),open:e,onCancel:s,footer:null,width:1e3,className:"top-8",styles:{body:{padding:"24px",maxHeight:"70vh",overflowY:"auto"},header:{padding:"24px 24px 0 24px",border:"none"}},children:[(0,t.jsx)("div",{style:{display:"flex",gap:6,flexWrap:"wrap",marginBottom:12},children:["All",...o].map(e=>{let s=f===e;return(0,t.jsx)("button",{onClick:()=>j(e),style:{padding:"4px 12px",borderRadius:4,border:s?"1px solid #111827":"1px solid #e5e7eb",background:s?"#111827":"#fff",color:s?"#fff":"#4b5563",cursor:"pointer",fontSize:12,fontWeight:s?500:400,lineHeight:"20px"},children:e},e)})}),(0,t.jsx)(tY,{placeholder:"Search servers...",value:p,onChange:e=>g(e.target.value),style:{marginBottom:16},allowClear:!0}),d&&(0,t.jsx)("div",{style:{display:"flex",flexDirection:"column",gap:4},children:Array.from({length:8}).map((e,s)=>(0,t.jsx)("div",{style:{height:36,borderRadius:6,background:"#f9fafb"}},s))}),u&&(0,t.jsx)("div",{style:{textAlign:"center",padding:"32px 0",color:"#9ca3af"},children:(0,t.jsxs)(tG,{children:["Failed to load servers: ",u]})}),!d&&!u&&0===y.length&&(0,t.jsx)("div",{style:{textAlign:"center",padding:"32px 0",color:"#9ca3af"},children:(0,t.jsxs)(tG,{children:["No servers found."," ",(0,t.jsx)("a",{onClick:l,style:{color:"#2563eb",cursor:"pointer"},children:"Add a custom server"})]})}),!d&&!u&&Object.entries(v).map(([e,s])=>(0,t.jsxs)("div",{style:{marginBottom:16},children:[(0,t.jsx)("div",{style:{fontSize:11,fontWeight:500,color:"#9ca3af",textTransform:"uppercase",letterSpacing:"0.05em",padding:"6px 0",borderBottom:"1px solid #f3f4f6",marginBottom:4},children:e}),(0,t.jsx)("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"0 16px"},children:s.map(e=>{var s;let l,a,n=(l=(s=e.title||e.name).charAt(0).toUpperCase(),a=s.split("").reduce((e,t)=>e+t.charCodeAt(0),0)%tQ.length,{initial:l,backgroundColor:tQ[a]});return(0,t.jsxs)("div",{onClick:()=>r(e),style:{display:"flex",alignItems:"center",padding:"8px 10px",borderRadius:6,cursor:"pointer",transition:"background 0.1s ease"},onMouseEnter:e=>{e.currentTarget.style.background="#f9fafb"},onMouseLeave:e=>{e.currentTarget.style.background="transparent"},children:[e.icon_url?(0,t.jsx)("img",{src:e.icon_url,alt:e.title,style:{width:20,height:20,objectFit:"contain",flexShrink:0,marginRight:12},onError:e=>{let t=e.currentTarget;t.style.display="none";let s=t.nextElementSibling;s&&(s.style.display="flex")}}):null,(0,t.jsx)("div",{style:{width:20,height:20,borderRadius:4,backgroundColor:n.backgroundColor,color:"#fff",display:e.icon_url?"none":"flex",alignItems:"center",justifyContent:"center",fontWeight:600,fontSize:11,flexShrink:0,marginRight:12},children:n.initial}),(0,t.jsx)("span",{style:{fontSize:14,fontWeight:400,color:"#111827",flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.title||e.name}),(0,t.jsx)("span",{style:{color:"#d1d5db",fontSize:14,flexShrink:0,marginLeft:8},children:"›"})]},e.name)})})]},e))]})};var tX=e.i(611052);let{Text:t0,Title:t2}=f.Typography,{Option:t1}=p.Select;e.s(["MCPServers",0,({accessToken:e,userRole:f,userID:N})=>{let{data:T,isLoading:k,refetch:A}=(0,j.useMCPServers)(),{data:I,isLoading:P,recheckServerHealth:O,recheckingServerIds:M}=(()=>{let{accessToken:e}=(0,w.default)(),t=(0,v.useQueryClient)(),[s,r]=(0,b.useState)(new Set),l=(0,y.useQuery)({queryKey:S.lists(),queryFn:async()=>await (0,_.fetchMCPServerHealth)(e),enabled:!!e,refetchInterval:3e4}),a=(0,b.useCallback)(async s=>{if(e){r(e=>new Set(e).add(s));try{let r=await (0,_.fetchMCPServerHealth)(e,[s]);t.setQueriesData({queryKey:S.lists()},e=>e?e.map(e=>r.find(t=>t.server_id===e.server_id)??e):r)}finally{r(e=>{let t=new Set(e);return t.delete(s),t})}}},[e,t]);return{...l,recheckServerHealth:a,recheckingServerIds:s}})(),F=(0,b.useMemo)(()=>{if(!T)return[];if(!I)return T;let e=new Map(I.map(e=>[e.server_id,e.status]));return T.map(t=>{let s=e.get(t.server_id);return{...t,status:s||t.status}})},[T,I]),[E,L]=(0,b.useState)(null),[R,z]=(0,b.useState)(!1),[U,B]=(0,b.useState)(null),[q,V]=(0,b.useState)(!1),[D,H]=(0,b.useState)("all"),[K,W]=(0,b.useState)("all"),[J,Y]=(0,b.useState)([]),[Q,X]=(0,b.useState)(!1),[ee,et]=(0,b.useState)(!1),[es,el]=(0,b.useState)(null),[ea,en]=(0,b.useState)(!1),[ei,eo]=(0,b.useState)(null),ec="Internal User"===f;(0,b.useEffect)(()=>{try{let e=(0,eG.getSecureItem)("litellm-mcp-oauth-edit-state");if(!e)return;let t=JSON.parse(e);t?.serverId&&(B(t.serverId),V(!0))}catch(e){console.error("Failed to restore MCP edit view state",e)}},[]);let ed=b.default.useMemo(()=>{if(!F)return[];let e=new Set,t=[];return F.forEach(s=>{s.teams&&s.teams.forEach(s=>{let r=s.team_id;e.has(r)||(e.add(r),t.push(s))})}),t},[F]),em=b.default.useMemo(()=>F?Array.from(new Set(F.flatMap(e=>e.mcp_access_groups).filter(e=>null!=e))):[],[F]),eu=(0,b.useCallback)((e,t)=>{if(!F)return Y([]);let s=F;"personal"===e?Y([]):("all"!==e&&(s=s.filter(t=>t.teams?.some(t=>t.team_id===e))),"all"!==t&&(s=s.filter(e=>e.mcp_access_groups?.some(e=>"string"==typeof e?e===t:e&&e.name===t))),Y([...s].sort((e,t)=>e.created_at||t.created_at?e.created_at?t.created_at?new Date(t.created_at).getTime()-new Date(e.created_at).getTime():-1:1:0)))},[F]);(0,b.useEffect)(()=>{eu(D,K)},[F,D,K,eu]);let ex=b.default.useMemo(()=>{let e,s,r,l;return e=e=>{B(e),V(!1)},s=e=>{B(e),V(!0)},r=eh,l=e=>eo(e),[{accessorKey:"server_id",header:"Server ID",enableSorting:!0,cell:({row:s})=>(0,t.jsxs)("button",{onClick:()=>e(s.original.server_id),className:"font-mono text-blue-600 bg-blue-50 hover:bg-blue-100 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 text-left truncate whitespace-nowrap cursor-pointer max-w-[15ch] transition-colors",children:[s.original.server_id.slice(0,7),"..."]})},{accessorKey:"server_name",header:"Name",enableSorting:!0,cell:({row:e})=>{let s=e.original.mcp_info?.logo_url,r=e.original.server_name;return(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[s?(0,t.jsx)("img",{src:s,alt:`${r??"MCP"} logo`,className:"h-5 w-5 rounded object-contain flex-shrink-0",onError:e=>{e.target.style.display="none"}}):null,(0,t.jsx)("span",{children:r})]})}},{accessorKey:"alias",header:"Alias",enableSorting:!0},{id:"url",header:"URL",cell:({row:e})=>{let s=e.original.url;if(!s)return(0,t.jsx)("span",{className:"text-gray-400",children:"—"});let{maskedUrl:r}=eH(s);return(0,t.jsx)("span",{className:"font-mono text-sm",children:r})}},{accessorKey:"transport",header:"Transport",enableSorting:!0,cell:({row:e})=>{let s=e.original.transport||"http",r=(e.original.spec_path&&"stdio"!==s?"OPENAPI":s).toUpperCase();return(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:r})}},{accessorKey:"auth_type",header:"Auth Type",enableSorting:!0,cell:({getValue:e})=>{let s=e()||"none";return(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:s})}},{id:"health_status",header:"Health Status",cell:({row:e})=>(0,t.jsx)(tu,{server:e.original,isLoadingHealth:P,isRechecking:M?.has(e.original.server_id),onRecheck:O})},{id:"mcp_access_groups",header:"Access Groups",cell:({row:e})=>{let s=e.original.mcp_access_groups;if(Array.isArray(s)&&s.length>0&&"string"==typeof s[0]){let e=s.join(", ");return(0,t.jsx)(g.Tooltip,{title:e,children:(0,t.jsxs)("div",{className:"flex items-center gap-1 max-w-[200px]",children:[(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200 truncate max-w-[140px]",children:s[0]}),s.length>1&&(0,t.jsxs)("span",{className:"text-xs text-gray-400 font-medium",children:["+",s.length-1]})]})})}return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"})}},{id:"available_on_public_internet",header:"Network Access",cell:({row:e})=>e.original.available_on_public_internet?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Public"]}):(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-orange-500"}),"Internal"]})},{header:"Created",accessorKey:"created_at",enableSorting:!0,sortingFn:"datetime",cell:({row:e})=>{let s=e.original;if(!s.created_at)return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"});let r=new Date(s.created_at);return(0,t.jsx)(g.Tooltip,{title:r.toLocaleString(),children:(0,t.jsx)("span",{className:"text-xs text-gray-600",children:r.toLocaleDateString()})})}},{header:"Updated",accessorKey:"updated_at",enableSorting:!0,sortingFn:"datetime",cell:({row:e})=>{let s=e.original;if(!s.updated_at)return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"});let r=new Date(s.updated_at);return(0,t.jsx)(g.Tooltip,{title:r.toLocaleString(),children:(0,t.jsx)("span",{className:"text-xs text-gray-600",children:r.toLocaleDateString()})})}},{id:"byok_credential",header:"Credential",cell:({row:e})=>{let s=e.original;return s.is_byok?s.has_user_credential?(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200",children:[(0,t.jsx)(tm.CheckOutlined,{style:{fontSize:10}})," Connected"]}),l&&(0,t.jsx)("button",{className:"text-xs text-gray-400 hover:text-blue-600 transition-colors",onClick:()=>l(s),children:"Update"})]}):l?(0,t.jsx)("button",{className:"text-xs bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md font-medium transition-colors shadow-sm",onClick:()=>l(s),children:"Connect"}):null:(0,t.jsx)("span",{className:"text-gray-300 text-xs",children:"—"})}},{id:"actions",header:"Actions",cell:({row:e})=>(0,t.jsxs)("div",{className:"flex items-center gap-1",children:[(0,t.jsx)(g.Tooltip,{title:"Edit",children:(0,t.jsx)("button",{onClick:()=>s(e.original.server_id),className:"p-1.5 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors",children:(0,t.jsx)(tc.Icon,{icon:td.PencilAltIcon,size:"sm"})})}),(0,t.jsx)(g.Tooltip,{title:"Delete",children:(0,t.jsx)("button",{onClick:()=>r(e.original.server_id),className:"p-1.5 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors",children:(0,t.jsx)(tc.Icon,{icon:G.TrashIcon,size:"sm"})})})]})}]},[f,P,O,M]);function eh(e){L(e),z(!0)}let ep=async()=>{if(null!=E&&null!=e)try{en(!0),await (0,_.deleteMCPServer)(e,E),C.default.success("Deleted MCP Server successfully"),A()}catch(e){console.error("Error deleting the mcp server:",e)}finally{en(!1),z(!1),L(null)}},eg=E?(T||[]).find(e=>e.server_id===E):null,ef=b.default.useMemo(()=>J.find(e=>e.server_id===U)||{server_id:"",server_name:"",alias:"",url:"",transport:"",auth_type:"",created_at:"",created_by:"",updated_at:"",updated_by:""},[J,U]),eb=b.default.useCallback(()=>{V(!1),B(null),A()},[A]);return e&&f&&N?(0,t.jsxs)("div",{className:"w-full h-full p-6",children:[(0,t.jsx)(h.Modal,{open:R,title:"Delete MCP Server?",onOk:ep,okText:ea?"Deleting...":"Delete",onCancel:()=>{z(!1),L(null)},cancelText:"Cancel",cancelButtonProps:{disabled:ea},okButtonProps:{danger:!0},confirmLoading:ea,children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsx)(t0,{className:"text-gray-600",children:"This action is permanent and cannot be undone. All associated configurations will be removed."}),eg&&(0,t.jsx)("div",{className:"mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200",children:(0,t.jsxs)(x.Descriptions,{column:1,size:"small",colon:!1,children:[eg.server_name&&(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"Name"}),children:(0,t.jsx)(t0,{strong:!0,className:"text-sm",children:eg.server_name})}),(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"ID"}),children:(0,t.jsx)(t0,{code:!0,className:"text-xs",children:eg.server_id})}),eg.url&&(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"URL"}),children:(0,t.jsx)(t0,{code:!0,className:"text-xs break-all",children:eg.url})})]})})]})}),(0,t.jsx)(e5,{userRole:f,accessToken:e,onCreateSuccess:e=>{Y(t=>[...t,e]),X(!1),A()},isModalVisible:Q,setModalVisible:X,availableAccessGroups:em,prefillData:es,onBackToDiscovery:()=>{X(!1),el(null),et(!0)}}),(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)(m.Title,{children:"MCP Servers"}),J.length>0&&(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 border border-gray-200",children:J.length})]}),(0,t.jsx)(d.Text,{className:"text-tremor-content mt-1",children:"Configure and manage your MCP servers"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,s.isAdminRole)(f)&&(0,t.jsx)(l.Button,{className:"flex-shrink-0",onClick:()=>et(!0),children:"+ Add New MCP Server"}),!(0,s.isAdminRole)(f)&&(0,t.jsx)(l.Button,{className:"flex-shrink-0",onClick:()=>{el(null),X(!0)},variant:"secondary",children:"+ Submit MCP Server"})]})]}),(0,t.jsx)(tZ,{isVisible:ee,onClose:()=>et(!1),onSelectServer:e=>{el(e),et(!1),X(!0)},onCustomServer:()=>{el(null),et(!1),X(!0)},accessToken:e}),(0,t.jsxs)(n.TabGroup,{className:"w-full h-full",children:[(0,t.jsx)(i.TabList,{className:"flex justify-between mt-2 w-full items-center",children:(0,t.jsxs)("div",{className:"flex",children:[(0,t.jsx)(a.Tab,{children:"All Servers"}),(0,t.jsx)(a.Tab,{children:"Toolsets"}),(0,t.jsx)(a.Tab,{children:"Connect"}),(0,t.jsx)(a.Tab,{children:"Semantic Filter"}),(0,t.jsx)(a.Tab,{children:"Network Settings"}),(0,s.isAdminRole)(f)&&(0,t.jsx)(a.Tab,{children:(0,t.jsxs)("span",{className:"flex items-center gap-2",children:["Submitted MCPs ",(0,t.jsx)(u.default,{})]})})]})}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsx)(o.TabPanel,{children:U?(0,t.jsx)(tI,{mcpServer:ef,onBack:eb,isProxyAdmin:(0,s.isAdminRole)(f),isEditing:q,accessToken:e,userID:N,userRole:f,availableAccessGroups:em},U):(0,t.jsxs)("div",{className:"w-full h-full",children:[(0,t.jsx)("div",{className:"w-full",children:(0,t.jsx)("div",{className:"flex flex-col space-y-4",children:(0,t.jsxs)("div",{className:"flex items-center gap-6 bg-white rounded-lg px-4 py-3 border border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-600 whitespace-nowrap",children:"Team"}),(0,t.jsxs)(p.Select,{value:D,onChange:e=>{H(e),eu(e,K)},style:{width:220},size:"middle",children:[(0,t.jsx)(t1,{value:"all",children:(0,t.jsx)("span",{className:"font-medium",children:ec?"All Available Servers":"All Servers"})}),(0,t.jsx)(t1,{value:"personal",children:(0,t.jsx)("span",{className:"font-medium",children:"Personal"})}),ed.map(e=>(0,t.jsx)(t1,{value:e.team_id,children:(0,t.jsx)("span",{className:"font-medium",children:e.team_alias||e.team_id})},e.team_id))]})]}),(0,t.jsx)("div",{className:"h-6 w-px bg-gray-200"}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsxs)(d.Text,{className:"text-sm font-medium text-gray-600 whitespace-nowrap",children:["Access Group",(0,t.jsx)(g.Tooltip,{title:"An MCP Access Group is a set of users or teams that have permission to access specific MCP servers. Use access groups to control and organize who can connect to which servers.",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{marginLeft:4,color:"#9ca3af"}})})]}),(0,t.jsxs)(p.Select,{value:K,onChange:e=>{W(e),eu(D,e)},style:{width:220},size:"middle",children:[(0,t.jsx)(t1,{value:"all",children:(0,t.jsx)("span",{className:"font-medium",children:"All Access Groups"})}),em.map(e=>(0,t.jsx)(t1,{value:e,children:(0,t.jsx)("span",{className:"font-medium",children:e})},e))]})]})]})})}),(0,t.jsx)("div",{className:"w-full mt-6",children:(0,t.jsx)(Z.DataTable,{data:J,columns:ex,renderSubComponent:()=>(0,t.jsx)("div",{}),getRowCanExpand:()=>!1,isLoading:k,noDataMessage:"No MCP servers configured. Click '+ Add New MCP Server' to get started.",loadingMessage:"Loading MCP servers...",enableSorting:!0})})]})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(er,{accessToken:e,userRole:f})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(to,{})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tH,{accessToken:e})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tJ,{accessToken:e})}),(0,s.isAdminRole)(f)&&(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)($,{accessToken:e})})]})]}),ei&&(0,t.jsx)(tX.ByokCredentialModal,{server:ei,open:!!ei,onClose:()=>eo(null),onSuccess:e=>{A(),eo(null)},accessToken:e||""})]}):(console.log("Missing required authentication parameters",{accessToken:e,userRole:f,userID:N}),(0,t.jsx)("div",{className:"p-6 text-center text-gray-500",children:"Missing required authentication parameters."}))}],280881)}]); \ No newline at end of file +<<<<<<<< HEAD:litellm/proxy/_experimental/out/_next/static/chunks/0279e5299e9f6e98.js +}'`})})]})]})}):(0,t.jsx)("div",{className:"p-6 text-center text-gray-500",children:"Please log in to configure semantic filter settings."})}var tK=e.i(262218);let{Text:tW}=f.Typography,tJ=({accessToken:e})=>{let s,[r,l]=(0,b.useState)(!0),[a,n]=(0,b.useState)(!1),[i,o]=(0,b.useState)([]),[c,d]=(0,b.useState)(null);(0,b.useEffect)(()=>{m(),u()},[e]);let m=async()=>{if(e){l(!0);try{for(let t of(await (0,_.getGeneralSettingsCall)(e)))"mcp_internal_ip_ranges"===t.field_name&&t.field_value&&o(t.field_value)}catch(e){console.error("Failed to load MCP network settings:",e)}finally{l(!1)}}},u=async()=>{if(!e)return;let t=await (0,_.fetchMCPClientIp)(e);t&&d(t)},x=async()=>{if(e){n(!0);try{i.length>0?await (0,_.updateConfigFieldSetting)(e,"mcp_internal_ip_ranges",i):await (0,_.deleteConfigFieldSetting)(e,"mcp_internal_ip_ranges")}catch(e){console.error("Failed to save MCP network settings:",e)}finally{n(!1)}}};if(r)return(0,t.jsx)("div",{className:"flex justify-center py-12",children:(0,t.jsx)(W.Spin,{})});let h=c?4!==(s=c.split(".")).length?c+"/32":`${s[0]}.${s[1]}.${s[2]}.0/24`:null;return(0,t.jsxs)("div",{className:"space-y-6 p-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(tW,{className:"text-lg font-semibold",children:"Private IP Ranges"}),(0,t.jsx)("p",{className:"text-sm text-gray-500 mt-1",children:'Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. Callers from any other IP can only see servers marked "Available on Public Internet".'})]}),(0,t.jsxs)(e4.Card,{children:[c&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 rounded-lg",children:[(0,t.jsxs)(tW,{className:"text-sm text-blue-700",children:["Your current IP: ",(0,t.jsx)("span",{className:"font-mono font-medium",children:c})]}),h&&!i.includes(h)&&(0,t.jsxs)("div",{className:"mt-1",children:[(0,t.jsx)(tW,{className:"text-sm text-blue-600",children:"Suggested range: "}),(0,t.jsx)(tK.Tag,{className:"cursor-pointer font-mono",color:"blue",icon:(0,t.jsx)(eE.PlusOutlined,{}),onClick:()=>{!i.includes(h)&&o([...i,h])},children:h})]})]}),(0,t.jsx)("div",{className:"flex items-center mb-2",children:(0,t.jsx)(tW,{className:"font-medium",children:"Your Private Network Ranges"})}),(0,t.jsx)(p.Select,{mode:"tags",value:i,onChange:o,placeholder:"Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8",tokenSeparators:[","],className:"w-full",size:"large",allowClear:!0}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-2",children:"Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP ranges are used."})]}),(0,t.jsx)("div",{className:"flex justify-end",children:(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tR.SaveOutlined,{}),onClick:x,loading:a,children:"Save"})})]})},{Search:tY}=H.Input,{Text:tG}=f.Typography,tQ=["#3B82F6","#10B981","#F59E0B","#EF4444","#8B5CF6","#EC4899","#06B6D4","#84CC16"],tZ=({isVisible:e,onClose:s,onSelectServer:r,onCustomServer:l,accessToken:a})=>{let[n,i]=(0,b.useState)([]),[o,c]=(0,b.useState)([]),[d,m]=(0,b.useState)(!1),[u,x]=(0,b.useState)(null),[p,g]=(0,b.useState)(""),[f,j]=(0,b.useState)("All");(0,b.useEffect)(()=>{e&&a&&(m(!0),x(null),(0,_.fetchDiscoverableMCPServers)(a).then(e=>{i(e.servers||[]),c(e.categories||[])}).catch(e=>{x(e.message||"Failed to load MCP servers")}).finally(()=>{m(!1)}))},[e,a]),(0,b.useEffect)(()=>{e&&(g(""),j("All"))},[e]);let y=(0,b.useMemo)(()=>{let e=n;if("All"!==f&&(e=e.filter(e=>e.category===f)),p.trim()){let t=p.toLowerCase();e=e.filter(e=>e.name.toLowerCase().includes(t)||e.title.toLowerCase().includes(t)||e.description.toLowerCase().includes(t))}return e},[n,f,p]),v=(0,b.useMemo)(()=>{let e={};for(let t of y){let s=t.category||"Other";e[s]||(e[s]=[]),e[s].push(t)}return e},[y]);return(0,t.jsxs)(h.Modal,{title:(0,t.jsxs)("div",{className:"flex items-center justify-between pb-4 border-b border-gray-100",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-3",children:[(0,t.jsx)("img",{src:eZ,alt:"MCP Logo",className:"w-8 h-8 object-contain",style:{height:"20px",width:"20px",marginRight:"8px",objectFit:"contain"}}),(0,t.jsx)("h2",{className:"text-xl font-semibold text-gray-900",children:"Add MCP Server"})]}),(0,t.jsx)("button",{onClick:l,className:"text-sm text-blue-600 hover:text-blue-800 cursor-pointer bg-transparent border-none font-medium",children:"+ Custom Server"})]}),open:e,onCancel:s,footer:null,width:1e3,className:"top-8",styles:{body:{padding:"24px",maxHeight:"70vh",overflowY:"auto"},header:{padding:"24px 24px 0 24px",border:"none"}},children:[(0,t.jsx)("div",{style:{display:"flex",gap:6,flexWrap:"wrap",marginBottom:12},children:["All",...o].map(e=>{let s=f===e;return(0,t.jsx)("button",{onClick:()=>j(e),style:{padding:"4px 12px",borderRadius:4,border:s?"1px solid #111827":"1px solid #e5e7eb",background:s?"#111827":"#fff",color:s?"#fff":"#4b5563",cursor:"pointer",fontSize:12,fontWeight:s?500:400,lineHeight:"20px"},children:e},e)})}),(0,t.jsx)(tY,{placeholder:"Search servers...",value:p,onChange:e=>g(e.target.value),style:{marginBottom:16},allowClear:!0}),d&&(0,t.jsx)("div",{style:{display:"flex",flexDirection:"column",gap:4},children:Array.from({length:8}).map((e,s)=>(0,t.jsx)("div",{style:{height:36,borderRadius:6,background:"#f9fafb"}},s))}),u&&(0,t.jsx)("div",{style:{textAlign:"center",padding:"32px 0",color:"#9ca3af"},children:(0,t.jsxs)(tG,{children:["Failed to load servers: ",u]})}),!d&&!u&&0===y.length&&(0,t.jsx)("div",{style:{textAlign:"center",padding:"32px 0",color:"#9ca3af"},children:(0,t.jsxs)(tG,{children:["No servers found."," ",(0,t.jsx)("a",{onClick:l,style:{color:"#2563eb",cursor:"pointer"},children:"Add a custom server"})]})}),!d&&!u&&Object.entries(v).map(([e,s])=>(0,t.jsxs)("div",{style:{marginBottom:16},children:[(0,t.jsx)("div",{style:{fontSize:11,fontWeight:500,color:"#9ca3af",textTransform:"uppercase",letterSpacing:"0.05em",padding:"6px 0",borderBottom:"1px solid #f3f4f6",marginBottom:4},children:e}),(0,t.jsx)("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"0 16px"},children:s.map(e=>{var s;let l,a,n=(l=(s=e.title||e.name).charAt(0).toUpperCase(),a=s.split("").reduce((e,t)=>e+t.charCodeAt(0),0)%tQ.length,{initial:l,backgroundColor:tQ[a]});return(0,t.jsxs)("div",{onClick:()=>r(e),style:{display:"flex",alignItems:"center",padding:"8px 10px",borderRadius:6,cursor:"pointer",transition:"background 0.1s ease"},onMouseEnter:e=>{e.currentTarget.style.background="#f9fafb"},onMouseLeave:e=>{e.currentTarget.style.background="transparent"},children:[e.icon_url?(0,t.jsx)("img",{src:e.icon_url,alt:e.title,style:{width:20,height:20,objectFit:"contain",flexShrink:0,marginRight:12},onError:e=>{let t=e.currentTarget;t.style.display="none";let s=t.nextElementSibling;s&&(s.style.display="flex")}}):null,(0,t.jsx)("div",{style:{width:20,height:20,borderRadius:4,backgroundColor:n.backgroundColor,color:"#fff",display:e.icon_url?"none":"flex",alignItems:"center",justifyContent:"center",fontWeight:600,fontSize:11,flexShrink:0,marginRight:12},children:n.initial}),(0,t.jsx)("span",{style:{fontSize:14,fontWeight:400,color:"#111827",flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.title||e.name}),(0,t.jsx)("span",{style:{color:"#d1d5db",fontSize:14,flexShrink:0,marginLeft:8},children:"›"})]},e.name)})})]},e))]})};var tX=e.i(611052);let{Text:t0,Title:t2}=f.Typography,{Option:t1}=p.Select;e.s(["MCPServers",0,({accessToken:e,userRole:f,userID:N})=>{let{data:T,isLoading:k,refetch:A}=(0,j.useMCPServers)(),{data:I,isLoading:P,recheckServerHealth:O,recheckingServerIds:M}=(()=>{let{accessToken:e}=(0,w.default)(),t=(0,v.useQueryClient)(),[s,r]=(0,b.useState)(new Set),l=(0,y.useQuery)({queryKey:S.lists(),queryFn:async()=>await (0,_.fetchMCPServerHealth)(e),enabled:!!e,refetchInterval:3e4}),a=(0,b.useCallback)(async s=>{if(e){r(e=>new Set(e).add(s));try{let r=await (0,_.fetchMCPServerHealth)(e,[s]);t.setQueriesData({queryKey:S.lists()},e=>e?e.map(e=>r.find(t=>t.server_id===e.server_id)??e):r)}finally{r(e=>{let t=new Set(e);return t.delete(s),t})}}},[e,t]);return{...l,recheckServerHealth:a,recheckingServerIds:s}})(),F=(0,b.useMemo)(()=>{if(!T)return[];if(!I)return T;let e=new Map(I.map(e=>[e.server_id,e.status]));return T.map(t=>{let s=e.get(t.server_id);return{...t,status:s||t.status}})},[T,I]),[E,L]=(0,b.useState)(null),[R,U]=(0,b.useState)(!1),[z,B]=(0,b.useState)(null),[q,V]=(0,b.useState)(!1),[D,H]=(0,b.useState)("all"),[K,W]=(0,b.useState)("all"),[J,Y]=(0,b.useState)([]),[Q,X]=(0,b.useState)(!1),[ee,et]=(0,b.useState)(!1),[es,el]=(0,b.useState)(null),[ea,en]=(0,b.useState)(!1),[ei,eo]=(0,b.useState)(null),ec="Internal User"===f;(0,b.useEffect)(()=>{try{let e=(0,eG.getSecureItem)("litellm-mcp-oauth-edit-state");if(!e)return;let t=JSON.parse(e);t?.serverId&&(B(t.serverId),V(!0))}catch(e){console.error("Failed to restore MCP edit view state",e)}},[]);let ed=b.default.useMemo(()=>{if(!F)return[];let e=new Set,t=[];return F.forEach(s=>{s.teams&&s.teams.forEach(s=>{let r=s.team_id;e.has(r)||(e.add(r),t.push(s))})}),t},[F]),em=b.default.useMemo(()=>F?Array.from(new Set(F.flatMap(e=>e.mcp_access_groups).filter(e=>null!=e))):[],[F]),eu=(0,b.useCallback)((e,t)=>{if(!F)return Y([]);let s=F;"personal"===e?Y([]):("all"!==e&&(s=s.filter(t=>t.teams?.some(t=>t.team_id===e))),"all"!==t&&(s=s.filter(e=>e.mcp_access_groups?.some(e=>"string"==typeof e?e===t:e&&e.name===t))),Y([...s].sort((e,t)=>e.created_at||t.created_at?e.created_at?t.created_at?new Date(t.created_at).getTime()-new Date(e.created_at).getTime():-1:1:0)))},[F]);(0,b.useEffect)(()=>{eu(D,K)},[F,D,K,eu]);let ex=b.default.useMemo(()=>{let e,s,r,l;return e=e=>{B(e),V(!1)},s=e=>{B(e),V(!0)},r=eh,l=e=>eo(e),[{accessorKey:"server_id",header:"Server ID",enableSorting:!0,cell:({row:s})=>(0,t.jsxs)("button",{onClick:()=>e(s.original.server_id),className:"font-mono text-blue-600 bg-blue-50 hover:bg-blue-100 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 text-left truncate whitespace-nowrap cursor-pointer max-w-[15ch] transition-colors",children:[s.original.server_id.slice(0,7),"..."]})},{accessorKey:"server_name",header:"Name",enableSorting:!0,cell:({row:e})=>{let s=e.original.mcp_info?.logo_url,r=e.original.server_name;return(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[s?(0,t.jsx)("img",{src:s,alt:`${r??"MCP"} logo`,className:"h-5 w-5 rounded object-contain flex-shrink-0",onError:e=>{e.target.style.display="none"}}):null,(0,t.jsx)("span",{children:r})]})}},{accessorKey:"alias",header:"Alias",enableSorting:!0},{id:"url",header:"URL",cell:({row:e})=>{let s=e.original.url;if(!s)return(0,t.jsx)("span",{className:"text-gray-400",children:"—"});let{maskedUrl:r}=eH(s);return(0,t.jsx)("span",{className:"font-mono text-sm",children:r})}},{accessorKey:"transport",header:"Transport",enableSorting:!0,cell:({row:e})=>{let s=e.original.transport||"http",r=(e.original.spec_path&&"stdio"!==s?"OPENAPI":s).toUpperCase();return(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:r})}},{accessorKey:"auth_type",header:"Auth Type",enableSorting:!0,cell:({getValue:e})=>{let s=e()||"none";return(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:s})}},{id:"health_status",header:"Health Status",cell:({row:e})=>(0,t.jsx)(tu,{server:e.original,isLoadingHealth:P,isRechecking:M?.has(e.original.server_id),onRecheck:O})},{id:"mcp_access_groups",header:"Access Groups",cell:({row:e})=>{let s=e.original.mcp_access_groups;if(Array.isArray(s)&&s.length>0&&"string"==typeof s[0]){let e=s.join(", ");return(0,t.jsx)(g.Tooltip,{title:e,children:(0,t.jsxs)("div",{className:"flex items-center gap-1 max-w-[200px]",children:[(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200 truncate max-w-[140px]",children:s[0]}),s.length>1&&(0,t.jsxs)("span",{className:"text-xs text-gray-400 font-medium",children:["+",s.length-1]})]})})}return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"})}},{id:"available_on_public_internet",header:"Network Access",cell:({row:e})=>e.original.available_on_public_internet?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Public"]}):(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-orange-500"}),"Internal"]})},{header:"Created",accessorKey:"created_at",enableSorting:!0,sortingFn:"datetime",cell:({row:e})=>{let s=e.original;if(!s.created_at)return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"});let r=new Date(s.created_at);return(0,t.jsx)(g.Tooltip,{title:r.toLocaleString(),children:(0,t.jsx)("span",{className:"text-xs text-gray-600",children:r.toLocaleDateString()})})}},{header:"Updated",accessorKey:"updated_at",enableSorting:!0,sortingFn:"datetime",cell:({row:e})=>{let s=e.original;if(!s.updated_at)return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"});let r=new Date(s.updated_at);return(0,t.jsx)(g.Tooltip,{title:r.toLocaleString(),children:(0,t.jsx)("span",{className:"text-xs text-gray-600",children:r.toLocaleDateString()})})}},{id:"byok_credential",header:"Credential",cell:({row:e})=>{let s=e.original;return s.is_byok?s.has_user_credential?(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200",children:[(0,t.jsx)(tm.CheckOutlined,{style:{fontSize:10}})," Connected"]}),l&&(0,t.jsx)("button",{className:"text-xs text-gray-400 hover:text-blue-600 transition-colors",onClick:()=>l(s),children:"Update"})]}):l?(0,t.jsx)("button",{className:"text-xs bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md font-medium transition-colors shadow-sm",onClick:()=>l(s),children:"Connect"}):null:(0,t.jsx)("span",{className:"text-gray-300 text-xs",children:"—"})}},{id:"actions",header:"Actions",cell:({row:e})=>(0,t.jsxs)("div",{className:"flex items-center gap-1",children:[(0,t.jsx)(g.Tooltip,{title:"Edit",children:(0,t.jsx)("button",{onClick:()=>s(e.original.server_id),className:"p-1.5 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors",children:(0,t.jsx)(tc.Icon,{icon:td.PencilAltIcon,size:"sm"})})}),(0,t.jsx)(g.Tooltip,{title:"Delete",children:(0,t.jsx)("button",{onClick:()=>r(e.original.server_id),className:"p-1.5 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors",children:(0,t.jsx)(tc.Icon,{icon:G.TrashIcon,size:"sm"})})})]})}]},[f,P,O,M]);function eh(e){L(e),U(!0)}let ep=async()=>{if(null!=E&&null!=e)try{en(!0),await (0,_.deleteMCPServer)(e,E),C.default.success("Deleted MCP Server successfully"),A()}catch(e){console.error("Error deleting the mcp server:",e)}finally{en(!1),U(!1),L(null)}},eg=E?(T||[]).find(e=>e.server_id===E):null,ef=b.default.useMemo(()=>J.find(e=>e.server_id===z)||{server_id:"",server_name:"",alias:"",url:"",transport:"",auth_type:"",created_at:"",created_by:"",updated_at:"",updated_by:""},[J,z]),eb=b.default.useCallback(()=>{V(!1),B(null),A()},[A]);return e&&f&&N?(0,t.jsxs)("div",{className:"w-full h-full p-6",children:[(0,t.jsx)(h.Modal,{open:R,title:"Delete MCP Server?",onOk:ep,okText:ea?"Deleting...":"Delete",onCancel:()=>{U(!1),L(null)},cancelText:"Cancel",cancelButtonProps:{disabled:ea},okButtonProps:{danger:!0},confirmLoading:ea,children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsx)(t0,{className:"text-gray-600",children:"This action is permanent and cannot be undone. All associated configurations will be removed."}),eg&&(0,t.jsx)("div",{className:"mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200",children:(0,t.jsxs)(x.Descriptions,{column:1,size:"small",colon:!1,children:[eg.server_name&&(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"Name"}),children:(0,t.jsx)(t0,{strong:!0,className:"text-sm",children:eg.server_name})}),(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"ID"}),children:(0,t.jsx)(t0,{code:!0,className:"text-xs",children:eg.server_id})}),eg.url&&(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"URL"}),children:(0,t.jsx)(t0,{code:!0,className:"text-xs break-all",children:eg.url})})]})})]})}),(0,t.jsx)(e5,{userRole:f,accessToken:e,onCreateSuccess:e=>{Y(t=>[...t,e]),X(!1),A()},isModalVisible:Q,setModalVisible:X,availableAccessGroups:em,prefillData:es,onBackToDiscovery:()=>{X(!1),el(null),et(!0)}}),(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)(m.Title,{children:"MCP Servers"}),J.length>0&&(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 border border-gray-200",children:J.length})]}),(0,t.jsx)(d.Text,{className:"text-tremor-content mt-1",children:"Configure and manage your MCP servers"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,s.isAdminRole)(f)&&(0,t.jsx)(l.Button,{className:"flex-shrink-0",onClick:()=>et(!0),children:"+ Add New MCP Server"}),!(0,s.isAdminRole)(f)&&(0,t.jsx)(l.Button,{className:"flex-shrink-0",onClick:()=>{el(null),X(!0)},variant:"secondary",children:"+ Submit MCP Server"})]})]}),(0,t.jsx)(tZ,{isVisible:ee,onClose:()=>et(!1),onSelectServer:e=>{el(e),et(!1),X(!0)},onCustomServer:()=>{el(null),et(!1),X(!0)},accessToken:e}),(0,t.jsxs)(n.TabGroup,{className:"w-full h-full",children:[(0,t.jsx)(i.TabList,{className:"flex justify-between mt-2 w-full items-center",children:(0,t.jsxs)("div",{className:"flex",children:[(0,t.jsx)(a.Tab,{children:"All Servers"}),(0,t.jsx)(a.Tab,{children:"Toolsets"}),(0,t.jsx)(a.Tab,{children:"Connect"}),(0,t.jsx)(a.Tab,{children:"Semantic Filter"}),(0,t.jsx)(a.Tab,{children:"Network Settings"}),(0,s.isAdminRole)(f)&&(0,t.jsx)(a.Tab,{children:(0,t.jsxs)("span",{className:"flex items-center gap-2",children:["Submitted MCPs ",(0,t.jsx)(u.default,{})]})})]})}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsx)(o.TabPanel,{children:z?(0,t.jsx)(tI,{mcpServer:ef,onBack:eb,isProxyAdmin:(0,s.isAdminRole)(f),isEditing:q,accessToken:e,userID:N,userRole:f,availableAccessGroups:em},z):(0,t.jsxs)("div",{className:"w-full h-full",children:[(0,t.jsx)("div",{className:"w-full",children:(0,t.jsx)("div",{className:"flex flex-col space-y-4",children:(0,t.jsxs)("div",{className:"flex items-center gap-6 bg-white rounded-lg px-4 py-3 border border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-600 whitespace-nowrap",children:"Team"}),(0,t.jsxs)(p.Select,{value:D,onChange:e=>{H(e),eu(e,K)},style:{width:220},size:"middle",children:[(0,t.jsx)(t1,{value:"all",children:(0,t.jsx)("span",{className:"font-medium",children:ec?"All Available Servers":"All Servers"})}),(0,t.jsx)(t1,{value:"personal",children:(0,t.jsx)("span",{className:"font-medium",children:"Personal"})}),ed.map(e=>(0,t.jsx)(t1,{value:e.team_id,children:(0,t.jsx)("span",{className:"font-medium",children:e.team_alias||e.team_id})},e.team_id))]})]}),(0,t.jsx)("div",{className:"h-6 w-px bg-gray-200"}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsxs)(d.Text,{className:"text-sm font-medium text-gray-600 whitespace-nowrap",children:["Access Group",(0,t.jsx)(g.Tooltip,{title:"An MCP Access Group is a set of users or teams that have permission to access specific MCP servers. Use access groups to control and organize who can connect to which servers.",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{marginLeft:4,color:"#9ca3af"}})})]}),(0,t.jsxs)(p.Select,{value:K,onChange:e=>{W(e),eu(D,e)},style:{width:220},size:"middle",children:[(0,t.jsx)(t1,{value:"all",children:(0,t.jsx)("span",{className:"font-medium",children:"All Access Groups"})}),em.map(e=>(0,t.jsx)(t1,{value:e,children:(0,t.jsx)("span",{className:"font-medium",children:e})},e))]})]})]})})}),(0,t.jsx)("div",{className:"w-full mt-6",children:(0,t.jsx)(Z.DataTable,{data:J,columns:ex,renderSubComponent:()=>(0,t.jsx)("div",{}),getRowCanExpand:()=>!1,isLoading:k,noDataMessage:"No MCP servers configured. Click '+ Add New MCP Server' to get started.",loadingMessage:"Loading MCP servers...",enableSorting:!0})})]})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(er,{accessToken:e,userRole:f})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(to,{})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tH,{accessToken:e})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tJ,{accessToken:e})}),(0,s.isAdminRole)(f)&&(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)($,{accessToken:e})})]})]}),ei&&(0,t.jsx)(tX.ByokCredentialModal,{server:ei,open:!!ei,onClose:()=>eo(null),onSuccess:e=>{A(),eo(null)},accessToken:e||""})]}):(console.log("Missing required authentication parameters",{accessToken:e,userRole:f,userID:N}),(0,t.jsx)("div",{className:"p-6 text-center text-gray-500",children:"Missing required authentication parameters."}))}],280881)}]); +======== +}'`})})]})]})}):(0,t.jsx)("div",{className:"p-6 text-center text-gray-500",children:"Please log in to configure semantic filter settings."})}var tK=e.i(262218);let{Text:tW}=f.Typography,tJ=({accessToken:e})=>{let s,[r,l]=(0,b.useState)(!0),[a,n]=(0,b.useState)(!1),[i,o]=(0,b.useState)([]),[c,d]=(0,b.useState)(null);(0,b.useEffect)(()=>{m(),u()},[e]);let m=async()=>{if(e){l(!0);try{for(let t of(await (0,_.getGeneralSettingsCall)(e)))"mcp_internal_ip_ranges"===t.field_name&&t.field_value&&o(t.field_value)}catch(e){console.error("Failed to load MCP network settings:",e)}finally{l(!1)}}},u=async()=>{if(!e)return;let t=await (0,_.fetchMCPClientIp)(e);t&&d(t)},x=async()=>{if(e){n(!0);try{i.length>0?await (0,_.updateConfigFieldSetting)(e,"mcp_internal_ip_ranges",i):await (0,_.deleteConfigFieldSetting)(e,"mcp_internal_ip_ranges")}catch(e){console.error("Failed to save MCP network settings:",e)}finally{n(!1)}}};if(r)return(0,t.jsx)("div",{className:"flex justify-center py-12",children:(0,t.jsx)(W.Spin,{})});let h=c?4!==(s=c.split(".")).length?c+"/32":`${s[0]}.${s[1]}.${s[2]}.0/24`:null;return(0,t.jsxs)("div",{className:"space-y-6 p-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(tW,{className:"text-lg font-semibold",children:"Private IP Ranges"}),(0,t.jsx)("p",{className:"text-sm text-gray-500 mt-1",children:'Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. Callers from any other IP can only see servers marked "Available on Public Internet".'})]}),(0,t.jsxs)(e4.Card,{children:[c&&(0,t.jsxs)("div",{className:"mb-4 p-3 bg-blue-50 rounded-lg",children:[(0,t.jsxs)(tW,{className:"text-sm text-blue-700",children:["Your current IP: ",(0,t.jsx)("span",{className:"font-mono font-medium",children:c})]}),h&&!i.includes(h)&&(0,t.jsxs)("div",{className:"mt-1",children:[(0,t.jsx)(tW,{className:"text-sm text-blue-600",children:"Suggested range: "}),(0,t.jsx)(tK.Tag,{className:"cursor-pointer font-mono",color:"blue",icon:(0,t.jsx)(eE.PlusOutlined,{}),onClick:()=>{!i.includes(h)&&o([...i,h])},children:h})]})]}),(0,t.jsx)("div",{className:"flex items-center mb-2",children:(0,t.jsx)(tW,{className:"font-medium",children:"Your Private Network Ranges"})}),(0,t.jsx)(p.Select,{mode:"tags",value:i,onChange:o,placeholder:"Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8",tokenSeparators:[","],className:"w-full",size:"large",allowClear:!0}),(0,t.jsx)("p",{className:"text-xs text-gray-400 mt-2",children:"Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP ranges are used."})]}),(0,t.jsx)("div",{className:"flex justify-end",children:(0,t.jsx)(eb.Button,{type:"primary",icon:(0,t.jsx)(tR.SaveOutlined,{}),onClick:x,loading:a,children:"Save"})})]})},{Search:tY}=H.Input,{Text:tG}=f.Typography,tQ=["#3B82F6","#10B981","#F59E0B","#EF4444","#8B5CF6","#EC4899","#06B6D4","#84CC16"],tZ=({isVisible:e,onClose:s,onSelectServer:r,onCustomServer:l,accessToken:a})=>{let[n,i]=(0,b.useState)([]),[o,c]=(0,b.useState)([]),[d,m]=(0,b.useState)(!1),[u,x]=(0,b.useState)(null),[p,g]=(0,b.useState)(""),[f,j]=(0,b.useState)("All");(0,b.useEffect)(()=>{e&&a&&(m(!0),x(null),(0,_.fetchDiscoverableMCPServers)(a).then(e=>{i(e.servers||[]),c(e.categories||[])}).catch(e=>{x(e.message||"Failed to load MCP servers")}).finally(()=>{m(!1)}))},[e,a]),(0,b.useEffect)(()=>{e&&(g(""),j("All"))},[e]);let y=(0,b.useMemo)(()=>{let e=n;if("All"!==f&&(e=e.filter(e=>e.category===f)),p.trim()){let t=p.toLowerCase();e=e.filter(e=>e.name.toLowerCase().includes(t)||e.title.toLowerCase().includes(t)||e.description.toLowerCase().includes(t))}return e},[n,f,p]),v=(0,b.useMemo)(()=>{let e={};for(let t of y){let s=t.category||"Other";e[s]||(e[s]=[]),e[s].push(t)}return e},[y]);return(0,t.jsxs)(h.Modal,{title:(0,t.jsxs)("div",{className:"flex items-center justify-between pb-4 border-b border-gray-100",children:[(0,t.jsxs)("div",{className:"flex items-center space-x-3",children:[(0,t.jsx)("img",{src:eZ,alt:"MCP Logo",className:"w-8 h-8 object-contain",style:{height:"20px",width:"20px",marginRight:"8px",objectFit:"contain"}}),(0,t.jsx)("h2",{className:"text-xl font-semibold text-gray-900",children:"Add MCP Server"})]}),(0,t.jsx)("button",{onClick:l,className:"text-sm text-blue-600 hover:text-blue-800 cursor-pointer bg-transparent border-none font-medium",children:"+ Custom Server"})]}),open:e,onCancel:s,footer:null,width:1e3,className:"top-8",styles:{body:{padding:"24px",maxHeight:"70vh",overflowY:"auto"},header:{padding:"24px 24px 0 24px",border:"none"}},children:[(0,t.jsx)("div",{style:{display:"flex",gap:6,flexWrap:"wrap",marginBottom:12},children:["All",...o].map(e=>{let s=f===e;return(0,t.jsx)("button",{onClick:()=>j(e),style:{padding:"4px 12px",borderRadius:4,border:s?"1px solid #111827":"1px solid #e5e7eb",background:s?"#111827":"#fff",color:s?"#fff":"#4b5563",cursor:"pointer",fontSize:12,fontWeight:s?500:400,lineHeight:"20px"},children:e},e)})}),(0,t.jsx)(tY,{placeholder:"Search servers...",value:p,onChange:e=>g(e.target.value),style:{marginBottom:16},allowClear:!0}),d&&(0,t.jsx)("div",{style:{display:"flex",flexDirection:"column",gap:4},children:Array.from({length:8}).map((e,s)=>(0,t.jsx)("div",{style:{height:36,borderRadius:6,background:"#f9fafb"}},s))}),u&&(0,t.jsx)("div",{style:{textAlign:"center",padding:"32px 0",color:"#9ca3af"},children:(0,t.jsxs)(tG,{children:["Failed to load servers: ",u]})}),!d&&!u&&0===y.length&&(0,t.jsx)("div",{style:{textAlign:"center",padding:"32px 0",color:"#9ca3af"},children:(0,t.jsxs)(tG,{children:["No servers found."," ",(0,t.jsx)("a",{onClick:l,style:{color:"#2563eb",cursor:"pointer"},children:"Add a custom server"})]})}),!d&&!u&&Object.entries(v).map(([e,s])=>(0,t.jsxs)("div",{style:{marginBottom:16},children:[(0,t.jsx)("div",{style:{fontSize:11,fontWeight:500,color:"#9ca3af",textTransform:"uppercase",letterSpacing:"0.05em",padding:"6px 0",borderBottom:"1px solid #f3f4f6",marginBottom:4},children:e}),(0,t.jsx)("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"0 16px"},children:s.map(e=>{var s;let l,a,n=(l=(s=e.title||e.name).charAt(0).toUpperCase(),a=s.split("").reduce((e,t)=>e+t.charCodeAt(0),0)%tQ.length,{initial:l,backgroundColor:tQ[a]});return(0,t.jsxs)("div",{onClick:()=>r(e),style:{display:"flex",alignItems:"center",padding:"8px 10px",borderRadius:6,cursor:"pointer",transition:"background 0.1s ease"},onMouseEnter:e=>{e.currentTarget.style.background="#f9fafb"},onMouseLeave:e=>{e.currentTarget.style.background="transparent"},children:[e.icon_url?(0,t.jsx)("img",{src:e.icon_url,alt:e.title,style:{width:20,height:20,objectFit:"contain",flexShrink:0,marginRight:12},onError:e=>{let t=e.currentTarget;t.style.display="none";let s=t.nextElementSibling;s&&(s.style.display="flex")}}):null,(0,t.jsx)("div",{style:{width:20,height:20,borderRadius:4,backgroundColor:n.backgroundColor,color:"#fff",display:e.icon_url?"none":"flex",alignItems:"center",justifyContent:"center",fontWeight:600,fontSize:11,flexShrink:0,marginRight:12},children:n.initial}),(0,t.jsx)("span",{style:{fontSize:14,fontWeight:400,color:"#111827",flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.title||e.name}),(0,t.jsx)("span",{style:{color:"#d1d5db",fontSize:14,flexShrink:0,marginLeft:8},children:"›"})]},e.name)})})]},e))]})};var tX=e.i(611052);let{Text:t0,Title:t2}=f.Typography,{Option:t1}=p.Select;e.s(["MCPServers",0,({accessToken:e,userRole:f,userID:N})=>{let{data:T,isLoading:k,refetch:A}=(0,j.useMCPServers)(),{data:I,isLoading:P,recheckServerHealth:O,recheckingServerIds:M}=(()=>{let{accessToken:e}=(0,w.default)(),t=(0,v.useQueryClient)(),[s,r]=(0,b.useState)(new Set),l=(0,y.useQuery)({queryKey:S.lists(),queryFn:async()=>await (0,_.fetchMCPServerHealth)(e),enabled:!!e,refetchInterval:3e4}),a=(0,b.useCallback)(async s=>{if(e){r(e=>new Set(e).add(s));try{let r=await (0,_.fetchMCPServerHealth)(e,[s]);t.setQueriesData({queryKey:S.lists()},e=>e?e.map(e=>r.find(t=>t.server_id===e.server_id)??e):r)}finally{r(e=>{let t=new Set(e);return t.delete(s),t})}}},[e,t]);return{...l,recheckServerHealth:a,recheckingServerIds:s}})(),F=(0,b.useMemo)(()=>{if(!T)return[];if(!I)return T;let e=new Map(I.map(e=>[e.server_id,e.status]));return T.map(t=>{let s=e.get(t.server_id);return{...t,status:s||t.status}})},[T,I]),[E,L]=(0,b.useState)(null),[R,z]=(0,b.useState)(!1),[U,B]=(0,b.useState)(null),[q,V]=(0,b.useState)(!1),[D,H]=(0,b.useState)("all"),[K,W]=(0,b.useState)("all"),[J,Y]=(0,b.useState)([]),[Q,X]=(0,b.useState)(!1),[ee,et]=(0,b.useState)(!1),[es,el]=(0,b.useState)(null),[ea,en]=(0,b.useState)(!1),[ei,eo]=(0,b.useState)(null),ec="Internal User"===f;(0,b.useEffect)(()=>{try{let e=(0,eG.getSecureItem)("litellm-mcp-oauth-edit-state");if(!e)return;let t=JSON.parse(e);t?.serverId&&(B(t.serverId),V(!0))}catch(e){console.error("Failed to restore MCP edit view state",e)}},[]);let ed=b.default.useMemo(()=>{if(!F)return[];let e=new Set,t=[];return F.forEach(s=>{s.teams&&s.teams.forEach(s=>{let r=s.team_id;e.has(r)||(e.add(r),t.push(s))})}),t},[F]),em=b.default.useMemo(()=>F?Array.from(new Set(F.flatMap(e=>e.mcp_access_groups).filter(e=>null!=e))):[],[F]),eu=(0,b.useCallback)((e,t)=>{if(!F)return Y([]);let s=F;"personal"===e?Y([]):("all"!==e&&(s=s.filter(t=>t.teams?.some(t=>t.team_id===e))),"all"!==t&&(s=s.filter(e=>e.mcp_access_groups?.some(e=>"string"==typeof e?e===t:e&&e.name===t))),Y([...s].sort((e,t)=>e.created_at||t.created_at?e.created_at?t.created_at?new Date(t.created_at).getTime()-new Date(e.created_at).getTime():-1:1:0)))},[F]);(0,b.useEffect)(()=>{eu(D,K)},[F,D,K,eu]);let ex=b.default.useMemo(()=>{let e,s,r,l;return e=e=>{B(e),V(!1)},s=e=>{B(e),V(!0)},r=eh,l=e=>eo(e),[{accessorKey:"server_id",header:"Server ID",enableSorting:!0,cell:({row:s})=>(0,t.jsxs)("button",{onClick:()=>e(s.original.server_id),className:"font-mono text-blue-600 bg-blue-50 hover:bg-blue-100 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 text-left truncate whitespace-nowrap cursor-pointer max-w-[15ch] transition-colors",children:[s.original.server_id.slice(0,7),"..."]})},{accessorKey:"server_name",header:"Name",enableSorting:!0,cell:({row:e})=>{let s=e.original.mcp_info?.logo_url,r=e.original.server_name;return(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[s?(0,t.jsx)("img",{src:s,alt:`${r??"MCP"} logo`,className:"h-5 w-5 rounded object-contain flex-shrink-0",onError:e=>{e.target.style.display="none"}}):null,(0,t.jsx)("span",{children:r})]})}},{accessorKey:"alias",header:"Alias",enableSorting:!0},{id:"url",header:"URL",cell:({row:e})=>{let s=e.original.url;if(!s)return(0,t.jsx)("span",{className:"text-gray-400",children:"—"});let{maskedUrl:r}=eH(s);return(0,t.jsx)("span",{className:"font-mono text-sm",children:r})}},{accessorKey:"transport",header:"Transport",enableSorting:!0,cell:({row:e})=>{let s=e.original.transport||"http",r=(e.original.spec_path&&"stdio"!==s?"OPENAPI":s).toUpperCase();return(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:r})}},{accessorKey:"auth_type",header:"Auth Type",enableSorting:!0,cell:({getValue:e})=>{let s=e()||"none";return(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200",children:s})}},{id:"health_status",header:"Health Status",cell:({row:e})=>(0,t.jsx)(tu,{server:e.original,isLoadingHealth:P,isRechecking:M?.has(e.original.server_id),onRecheck:O})},{id:"mcp_access_groups",header:"Access Groups",cell:({row:e})=>{let s=e.original.mcp_access_groups;if(Array.isArray(s)&&s.length>0&&"string"==typeof s[0]){let e=s.join(", ");return(0,t.jsx)(g.Tooltip,{title:e,children:(0,t.jsxs)("div",{className:"flex items-center gap-1 max-w-[200px]",children:[(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200 truncate max-w-[140px]",children:s[0]}),s.length>1&&(0,t.jsxs)("span",{className:"text-xs text-gray-400 font-medium",children:["+",s.length-1]})]})})}return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"})}},{id:"available_on_public_internet",header:"Network Access",cell:({row:e})=>e.original.available_on_public_internet?(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-green-500"}),"Public"]}):(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium",children:[(0,t.jsx)("span",{className:"h-1.5 w-1.5 rounded-full bg-orange-500"}),"Internal"]})},{header:"Created",accessorKey:"created_at",enableSorting:!0,sortingFn:"datetime",cell:({row:e})=>{let s=e.original;if(!s.created_at)return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"});let r=new Date(s.created_at);return(0,t.jsx)(g.Tooltip,{title:r.toLocaleString(),children:(0,t.jsx)("span",{className:"text-xs text-gray-600",children:r.toLocaleDateString()})})}},{header:"Updated",accessorKey:"updated_at",enableSorting:!0,sortingFn:"datetime",cell:({row:e})=>{let s=e.original;if(!s.updated_at)return(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"—"});let r=new Date(s.updated_at);return(0,t.jsx)(g.Tooltip,{title:r.toLocaleString(),children:(0,t.jsx)("span",{className:"text-xs text-gray-600",children:r.toLocaleDateString()})})}},{id:"byok_credential",header:"Credential",cell:({row:e})=>{let s=e.original;return s.is_byok?s.has_user_credential?(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsxs)("span",{className:"inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200",children:[(0,t.jsx)(tm.CheckOutlined,{style:{fontSize:10}})," Connected"]}),l&&(0,t.jsx)("button",{className:"text-xs text-gray-400 hover:text-blue-600 transition-colors",onClick:()=>l(s),children:"Update"})]}):l?(0,t.jsx)("button",{className:"text-xs bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md font-medium transition-colors shadow-sm",onClick:()=>l(s),children:"Connect"}):null:(0,t.jsx)("span",{className:"text-gray-300 text-xs",children:"—"})}},{id:"actions",header:"Actions",cell:({row:e})=>(0,t.jsxs)("div",{className:"flex items-center gap-1",children:[(0,t.jsx)(g.Tooltip,{title:"Edit",children:(0,t.jsx)("button",{onClick:()=>s(e.original.server_id),className:"p-1.5 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors",children:(0,t.jsx)(tc.Icon,{icon:td.PencilAltIcon,size:"sm"})})}),(0,t.jsx)(g.Tooltip,{title:"Delete",children:(0,t.jsx)("button",{onClick:()=>r(e.original.server_id),className:"p-1.5 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors",children:(0,t.jsx)(tc.Icon,{icon:G.TrashIcon,size:"sm"})})})]})}]},[f,P,O,M]);function eh(e){L(e),z(!0)}let ep=async()=>{if(null!=E&&null!=e)try{en(!0),await (0,_.deleteMCPServer)(e,E),C.default.success("Deleted MCP Server successfully"),A()}catch(e){console.error("Error deleting the mcp server:",e)}finally{en(!1),z(!1),L(null)}},eg=E?(T||[]).find(e=>e.server_id===E):null,ef=b.default.useMemo(()=>J.find(e=>e.server_id===U)||{server_id:"",server_name:"",alias:"",url:"",transport:"",auth_type:"",created_at:"",created_by:"",updated_at:"",updated_by:""},[J,U]),eb=b.default.useCallback(()=>{V(!1),B(null),A()},[A]);return e&&f&&N?(0,t.jsxs)("div",{className:"w-full h-full p-6",children:[(0,t.jsx)(h.Modal,{open:R,title:"Delete MCP Server?",onOk:ep,okText:ea?"Deleting...":"Delete",onCancel:()=>{z(!1),L(null)},cancelText:"Cancel",cancelButtonProps:{disabled:ea},okButtonProps:{danger:!0},confirmLoading:ea,children:(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsx)(t0,{className:"text-gray-600",children:"This action is permanent and cannot be undone. All associated configurations will be removed."}),eg&&(0,t.jsx)("div",{className:"mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200",children:(0,t.jsxs)(x.Descriptions,{column:1,size:"small",colon:!1,children:[eg.server_name&&(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"Name"}),children:(0,t.jsx)(t0,{strong:!0,className:"text-sm",children:eg.server_name})}),(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"ID"}),children:(0,t.jsx)(t0,{code:!0,className:"text-xs",children:eg.server_id})}),eg.url&&(0,t.jsx)(x.Descriptions.Item,{label:(0,t.jsx)("span",{className:"text-gray-500 text-sm",children:"URL"}),children:(0,t.jsx)(t0,{code:!0,className:"text-xs break-all",children:eg.url})})]})})]})}),(0,t.jsx)(e5,{userRole:f,accessToken:e,onCreateSuccess:e=>{Y(t=>[...t,e]),X(!1),A()},isModalVisible:Q,setModalVisible:X,availableAccessGroups:em,prefillData:es,onBackToDiscovery:()=>{X(!1),el(null),et(!0)}}),(0,t.jsxs)("div",{className:"flex items-center justify-between",children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)(m.Title,{children:"MCP Servers"}),J.length>0&&(0,t.jsx)("span",{className:"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 border border-gray-200",children:J.length})]}),(0,t.jsx)(d.Text,{className:"text-tremor-content mt-1",children:"Configure and manage your MCP servers"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,s.isAdminRole)(f)&&(0,t.jsx)(l.Button,{className:"flex-shrink-0",onClick:()=>et(!0),children:"+ Add New MCP Server"}),!(0,s.isAdminRole)(f)&&(0,t.jsx)(l.Button,{className:"flex-shrink-0",onClick:()=>{el(null),X(!0)},variant:"secondary",children:"+ Submit MCP Server"})]})]}),(0,t.jsx)(tZ,{isVisible:ee,onClose:()=>et(!1),onSelectServer:e=>{el(e),et(!1),X(!0)},onCustomServer:()=>{el(null),et(!1),X(!0)},accessToken:e}),(0,t.jsxs)(n.TabGroup,{className:"w-full h-full",children:[(0,t.jsx)(i.TabList,{className:"flex justify-between mt-2 w-full items-center",children:(0,t.jsxs)("div",{className:"flex",children:[(0,t.jsx)(a.Tab,{children:"All Servers"}),(0,t.jsx)(a.Tab,{children:"Toolsets"}),(0,t.jsx)(a.Tab,{children:"Connect"}),(0,t.jsx)(a.Tab,{children:"Semantic Filter"}),(0,t.jsx)(a.Tab,{children:"Network Settings"}),(0,s.isAdminRole)(f)&&(0,t.jsx)(a.Tab,{children:(0,t.jsxs)("span",{className:"flex items-center gap-2",children:["Submitted MCPs ",(0,t.jsx)(u.default,{})]})})]})}),(0,t.jsxs)(c.TabPanels,{children:[(0,t.jsx)(o.TabPanel,{children:U?(0,t.jsx)(tI,{mcpServer:ef,onBack:eb,isProxyAdmin:(0,s.isAdminRole)(f),isEditing:q,accessToken:e,userID:N,userRole:f,availableAccessGroups:em},U):(0,t.jsxs)("div",{className:"w-full h-full",children:[(0,t.jsx)("div",{className:"w-full",children:(0,t.jsx)("div",{className:"flex flex-col space-y-4",children:(0,t.jsxs)("div",{className:"flex items-center gap-6 bg-white rounded-lg px-4 py-3 border border-gray-200",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)(d.Text,{className:"text-sm font-medium text-gray-600 whitespace-nowrap",children:"Team"}),(0,t.jsxs)(p.Select,{value:D,onChange:e=>{H(e),eu(e,K)},style:{width:220},size:"middle",children:[(0,t.jsx)(t1,{value:"all",children:(0,t.jsx)("span",{className:"font-medium",children:ec?"All Available Servers":"All Servers"})}),(0,t.jsx)(t1,{value:"personal",children:(0,t.jsx)("span",{className:"font-medium",children:"Personal"})}),ed.map(e=>(0,t.jsx)(t1,{value:e.team_id,children:(0,t.jsx)("span",{className:"font-medium",children:e.team_alias||e.team_id})},e.team_id))]})]}),(0,t.jsx)("div",{className:"h-6 w-px bg-gray-200"}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsxs)(d.Text,{className:"text-sm font-medium text-gray-600 whitespace-nowrap",children:["Access Group",(0,t.jsx)(g.Tooltip,{title:"An MCP Access Group is a set of users or teams that have permission to access specific MCP servers. Use access groups to control and organize who can connect to which servers.",children:(0,t.jsx)(r.QuestionCircleOutlined,{style:{marginLeft:4,color:"#9ca3af"}})})]}),(0,t.jsxs)(p.Select,{value:K,onChange:e=>{W(e),eu(D,e)},style:{width:220},size:"middle",children:[(0,t.jsx)(t1,{value:"all",children:(0,t.jsx)("span",{className:"font-medium",children:"All Access Groups"})}),em.map(e=>(0,t.jsx)(t1,{value:e,children:(0,t.jsx)("span",{className:"font-medium",children:e})},e))]})]})]})})}),(0,t.jsx)("div",{className:"w-full mt-6",children:(0,t.jsx)(Z.DataTable,{data:J,columns:ex,renderSubComponent:()=>(0,t.jsx)("div",{}),getRowCanExpand:()=>!1,isLoading:k,noDataMessage:"No MCP servers configured. Click '+ Add New MCP Server' to get started.",loadingMessage:"Loading MCP servers...",enableSorting:!0})})]})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(er,{accessToken:e,userRole:f})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(to,{})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tH,{accessToken:e})}),(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)(tJ,{accessToken:e})}),(0,s.isAdminRole)(f)&&(0,t.jsx)(o.TabPanel,{children:(0,t.jsx)($,{accessToken:e})})]})]}),ei&&(0,t.jsx)(tX.ByokCredentialModal,{server:ei,open:!!ei,onClose:()=>eo(null),onSuccess:e=>{A(),eo(null)},accessToken:e||""})]}):(console.log("Missing required authentication parameters",{accessToken:e,userRole:f,userID:N}),(0,t.jsx)("div",{className:"p-6 text-center text-gray-500",children:"Missing required authentication parameters."}))}],280881)}]); +>>>>>>>> origin/litellm_internal_staging:litellm/proxy/_experimental/out/_next/static/chunks/e1a670efcb966aaa.js 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/chat.html b/litellm/proxy/_experimental/out/chat/index.html similarity index 100% rename from litellm/proxy/_experimental/out/chat.html rename to litellm/proxy/_experimental/out/chat/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/skills.html b/litellm/proxy/_experimental/out/skills/index.html similarity index 100% rename from litellm/proxy/_experimental/out/skills.html rename to litellm/proxy/_experimental/out/skills/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/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index eea6974193f..27cdc483d4a 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -3171,7 +3171,7 @@ ] }, "post": { - "description": "Create a new agent\n\nExample Request:\n```bash\ncurl -X POST \"http://localhost:4000/agents\" \\\n -H \"Authorization: Bearer \" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"agent\": {\n \"agent_name\": \"my-custom-agent\",\n \"agent_card_params\": {\n \"protocolVersion\": \"1.0\",\n \"name\": \"Hello World Agent\",\n \"description\": \"Just a hello world agent\",\n \"url\": \"http://localhost:9999/\",\n \"version\": \"1.0.0\",\n \"defaultInputModes\": [\"text\"],\n \"defaultOutputModes\": [\"text\"],\n \"capabilities\": {\n \"streaming\": true\n },\n \"skills\": [\n {\n \"id\": \"hello_world\",\n \"name\": \"Returns hello world\",\n \"description\": \"just returns hello world\",\n \"tags\": [\"hello world\"],\n \"examples\": [\"hi\", \"hello world\"]\n }\n ]\n },\n \"litellm_params\": {\n \"make_public\": true\n }\n }\n }'\n```", + "description": "Create a new agent\n\nExample Request:\n```bash\ncurl -X POST \"http://localhost:4000/v1/agents\" \\\n -H \"Authorization: Bearer \" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"agent_name\": \"my-custom-agent\",\n \"agent_card_params\": {\n \"protocolVersion\": \"1.0\",\n \"name\": \"Hello World Agent\",\n \"description\": \"Just a hello world agent\",\n \"url\": \"http://localhost:9999/\",\n \"version\": \"1.0.0\",\n \"defaultInputModes\": [\"text\"],\n \"defaultOutputModes\": [\"text\"],\n \"capabilities\": {\n \"streaming\": true\n },\n \"skills\": [\n {\n \"id\": \"hello_world\",\n \"name\": \"Returns hello world\",\n \"description\": \"just returns hello world\",\n \"tags\": [\"hello world\"],\n \"examples\": [\"hi\", \"hello world\"]\n }\n ]\n },\n \"litellm_params\": {\n \"make_public\": true\n }\n }'\n```", "operationId": "create_agent_v1_agents_post", "requestBody": { "content": { diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 290324d94b5..9fd2e76c772 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2361,6 +2361,30 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): database_connection_timeout: Optional[float] = Field( 60, description="default timeout for a connection to the database" ) + database_connect_timeout: Optional[float] = Field( + None, + description=( + "Prisma `connect_timeout` URL param (seconds). Bounds how long the " + "engine waits to establish a new connection before failing. Defaults " + "to Prisma's built-in value when unset." + ), + ) + database_socket_timeout: Optional[float] = Field( + None, + description=( + "Prisma `socket_timeout` URL param (seconds). When set, an idle/slow " + "connection that has not produced data within this window is closed. " + "This is the main knob for capping idle DB connections from LiteLLM." + ), + ) + database_extra_connection_params: Optional[Dict[str, Any]] = Field( + None, + description=( + "Escape hatch: extra key/value pairs appended verbatim to the Prisma " + "DATABASE_URL / DIRECT_URL query string (e.g. `sslmode`, `pgbouncer`, " + "`statement_cache_size`). Keys here override any default LiteLLM sets." + ), + ) database_type: Optional[Literal["dynamo_db"]] = Field( None, description="to use dynamodb instead of postgres db" ) diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index bf76f99db69..dea79d84250 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -4,13 +4,17 @@ from typing import Dict, List, Optional, Set import litellm from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.proxy._types import SpecialModelNames, UserAPIKeyAuth from litellm.router import Router from litellm.router_utils.fallback_event_handlers import get_fallback_model_group -from litellm.types.router import LiteLLM_Params +from litellm.types.router import CredentialLiteLLMParams, LiteLLM_Params from litellm.utils import get_valid_models +_CREDENTIAL_LITELLM_PARAM_FIELDS = set(CredentialLiteLLMParams.model_fields) + + def _check_wildcard_routing(model: str) -> bool: """ Returns True if a model is a provider wildcard. @@ -178,6 +182,7 @@ def get_complete_model_list( model_access_groups: Dict[str, List[str]] = {}, include_model_access_groups: Optional[bool] = False, only_model_access_groups: Optional[bool] = False, + team_id: Optional[str] = None, ) -> List[str]: """Logic for returning complete model list for a given key + team pair""" @@ -222,6 +227,7 @@ def get_complete_model_list( unique_models=unique_models, return_wildcard_routes=return_wildcard_routes, llm_router=llm_router, + team_id=team_id, ) complete_model_list = unique_models + all_wildcard_models @@ -229,6 +235,29 @@ def get_complete_model_list( return complete_model_list +def _hydrate_litellm_credential_name( + litellm_params: Optional[LiteLLM_Params], +) -> Optional[LiteLLM_Params]: + if litellm_params is None or litellm_params.litellm_credential_name is None: + return litellm_params + + credential_values = CredentialAccessor.get_credential_values( + litellm_params.litellm_credential_name + ) + if not credential_values: + return litellm_params + + litellm_params = litellm_params.model_copy() + for key, value in credential_values.items(): + if ( + key in _CREDENTIAL_LITELLM_PARAM_FIELDS + and getattr(litellm_params, key, None) is None + ): + setattr(litellm_params, key, value) + litellm_params.litellm_credential_name = None + return litellm_params + + def get_known_models_from_wildcard( wildcard_model: str, litellm_params: Optional[LiteLLM_Params] = None ) -> List[str]: @@ -247,7 +276,7 @@ def get_known_models_from_wildcard( else: provider = wildcard_provider_prefix - # get all known provider models + litellm_params = _hydrate_litellm_credential_name(litellm_params) wildcard_models = get_provider_models( provider=provider, litellm_params=litellm_params @@ -285,6 +314,7 @@ def _get_wildcard_models( unique_models: List[str], return_wildcard_routes: Optional[bool] = False, llm_router: Optional[Router] = None, + team_id: Optional[str] = None, ) -> List[str]: models_to_remove = set() all_wildcard_models = [] @@ -297,7 +327,9 @@ def _get_wildcard_models( ## get litellm params from model if llm_router is not None: - model_list = llm_router.get_model_list(model_name=model) + model_list = llm_router.get_model_list( + model_name=model, team_id=team_id + ) if model_list: for router_model in model_list: wildcard_models = get_known_models_from_wildcard( diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 30b5d36e14a..0cca9414b2a 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -12,7 +12,7 @@ import fnmatch import re import secrets from datetime import datetime, timezone -from typing import Any, Iterator, List, Optional, Tuple, Union, cast +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast import fastapi from fastapi import HTTPException, Request, WebSocket, status @@ -333,8 +333,22 @@ def _apply_budget_limits_to_end_user_params( async def user_api_key_auth_websocket(websocket: WebSocket): # Accept the WebSocket connection - scope_headers = list(websocket.scope.get("headers") or []) - request = Request(scope={"type": "http", "headers": scope_headers}) + ws_scope = websocket.scope or {} + scope_headers = list(ws_scope.get("headers") or []) + # ``get_request_route`` falls back to ``request.url.path`` when + # ``scope["path"]`` is absent. On WebSockets that fallback reads + # ``websocket.url``, which Starlette reconstructs from the (poisonable) + # Host header. Carry the ASGI scope's path / root_path so the lookup + # never reaches the fallback. + synthetic_scope: Dict[str, Any] = { + "type": "http", + "headers": scope_headers, + "path": ws_scope.get("path", ""), + } + for key in ("root_path", "app_root_path"): + if key in ws_scope: + synthetic_scope[key] = ws_scope[key] + request = Request(scope=synthetic_scope) request._url = websocket.url diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 935b96a0e39..166ef7a66d0 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -523,6 +523,10 @@ async def retrieve_batch( # noqa: PLR0915 custom_llm_provider=custom_llm_provider, **data # type: ignore ) + response = await proxy_logging_obj.post_call_success_hook( + data=data, user_api_key_dict=user_api_key_dict, response=response + ) + # FIX: Update the database with the latest state from provider await update_batch_in_database( batch_id=batch_id, @@ -533,19 +537,9 @@ async def retrieve_batch( # noqa: PLR0915 verbose_proxy_logger=verbose_proxy_logger, db_batch_object=db_batch_object, operation="retrieve", + user_api_key_dict=user_api_key_dict, ) - ### CALL HOOKS ### - modify outgoing data - response = await proxy_logging_obj.post_call_success_hook( - data=data, user_api_key_dict=user_api_key_dict, response=response - ) - - # Fix: bug_feb14_batch_retrieve_returns_raw_input_file_id - # Resolve raw provider file IDs (input, output, error) to unified IDs. - if unified_batch_id: - await resolve_input_file_id_to_unified(response, prisma_client) - await resolve_output_file_ids_to_unified(response, prisma_client) - ### ALERTING ### asyncio.create_task( proxy_logging_obj.update_request_status( @@ -917,10 +911,14 @@ async def cancel_batch( **_cancel_batch_data, ) - # FIX: Update the database with the new cancelled state managed_files_obj = proxy_logging_obj.get_proxy_hook("managed_files") from litellm.proxy.proxy_server import prisma_client + response = await proxy_logging_obj.post_call_success_hook( + data=data, user_api_key_dict=user_api_key_dict, response=response + ) + + # FIX: Update the database with the new cancelled state await update_batch_in_database( batch_id=batch_id, unified_batch_id=unified_batch_id, @@ -929,11 +927,7 @@ async def cancel_batch( prisma_client=prisma_client, verbose_proxy_logger=verbose_proxy_logger, operation="cancel", - ) - - ### CALL HOOKS ### - modify outgoing data - response = await proxy_logging_obj.post_call_success_hook( - data=data, user_api_key_dict=user_api_key_dict, response=response + user_api_key_dict=user_api_key_dict, ) ### ALERTING ### diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 71abdfa5e9e..fecfc1b4714 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -13,6 +13,34 @@ from litellm.proxy.common_utils.callback_utils import ( from litellm.types.router import Deployment +_FORM_CONTENT_TYPES: frozenset[str] = frozenset( + {"application/x-www-form-urlencoded", "multipart/form-data"} +) + + +def _normalize_media_type(content_type: str) -> str: + """Return the bare media type per RFC 7231: strip params, trim, lowercase.""" + if not content_type: + return "" + return content_type.split(";", 1)[0].strip().lower() + + +def _is_form_content_type(content_type: str) -> bool: + """ + True iff Starlette's ``request.form()`` will actually parse this body. + + Substring matching ``"form"`` is unsafe: ``request.form()`` returns empty + ``FormData`` for non-canonical types without consuming the body, leaving + the auth-time pre-read and the handler's read seeing different payloads. + """ + return _normalize_media_type(content_type) in _FORM_CONTENT_TYPES + + +def _is_json_content_type(content_type: str) -> bool: + """True iff the body should be parsed as JSON.""" + return _normalize_media_type(content_type) == "application/json" + + async def _read_request_body(request: Optional[Request]) -> Dict: """ Safely read the request body and parse it as JSON. @@ -37,8 +65,24 @@ async def _read_request_body(request: Optional[Request]) -> Dict: _request_headers: dict = _safe_get_request_headers(request=request) content_type = _request_headers.get("content-type", "") - if "form" in content_type: - parsed_body = dict(await request.form()) + if _is_form_content_type(content_type): + try: + form_data = await request.form() + except Exception as e: + # ``request.form()`` raises on malformed multipart (missing + # boundary, malformed chunk encoding, …). Surface as 400 so + # the auth-time pre-read does not silently cache ``{}`` while + # a later raw-body re-read sees the original payload — + # banned-param checks must see the same body the handler + # acts on. + verbose_proxy_logger.error(f"Invalid form payload: {e}") + raise ProxyException( + message=f"Invalid form payload: {e}", + type="invalid_request_error", + param="request_body", + code=status.HTTP_400_BAD_REQUEST, + ) + parsed_body = dict(form_data) if "metadata" in parsed_body and isinstance(parsed_body["metadata"], str): parsed_body["metadata"] = json.loads(parsed_body["metadata"]) else: @@ -306,18 +350,13 @@ async def get_request_body(request: Request) -> Dict[str, Any]: Read the request body and parse it as JSON. """ if request.method == "POST": - if request.headers.get("content-type", "") == "application/json": + content_type = request.headers.get("content-type", "") + if _is_json_content_type(content_type): return await _read_request_body(request) - elif "multipart/form-data" in request.headers.get( - "content-type", "" - ) or "application/x-www-form-urlencoded" in request.headers.get( - "content-type", "" - ): + elif _is_form_content_type(content_type): return await get_form_data(request) else: - raise ValueError( - f"Unsupported content type: {request.headers.get('content-type')}" - ) + raise ValueError(f"Unsupported content type: {content_type}") return {} diff --git a/litellm/proxy/container_endpoints/endpoints.py b/litellm/proxy/container_endpoints/endpoints.py index 9650604bf81..fc1f77bb684 100644 --- a/litellm/proxy/container_endpoints/endpoints.py +++ b/litellm/proxy/container_endpoints/endpoints.py @@ -328,7 +328,7 @@ async def retrieve_container( custom_llm_provider=custom_llm_provider, ) data.update( - get_container_forwarding_params( + await get_container_forwarding_params( container_id, original_container_id, custom_llm_provider, @@ -433,7 +433,7 @@ async def delete_container( custom_llm_provider=custom_llm_provider, ) data.update( - get_container_forwarding_params( + await get_container_forwarding_params( container_id, original_container_id, custom_llm_provider, diff --git a/litellm/proxy/container_endpoints/handler_factory.py b/litellm/proxy/container_endpoints/handler_factory.py index 4284cdd5d4a..7eeb11fc372 100644 --- a/litellm/proxy/container_endpoints/handler_factory.py +++ b/litellm/proxy/container_endpoints/handler_factory.py @@ -196,10 +196,12 @@ async def _process_binary_request( ) data: Dict[str, Any] = { "file_id": file_id, - **get_container_forwarding_params( - container_id=container_id, - original_container_id=original_container_id, - custom_llm_provider=resolved_provider, + **( + await get_container_forwarding_params( + container_id=container_id, + original_container_id=original_container_id, + custom_llm_provider=resolved_provider, + ) ), } processor = ProxyBaseLLMRequestProcessing(data=data) @@ -316,7 +318,7 @@ async def _process_multipart_upload_request( ) data.update( - get_container_forwarding_params( + await get_container_forwarding_params( container_id=container_id, original_container_id=original_container_id, custom_llm_provider=resolved_provider, @@ -396,7 +398,7 @@ async def _process_request( ) ) data.update( - get_container_forwarding_params( + await get_container_forwarding_params( container_id=path_params["container_id"], original_container_id=original_container_id, custom_llm_provider=resolved_provider, diff --git a/litellm/proxy/container_endpoints/ownership.py b/litellm/proxy/container_endpoints/ownership.py index 568eca523ae..57de6c4a63d 100644 --- a/litellm/proxy/container_endpoints/ownership.py +++ b/litellm/proxy/container_endpoints/ownership.py @@ -23,6 +23,13 @@ CONTAINER_OBJECT_PURPOSE = "container" _NEGATIVE_OWNER_SENTINEL = "__litellm_container_no_owner__" _CONTAINER_OWNER_CACHE = InMemoryCache(max_size_in_memory=10000, default_ttl=60) +# Caches the stored ``unified_object_id`` (the encoded container ID +# captured at create time) so ``get_container_forwarding_params`` can +# recover the deployment ``model_id`` for native upstream IDs without +# re-hitting Prisma on every retrieve/delete. +_NEGATIVE_STORED_ID_SENTINEL = "__litellm_container_no_stored_id__" +_CONTAINER_STORED_ID_CACHE = InMemoryCache(max_size_in_memory=10000, default_ttl=60) + # Per-caller-scope cache for ``GET /v1/containers`` list filtering. Without # this, every list call issues a fresh ``find_many`` against # ``litellm_managedobjecttable``. The cache key is the sorted owner-scope @@ -56,7 +63,7 @@ def decode_container_id_for_ownership( return original_container_id, custom_llm_provider -def get_container_forwarding_params( +async def get_container_forwarding_params( container_id: str, original_container_id: str, custom_llm_provider: str ) -> Dict[str, str]: params = { @@ -65,6 +72,20 @@ def get_container_forwarding_params( } decoded = ResponsesAPIRequestUtils._decode_container_id(container_id) model_id = decoded.get("model_id") + if not (isinstance(model_id, str) and model_id): + # Native upstream IDs (e.g. Azure ``cntr_``) carry no LiteLLM + # routing payload, so decoding the user-supplied id yields no + # ``model_id``. Recover it from the encoded ``unified_object_id`` + # captured on the ownership row at create time — when the router + # selected a specific deployment that ID embeds the model_id. + stored_id = await _get_stored_container_id( + original_container_id, custom_llm_provider + ) + if stored_id and stored_id != container_id: + stored_decoded = ResponsesAPIRequestUtils._decode_container_id(stored_id) + stored_model_id = stored_decoded.get("model_id") + if isinstance(stored_model_id, str) and stored_model_id: + model_id = stored_model_id if isinstance(model_id, str) and model_id: params["model_id"] = model_id return params @@ -168,6 +189,7 @@ async def record_container_owner( ) _CONTAINER_OWNER_CACHE.set_cache(model_object_id, owner) + _CONTAINER_STORED_ID_CACHE.set_cache(model_object_id, container_id) # Drop the caller's own list-cache entry so the just-created container # shows up on their next ``GET /v1/containers``. Other callers with # disjoint scope tuples have their own entries; intersecting-scope @@ -207,9 +229,60 @@ async def _get_container_owner( _CONTAINER_OWNER_CACHE.set_cache( model_object_id, owner if owner is not None else _NEGATIVE_OWNER_SENTINEL ) + stored_id = getattr(row, "unified_object_id", None) if row is not None else None + _CONTAINER_STORED_ID_CACHE.set_cache( + model_object_id, + ( + stored_id + if isinstance(stored_id, str) and stored_id + else _NEGATIVE_STORED_ID_SENTINEL + ), + ) return owner +async def _get_stored_container_id( + original_container_id: str, custom_llm_provider: str +) -> Optional[str]: + """Return the ``unified_object_id`` stored at create time, if any. + + Used by :func:`get_container_forwarding_params` to recover the + deployment ``model_id`` for native upstream container IDs: the stored + value is the encoded form produced by ``encode_container_id_in_response`` + when the router selected a specific deployment. + """ + model_object_id = _container_model_object_id( + original_container_id, custom_llm_provider + ) + + cached = _CONTAINER_STORED_ID_CACHE.get_cache(model_object_id) + if cached == _NEGATIVE_STORED_ID_SENTINEL: + return None + if isinstance(cached, str) and cached: + return cached + + prisma_client = await _get_prisma_client() + if prisma_client is None: + return None + + row = await prisma_client.db.litellm_managedobjecttable.find_first( + where={ + "model_object_id": model_object_id, + "file_purpose": CONTAINER_OBJECT_PURPOSE, + } + ) + stored_id = getattr(row, "unified_object_id", None) if row is not None else None + _CONTAINER_STORED_ID_CACHE.set_cache( + model_object_id, + ( + stored_id + if isinstance(stored_id, str) and stored_id + else _NEGATIVE_STORED_ID_SENTINEL + ), + ) + return stored_id if isinstance(stored_id, str) and stored_id else None + + async def assert_user_can_access_container( container_id: str, user_api_key_dict: UserAPIKeyAuth, diff --git a/litellm/proxy/db/spend_counter_reseed.py b/litellm/proxy/db/spend_counter_reseed.py index 19ec6699390..e7c5fa3f72c 100644 --- a/litellm/proxy/db/spend_counter_reseed.py +++ b/litellm/proxy/db/spend_counter_reseed.py @@ -178,15 +178,28 @@ class SpendCounterReseed: if db_spend is None: return None # Warm even when 0 so subsequent reads hit cache, not DB. + # + # Seed via SET NX (cross-pod safe): only one pod initializes the + # Redis key with db_spend; concurrent seeders read the winner's + # value. INCRBYFLOAT-of-db_spend from N pods would multiply the + # counter (N x db_spend) and trigger spurious budget alerts. + current_value: float = float(db_spend) try: if spend_counter_cache.redis_cache is not None: - current_value = ( - await spend_counter_cache.redis_cache.async_increment( - key=counter_key, - value=db_spend, - refresh_ttl=True, - ) + seeded = await spend_counter_cache.redis_cache.async_set_cache( + key=counter_key, + value=db_spend, + nx=True, ) + if seeded: + current_value = float(db_spend) + else: + cached = await spend_counter_cache.redis_cache.async_get_cache( + key=counter_key + ) + current_value = ( + float(cached) if cached is not None else float(db_spend) + ) spend_counter_cache.in_memory_cache.set_cache( key=counter_key, value=current_value, @@ -202,7 +215,7 @@ class SpendCounterReseed: ) if require_cache_warm: raise - return db_spend + return current_value @staticmethod async def window_from_spend_logs( diff --git a/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py b/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py index 5502076829f..0f299f4c5f7 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py +++ b/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py @@ -92,6 +92,8 @@ from litellm.types.utils import CallTypesLiteral # Module-level singleton for the JWKS discovery endpoint to access. _mcp_jwt_signer_instance: Optional["MCPJWTSigner"] = None +_MCP_JWT_CALL_TYPES = frozenset({"call_mcp_tool", "list_mcp_tools"}) + # Simple in-memory JWKS cache: keyed by JWKS URI → (keys_list, fetched_at). _jwks_cache: Dict[str, tuple] = {} _JWKS_CACHE_TTL = 3600 # 1 hour @@ -603,17 +605,23 @@ class MCPJWTSigner(CustomGuardrail): # FR-10: Scope building # ------------------------------------------------------------------ - def _build_scope(self, raw_tool_name: str) -> str: + def _build_scope( + self, + raw_tool_name: str, + call_type: Optional[CallTypesLiteral] = None, + ) -> str: """ Build the JWT scope string. When allowed_scopes is configured: join them verbatim. Otherwise auto-generate minimal, least-privilege scopes: - Tool call → mcp:tools/call mcp:tools/:call - - No tool → mcp:tools/call mcp:tools/list + - No tool → mcp:tools/list NOTE: tools/list is intentionally NOT granted on tool-call JWTs to prevent callers from enumerating tools they didn't ask to use. + Conversely, tools/call is NOT granted on tools/list-only JWTs so an + intercepted list token cannot be replayed to invoke tools. """ if self.allowed_scopes is not None: return " ".join(self.allowed_scopes) @@ -623,8 +631,14 @@ class MCPJWTSigner(CustomGuardrail): ) if tool_name: scopes = ["mcp:tools/call", f"mcp:tools/{tool_name}:call"] + elif call_type == "call_mcp_tool": + # Tool-call request reached the signer without a tool name (e.g. + # missing mcp_tool_name in hook data). Fall back to a generic + # tools/call scope so the upstream server still accepts the + # invocation rather than rejecting it as a tools/list-only token. + scopes = ["mcp:tools/call"] else: - scopes = ["mcp:tools/call", "mcp:tools/list"] + scopes = ["mcp:tools/list"] return " ".join(scopes) # ------------------------------------------------------------------ @@ -673,6 +687,7 @@ class MCPJWTSigner(CustomGuardrail): user_api_key_dict: UserAPIKeyAuth, data: dict, jwt_claims: Optional[Dict[str, Any]] = None, + call_type: Optional[CallTypesLiteral] = None, ) -> Dict[str, Any]: """ Build JWT claims for the outbound MCP access token. @@ -713,7 +728,7 @@ class MCPJWTSigner(CustomGuardrail): # scope (FR-10) raw_tool_name: str = data.get("mcp_tool_name", "") - claims["scope"] = self._build_scope(raw_tool_name) + claims["scope"] = self._build_scope(raw_tool_name, call_type=call_type) # optional_claims passthrough (FR-15) claims = self._passthrough_optional_claims(claims, jwt_claims) @@ -779,16 +794,20 @@ class MCPJWTSigner(CustomGuardrail): Verifies the incoming token (when configured), validates required claims, then signs an outbound JWT and injects it as the Authorization header. - All non-MCP call types pass through unchanged. + Signs outbound MCP tool calls and tools/list requests. """ - if call_type != "call_mcp_tool": + if call_type not in _MCP_JWT_CALL_TYPES: return data + hook_data = dict(data) + if call_type == "list_mcp_tools": + hook_data["mcp_tool_name"] = "" + # ------------------------------------------------------------------ # FR-5: Verify incoming token before re-signing # ------------------------------------------------------------------ jwt_claims: Optional[Dict[str, Any]] = None - raw_token: Optional[str] = data.get("incoming_bearer_token") + raw_token: Optional[str] = hook_data.get("incoming_bearer_token") if self.access_token_discovery_uri and raw_token: # Three-dot pattern → JWT; otherwise opaque. @@ -837,7 +856,9 @@ class MCPJWTSigner(CustomGuardrail): # ------------------------------------------------------------------ # Build outbound access token # ------------------------------------------------------------------ - claims = self._build_claims(user_api_key_dict, data, jwt_claims) + claims = self._build_claims( + user_api_key_dict, hook_data, jwt_claims, call_type=call_type + ) signed_token = jwt.encode( claims, @@ -848,7 +869,7 @@ class MCPJWTSigner(CustomGuardrail): # Merge into existing extra_headers — a prior guardrail in the chain may # have already injected tracing headers or correlation IDs. - existing_headers: Dict[str, str] = data.get("extra_headers") or {} + existing_headers: Dict[str, str] = hook_data.get("extra_headers") or {} new_headers: Dict[str, str] = { **existing_headers, "Authorization": f"Bearer {signed_token}", @@ -875,17 +896,74 @@ class MCPJWTSigner(CustomGuardrail): claims, self._kid ) - data["extra_headers"] = new_headers + hook_data["extra_headers"] = new_headers verbose_proxy_logger.debug( "MCPJWTSigner: signed JWT sub=%s act=%s tool=%s exp=%d " - "verified=%s channel=%s", + "verified=%s channel=%s call_type=%s", claims.get("sub"), claims.get("act", {}).get("sub"), - data.get("mcp_tool_name"), + hook_data.get("mcp_tool_name"), claims["exp"], jwt_claims is not None, bool(self.channel_token_audience), + call_type, ) - return data + return hook_data + + +async def inject_mcp_jwt_headers_for_upstream( + user_api_key_dict: Optional[UserAPIKeyAuth], + extra_headers: Optional[Dict[str, str]] = None, + raw_headers: Optional[Dict[str, str]] = None, + *, + for_list_tools: bool = False, + mcp_tool_name: str = "", +) -> Dict[str, str]: + """ + Sign outbound MCP headers when MCPJWTSigner is configured. + + Used by tools/list paths that do not go through proxy pre_call_hook. + """ + merged = dict(extra_headers or {}) + signer = get_mcp_jwt_signer() + if signer is None or user_api_key_dict is None: + return merged + + normalized_raw = {k.lower(): v for k, v in (raw_headers or {}).items()} + incoming_bearer_token: Optional[str] = None + auth_hdr = normalized_raw.get("authorization", "") + if auth_hdr.lower().startswith("bearer "): + incoming_bearer_token = auth_hdr[len("bearer ") :] + + hook_data: Dict[str, Any] = { + "mcp_tool_name": "" if for_list_tools else mcp_tool_name, + "incoming_bearer_token": incoming_bearer_token, + "extra_headers": merged, + } + call_type: CallTypesLiteral = ( + "list_mcp_tools" if for_list_tools else "call_mcp_tool" + ) + try: + from litellm.proxy.proxy_server import ( # noqa: PLC0415 + proxy_logging_obj as _proxy_logging, + ) + + shared_cache = ( + _proxy_logging.internal_usage_cache.dual_cache + if _proxy_logging is not None + else DualCache() + ) + except Exception: + shared_cache = DualCache() + + result = await signer.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=shared_cache, + data=hook_data, + call_type=call_type, + ) + if isinstance(result, dict) and result.get("extra_headers"): + merged.update(result["extra_headers"]) + return merged diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 6e2e2bedac1..ff3bbf47389 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -1798,7 +1798,10 @@ async def cli_sso_callback( from fastapi.responses import HTMLResponse - verify_url = str(request.url_for("cli_sso_complete", login_id=key)) + verify_url = get_custom_url( + request_base_url=str(request.base_url), + route=f"sso/cli/complete/{key}", + ) html_content = _render_cli_sso_verification_page( verify_url=verify_url, browser_complete_token=browser_complete_token, diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 30c78ed5ba7..0415bb456ec 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -727,6 +727,76 @@ async def resolve_output_file_ids_to_unified(response, prisma_client) -> None: pass +async def ensure_batch_response_managed_file_ids( + response, + managed_files_obj, + prisma_client, + verbose_proxy_logger, + user_api_key_dict=None, + db_batch_object=None, +) -> None: + """Normalize batch file IDs to managed unified IDs before DB persistence.""" + await resolve_input_file_id_to_unified(response, prisma_client) + await resolve_output_file_ids_to_unified(response, prisma_client) + + if managed_files_obj is None: + return + + hidden_params = getattr(response, "_hidden_params", None) or {} + model_id = hidden_params.get("model_id") + if not model_id: + return + + model_name = hidden_params.get("model_name") + unified_file_id = hidden_params.get("unified_file_id") + if not model_name and isinstance(unified_file_id, str): + decoded_unified_file_id = ( + _is_base64_encoded_unified_file_id(unified_file_id) or unified_file_id + ) + target_model_names = get_models_from_unified_file_id(decoded_unified_file_id) + if target_model_names: + model_name = ",".join(target_model_names) + + if user_api_key_dict is None and db_batch_object is not None: + from litellm.proxy._types import UserAPIKeyAuth + + user_api_key_dict = UserAPIKeyAuth( + user_id=getattr(db_batch_object, "created_by", None) or "default-user-id", + team_id=getattr(db_batch_object, "team_id", None), + ) + if user_api_key_dict is None: + return + + for file_attr in ("output_file_id", "error_file_id"): + raw_file_id = getattr(response, file_attr, None) + if not raw_file_id or _is_base64_encoded_unified_file_id(raw_file_id): + continue + try: + new_unified_file_id = managed_files_obj.get_unified_output_file_id( + output_file_id=raw_file_id, + model_id=model_id, + model_name=model_name, + ) + await managed_files_obj.store_unified_file_id( + file_id=new_unified_file_id, + file_object=None, + litellm_parent_otel_span=getattr( + user_api_key_dict, "parent_otel_span", None + ), + model_mappings={model_id: raw_file_id}, + user_api_key_dict=user_api_key_dict, + ) + setattr(response, file_attr, new_unified_file_id) + verbose_proxy_logger.debug( + f"Converted batch {file_attr} {raw_file_id!r} to managed ID before DB write" + ) + except Exception as e: + verbose_proxy_logger.warning( + f"Failed to convert batch {file_attr}={raw_file_id!r} to managed ID " + f"before DB write: {e}" + ) + + async def get_batch_from_database( batch_id: str, unified_batch_id: Union[str, Literal[False]], @@ -800,6 +870,7 @@ async def update_batch_in_database( verbose_proxy_logger, db_batch_object=None, operation: str = "update", + user_api_key_dict=None, ): """ Update batch status and object in ManagedObjectTable. @@ -813,6 +884,7 @@ async def update_batch_in_database( verbose_proxy_logger: Logger instance db_batch_object: Optional existing database object (for comparison) operation: Description of operation ("update", "cancel", etc.) + user_api_key_dict: Optional auth context for creating managed file IDs """ import litellm.utils @@ -823,6 +895,18 @@ async def update_batch_in_database( if not prisma_client: return + # Always normalize the response's file IDs to unified managed IDs + # (mutates in place) so the caller returns unified IDs to the user + # even when we skip the DB update below for an unchanged status. + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=managed_files_obj, + prisma_client=prisma_client, + verbose_proxy_logger=verbose_proxy_logger, + user_api_key_dict=user_api_key_dict, + db_batch_object=db_batch_object, + ) + # Only update if status has changed (when db_batch_object is provided) if db_batch_object and response.status == db_batch_object.status: return diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 5fc8c44b2d8..37bab3a45d0 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -38,6 +38,35 @@ class LiteLLMDatabaseConnectionPool(Enum): database_connection_pool_timeout = 60 +def _build_db_connection_url_params( + connection_limit: int, + pool_timeout: Optional[Union[int, float]], + connect_timeout: Optional[Union[int, float]] = None, + socket_timeout: Optional[Union[int, float]] = None, + extra_params: Optional[dict] = None, +) -> dict: + """Build the Prisma DATABASE_URL query params controlling connection pool behavior. + + `connect_timeout` / `socket_timeout` map to the Prisma URL params of the same + name (https://www.prisma.io/docs/orm/overview/databases/postgresql) and are + omitted when None so Prisma's defaults apply. `extra_params` is an + untyped passthrough — keys it provides win over the named arguments above, + so it can be used to override any default we set here. + """ + params: dict = { + "connection_limit": connection_limit, + } + if pool_timeout is not None: + params["pool_timeout"] = pool_timeout + if connect_timeout is not None: + params["connect_timeout"] = connect_timeout + if socket_timeout is not None: + params["socket_timeout"] = socket_timeout + if extra_params: + params.update(extra_params) + return params + + def append_query_params(url: Optional[str], params: dict) -> str: from litellm._logging import verbose_proxy_logger @@ -807,6 +836,9 @@ def run_server( # noqa: PLR0915 db_connection_pool_limit = 100 # Starts optional due to config fallback checks; guaranteed non-None before use. db_connection_timeout: Optional[Union[int, float]] = 60 + db_connect_timeout: Optional[Union[int, float]] = None + db_socket_timeout: Optional[Union[int, float]] = None + db_extra_connection_params: Optional[dict] = None general_settings = {} ### GET DB TOKEN FOR IAM AUTH ### @@ -924,6 +956,11 @@ def run_server( # noqa: PLR0915 db_connection_timeout = ( LiteLLMDatabaseConnectionPool.database_connection_pool_timeout.value ) + db_connect_timeout = general_settings.get("database_connect_timeout") + db_socket_timeout = general_settings.get("database_socket_timeout") + db_extra_connection_params = general_settings.get( + "database_extra_connection_params" + ) if database_url and database_url.startswith("os.environ/"): original_dir = os.getcwd() # set the working directory to where this script is @@ -963,27 +1000,26 @@ def run_server( # noqa: PLR0915 try: from litellm.secret_managers.main import get_secret + connection_url_params = _build_db_connection_url_params( + connection_limit=db_connection_pool_limit, + pool_timeout=db_connection_timeout, + connect_timeout=db_connect_timeout, + socket_timeout=db_socket_timeout, + extra_params=db_extra_connection_params, + ) if os.getenv("DATABASE_URL", None) is not None: - ### add connection pool + pool timeout args - params = { - "connection_limit": db_connection_pool_limit, - "pool_timeout": db_connection_timeout, - } database_url = get_secret("DATABASE_URL", default_value=None) modified_url = append_query_params( - str(database_url) if database_url else None, params + str(database_url) if database_url else None, + connection_url_params, ) os.environ["DATABASE_URL"] = modified_url if os.getenv("DIRECT_URL", None) is not None: - ### add connection pool + pool timeout args - params = { - "connection_limit": db_connection_pool_limit, - "pool_timeout": db_connection_timeout, - } database_url = os.getenv("DIRECT_URL") - modified_url = append_query_params(database_url, params) + modified_url = append_query_params( + database_url, connection_url_params + ) os.environ["DIRECT_URL"] = modified_url - ### subprocess.run(["prisma"], capture_output=True) is_prisma_runnable = True except FileNotFoundError: diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 23f2867152a..0d40f56d8b6 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4328,6 +4328,19 @@ class ProxyConfig: "health_check_concurrency", None ) health_check_details = general_settings.get("health_check_details", True) + ### INTERACTIONS API SCHEMA ### + _use_legacy_interactions_schema = general_settings.get( + "use_legacy_interactions_schema" + ) + if _use_legacy_interactions_schema is not None: + if isinstance(_use_legacy_interactions_schema, str): + litellm.use_legacy_interactions_schema = ( + _use_legacy_interactions_schema.lower() == "true" + ) + else: + litellm.use_legacy_interactions_schema = bool( + _use_legacy_interactions_schema + ) # Health-check-driven routing (opt-in, passes through to Router later) _enable_hc_routing = general_settings.get( "enable_health_check_routing", False diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 32c887f17b2..36fd605cf72 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -6068,6 +6068,8 @@ async def get_available_models_for_user( include_model_access_groups=include_model_access_groups, ) + effective_team_id = team_id or user_api_key_dict.team_id + # Get complete model list all_models = get_complete_model_list( key_models=key_models, @@ -6080,6 +6082,7 @@ async def get_available_models_for_user( model_access_groups=model_access_groups, include_model_access_groups=include_model_access_groups, only_model_access_groups=only_model_access_groups, + team_id=effective_team_id, ) return all_models diff --git a/litellm/responses/litellm_completion_transformation/handler.py b/litellm/responses/litellm_completion_transformation/handler.py index f730a089624..03a2f339bea 100644 --- a/litellm/responses/litellm_completion_transformation/handler.py +++ b/litellm/responses/litellm_completion_transformation/handler.py @@ -65,8 +65,7 @@ class LiteLLMCompletionTransformationHandler: litellm_completion_response: Union[ ModelResponse, litellm.CustomStreamWrapper ] = litellm.completion( - **litellm_completion_request, - **kwargs, + **completion_args, ) if isinstance(litellm_completion_response, ModelResponse): diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 4ee9235af7d..35680889d86 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -1115,6 +1115,7 @@ def responses( stream=stream, extra_headers=extra_headers, extra_body=extra_body, + timeout=timeout if timeout is not None else request_timeout, **kwargs, ) diff --git a/litellm/router.py b/litellm/router.py index 019f565e5c4..c968c819400 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -208,6 +208,15 @@ if TYPE_CHECKING: from litellm.router_strategy.quality_router.quality_router import ( QualityRouter, ) + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject + from litellm.types.llms.openai import ( + ResponseAPIUsage, + ResponseInputParam, + ResponsesAPIResponse, + ) Span = Union[_Span, Any] else: @@ -2246,6 +2255,388 @@ class Router: return FallbackStreamWrapper(stream_with_fallbacks()) + @staticmethod + def _extract_partial_responses_usage( + source_iterator: "BaseResponsesAPIStreamingIterator", + ) -> Optional["ResponseAPIUsage"]: + """ + Best-effort: pull partial token usage from a Responses-API streaming + iterator that errored mid-stream, normalized to ResponseAPIUsage so + the caller can combine without crossing token-naming conventions. + + Two sources, in priority order: + 1. The bridge path (LiteLLMCompletionStreamingIterator) accumulates + chat-completion chunks while streaming — feed them through + stream_chunk_builder to recover chat Usage, then translate + (prompt_tokens → input_tokens, completion_tokens → output_tokens). + 2. The native path (ResponsesAPIStreamingIterator) only has a + completed_response object if the stream reached + RESPONSE_COMPLETED before erroring — uncommon mid-stream but + worth checking. Already ResponseAPIUsage-shaped. + + Returns None when no partial usage is recoverable. + """ + from litellm.responses.litellm_completion_transformation.streaming_iterator import ( + LiteLLMCompletionStreamingIterator, + ) + from litellm.types.llms.openai import ( + ResponseAPIUsage, + ResponseCompletedEvent, + ResponseFailedEvent, + ResponseIncompleteEvent, + ) + + # Bridge subclass is the only iterator that accumulates chat-completion + # chunks. isinstance narrows the type so we can read the attribute + # directly instead of getattr-ing on the base class. + if isinstance(source_iterator, LiteLLMCompletionStreamingIterator): + chunks = source_iterator.collected_chat_completion_chunks + if chunks: + try: + from litellm.main import stream_chunk_builder + + built = stream_chunk_builder(chunks=chunks) + # stream_chunk_builder returns ModelResponse | + # TextCompletionResponse | None. ModelResponse sets .usage + # in __init__ rather than declaring it as a class field, so + # static narrowing doesn't expose it. Mirror the sync path + # (_completion_streaming_iterator) and pull via getattr. + chat = getattr(built, "usage", None) if built is not None else None + if chat is not None: + # getattr-with-default because the test path may + # substitute a SimpleNamespace lacking some fields; + # real Usage instances always have them. + prompt = int(getattr(chat, "prompt_tokens", 0) or 0) + completion = int(getattr(chat, "completion_tokens", 0) or 0) + total = int( + getattr(chat, "total_tokens", prompt + completion) + or (prompt + completion) + ) + return ResponseAPIUsage( + input_tokens=prompt, + output_tokens=completion, + total_tokens=total, + ) + except Exception: + # Builder is best-effort — fall through to native path. + pass + + # Native path: completed_response is set only if RESPONSE_COMPLETED + # arrived before the error (uncommon mid-stream but worth checking). + # Already ResponseAPIUsage-shaped — return as-is. + completed = source_iterator.completed_response + if isinstance( + completed, + (ResponseCompletedEvent, ResponseFailedEvent, ResponseIncompleteEvent), + ): + return completed.response.usage + return None + + @staticmethod + def _combine_responses_fallback_usage( + fallback_item: "BaseLiteLLMOpenAIResponseObject", + partial_usage: "ResponseAPIUsage", + ) -> None: + """ + Merge partial-stream usage with fallback-stream usage on a + Responses-API streaming event. + + Only mutates events that carry a `response` with a `usage` field + (response.completed / response.failed / response.incomplete). Other + events pass through unchanged. + + Both inputs are ResponseAPIUsage-shaped (see + _extract_partial_responses_usage which normalizes the bridge path), + so we can sum input_tokens / output_tokens / total_tokens directly + and produce a clean ResponseAPIUsage — no token-naming split, no + setattr bypass. + """ + from litellm.types.llms.openai import ( + ResponseAPIUsage, + ResponseCompletedEvent, + ResponseFailedEvent, + ResponseIncompleteEvent, + ) + + if not isinstance( + fallback_item, + (ResponseCompletedEvent, ResponseFailedEvent, ResponseIncompleteEvent), + ): + return + response = fallback_item.response + if response.usage is None: + return + + fb = response.usage + response.usage = ResponseAPIUsage( + input_tokens=(partial_usage.input_tokens or 0) + (fb.input_tokens or 0), + output_tokens=(partial_usage.output_tokens or 0) + (fb.output_tokens or 0), + total_tokens=(partial_usage.total_tokens or 0) + (fb.total_tokens or 0), + ) + + @staticmethod + def _build_responses_continuation_input( + input_val: Optional[Union[str, "ResponseInputParam"]], + generated_content: str, + ) -> "ResponseInputParam": + """ + Convert Responses-API input + partial assistant output into a + continuation input that asks the fallback model to pick up where the + prior assistant message stopped. + + Best effort across providers. The chat-completions path uses + Anthropic's `prefix: True` prefill trick on the assistant message; + the Responses-API input schema has no direct equivalent, so we + append an instruction (developer role) plus a prior assistant + message containing the partial output. Providers without prefill + semantics (OpenAI, Vertex) treat this as conversational context + and may regenerate — same trade-off as the chat-completions path + for non-Anthropic fallbacks. + """ + # base/continuation are List[Any] because ResponseInputParam items + # are a wide Union of TypedDicts (EasyInputMessageParam, Message, + # ResponseOutputMessageParam, ...) — annotating as List[Dict[str, Any]] + # rejects the list() spread of input_val. We cast the combined list to + # ResponseInputParam at the return. + base: List[Any] + if isinstance(input_val, str): + base = [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": input_val}], + } + ] + elif isinstance(input_val, list): + base = list(input_val) + else: + base = [] + continuation: List[Any] = [ + { + "type": "message", + "role": "developer", + "content": [ + { + "type": "input_text", + "text": ( + "The previous assistant response was interrupted " + "mid-stream. Continue exactly where it stopped — " + "do not repeat any of its content. Your response " + "must read as a seamless continuation." + ), + } + ], + }, + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": generated_content}], + }, + ] + return cast("ResponseInputParam", base + continuation) + + async def _aresponses_streaming_iterator( + self, + response: "BaseResponsesAPIStreamingIterator", + initial_kwargs: Dict[str, Any], + ) -> "BaseResponsesAPIStreamingIterator": + """ + Wrap a Responses-API streaming iterator so MidStreamFallbackError + triggers the Router's fallback chain (parity with + _acompletion_streaming_iterator for the chat-completions path). + + The Responses-API streaming path goes through + _ageneric_api_call_with_fallbacks rather than _acompletion, so the + returned iterator is never wrapped by the chat completions + fallback handler. Without this wrapper, MidStreamFallbackError + raised mid-stream from the underlying CustomStreamWrapper (used by + LiteLLMCompletionStreamingIterator when the Responses API is + served via the completion bridge) propagates unhandled and the + configured cross-provider fallback never fires. + + Full parity with the chat-completions path: + - Pre-first-chunk: retry with the original input unchanged. + - Partial content: inject a developer instruction + prior + assistant message carrying the generated text so the fallback + model continues rather than restarts. + - Usage combining: merge partial-stream usage onto the fallback's + response.completed event so accounting reflects both attempts. + - Stream cleanup: shielded aclose() on both source and fallback + iterators on terminate. + """ + from litellm.exceptions import MidStreamFallbackError + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + + source_iterator = response + + class FallbackResponsesStreamWrapper(BaseResponsesAPIStreamingIterator): + """ + Subclasses BaseResponsesAPIStreamingIterator only for isinstance + compatibility (proxy + interactions code paths check the type). + Bypasses the parent constructor and delegates iteration to an + async generator. + """ + + def __init__(self, async_generator: AsyncGenerator): + import time + from datetime import datetime + + self._async_generator = async_generator + # Mirror every attribute BaseResponsesAPIStreamingIterator.__init__ + # would have set. The wrapper bypasses super().__init__ (it has no + # httpx.Response of its own and no provider config to drive), so + # we copy from source_iterator where applicable and use safe + # defaults elsewhere. This keeps inherited methods (e.g. + # _check_max_streaming_duration, _handle_failure) safe to call. + # + # The bridge path (LiteLLMCompletionStreamingIterator used by + # Anthropic/Bedrock/Vertex) does not call super().__init__ and + # is missing many of these attributes — use getattr fallbacks + # so wrapper construction never raises AttributeError. The + # bridge stores the logging object as `litellm_logging_obj`. + self.response = getattr(source_iterator, "response", None) + self.model = getattr(source_iterator, "model", None) + self.logging_obj = getattr( + source_iterator, + "logging_obj", + getattr(source_iterator, "litellm_logging_obj", None), + ) + self.finished = False + self.responses_api_provider_config = getattr( + source_iterator, "responses_api_provider_config", None + ) + self.completed_response = None + self.start_time = getattr(source_iterator, "start_time", datetime.now()) + self._failure_handled = False + self._completed_response_cached = False + self._completed_response_logged = False + self._completed_response_cache_hit = None + self._persist_completed_response_before_logging = True + self._stream_created_time = time.time() + self.litellm_metadata = getattr( + source_iterator, "litellm_metadata", None + ) + self.custom_llm_provider = getattr( + source_iterator, "custom_llm_provider", None + ) + self.request_data = getattr(source_iterator, "request_data", {}) or {} + self.call_type = getattr(source_iterator, "call_type", None) + # Preserve hidden params so response headers (model_id, + # api_base, additional_headers) keep flowing. + self._hidden_params = dict( + getattr(source_iterator, "_hidden_params", None) or {} + ) + + def __aiter__(self): + return self + + async def __anext__(self): + return await self._async_generator.__anext__() + + async def aclose(self): + # async generators always expose aclose — no defensive check needed. + await self._async_generator.aclose() + + async def stream_with_fallbacks(): + fallback_response = None + try: + async for item in source_iterator: + yield item + except MidStreamFallbackError as e: + partial_usage = Router._extract_partial_responses_usage(source_iterator) + try: + model_group = cast(str, initial_kwargs.get("model")) + fallbacks: Optional[List] = initial_kwargs.get( + "fallbacks", self.fallbacks + ) + context_window_fallbacks: Optional[List] = initial_kwargs.get( + "context_window_fallbacks", self.context_window_fallbacks + ) + content_policy_fallbacks: Optional[List] = initial_kwargs.get( + "content_policy_fallbacks", self.content_policy_fallbacks + ) + # Re-enter via the per-attempt helper so the fallback chain + # picks deployments through + # _ageneric_api_call_with_fallbacks_helper. + # original_generic_function is preserved by the caller so + # the helper knows what underlying API to invoke per attempt. + initial_kwargs["original_function"] = ( + self._ageneric_api_call_with_fallbacks_helper + ) + if e.is_pre_first_chunk or not e.generated_content: + # No content generated before the error — retry with the + # original input. Adding a continuation prompt would + # waste tokens and confuse the model. + pass + else: + initial_kwargs["input"] = ( + Router._build_responses_continuation_input( + initial_kwargs.get("input"), + e.generated_content, + ) + ) + # The Responses-API path stores observability metadata + # under "litellm_metadata" (not the default "metadata") — + # see _ageneric_api_call_with_fallbacks. Mirroring that + # here ensures model_group, model_group_alias, and trace + # ids land in the same key litellm.aresponses reads from. + self._update_kwargs_before_fallbacks( + model=model_group, + kwargs=initial_kwargs, + metadata_variable_name="litellm_metadata", + ) + fallback_response = ( + await self.async_function_with_fallbacks_common_utils( + e=e, + disable_fallbacks=False, + fallbacks=fallbacks, + context_window_fallbacks=context_window_fallbacks, + content_policy_fallbacks=content_policy_fallbacks, + model_group=model_group, + args=(), + kwargs=initial_kwargs, + ) + ) + + if hasattr(fallback_response, "__aiter__"): + async for fallback_item in fallback_response: # type: ignore + if partial_usage is not None: + Router._combine_responses_fallback_usage( + fallback_item, partial_usage + ) + yield fallback_item + else: + yield fallback_response + except Exception as fallback_error: + verbose_router_logger.error( + f"Responses streaming fallback also failed: {fallback_error}" + ) + raise fallback_error + finally: + with anyio.CancelScope(shield=True): + if hasattr(source_iterator, "aclose"): + try: + await source_iterator.aclose() # type: ignore[func-returns-value] + except BaseException as exc: + verbose_router_logger.debug( + "stream_with_fallbacks(aresponses): error closing source: %s", + exc, + ) + if fallback_response is not None and hasattr( + fallback_response, "aclose" + ): + try: + await fallback_response.aclose() + except BaseException as exc: + verbose_router_logger.debug( + "stream_with_fallbacks(aresponses): error closing fallback: %s", + exc, + ) + + return FallbackResponsesStreamWrapper(stream_with_fallbacks()) + def _completion_streaming_iterator( # noqa: PLR0915 self, model_response: CustomStreamWrapper, @@ -4292,6 +4683,61 @@ class Router: self.fail_calls[model] += 1 raise e + async def _aresponses_with_streaming_fallbacks( + self, original_function: Callable, **kwargs: Any + ) -> Union["ResponsesAPIResponse", "BaseResponsesAPIStreamingIterator"]: + """ + _ageneric_api_call_with_fallbacks for the Responses API, with the + addition of mid-stream fallback handling. + + When stream=True and the underlying call returns a + BaseResponsesAPIStreamingIterator, wrap it with + _aresponses_streaming_iterator so MidStreamFallbackError raised + during iteration triggers the Router's cross-provider fallback chain. + """ + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + + from litellm.litellm_core_utils.core_helpers import safe_deep_copy + + # Snapshot the request kwargs before _ageneric_api_call_with_fallbacks + # mutates them. A shallow copy alone is not enough: the primary + # attempt mutates nested dicts in place — notably `litellm_metadata`, + # which `_update_kwargs_with_deployment` populates with + # deployment-specific fields (`deployment`, `model_info`, `api_base`, + # tags, etc.). Without an explicit copy of that dict, the shallow + # copy would still share its reference, leaking primary-deployment + # metadata into the mid-stream fallback request. + # + # We avoid deep-copying the full kwargs because it can contain + # non-deepcopyable objects (logging handles, async clients, etc.); + # `safe_deep_copy` deep-copies the metadata dicts key-by-key with a + # fallback to the original reference for any non-picklable value. + # The original_generic_function is preserved so the per-attempt + # helper knows which underlying API to call on fallback. + fallback_kwargs: Dict[str, Any] = kwargs.copy() + if isinstance(fallback_kwargs.get("litellm_metadata"), dict): + fallback_kwargs["litellm_metadata"] = safe_deep_copy( + fallback_kwargs["litellm_metadata"] + ) + if isinstance(fallback_kwargs.get("metadata"), dict): + fallback_kwargs["metadata"] = safe_deep_copy(fallback_kwargs["metadata"]) + fallback_kwargs["original_generic_function"] = original_function + + response = await self._ageneric_api_call_with_fallbacks( + original_function=original_function, **kwargs + ) + + if kwargs.get("stream") and isinstance( + response, BaseResponsesAPIStreamingIterator + ): + return await self._aresponses_streaming_iterator( + response=response, + initial_kwargs=fallback_kwargs, + ) + return response + def _generic_api_call_with_fallbacks( self, model: str, original_function: Callable, **kwargs ): @@ -5511,9 +5957,13 @@ class Router: custom_llm_provider=custom_llm_provider, **kwargs, ) + elif call_type == "aresponses": + return await self._aresponses_with_streaming_fallbacks( + original_function=original_function, + **kwargs, + ) elif call_type in ( "anthropic_messages", - "aresponses", "_arealtime", "_aresponses_websocket", "acreate_fine_tuning_job", @@ -5670,6 +6120,7 @@ class Router: from litellm.responses.utils import ResponsesAPIRequestUtils container_id = kwargs.get("container_id") + _forwarded_model_id = kwargs.get("model_id") if isinstance(container_id, str): decoded = ResponsesAPIRequestUtils._decode_container_id(container_id) original_id = decoded.get("response_id", container_id) @@ -5678,7 +6129,14 @@ class Router: decoded_provider = decoded.get("custom_llm_provider") if decoded_provider and kwargs.get("custom_llm_provider") == "openai": kwargs["custom_llm_provider"] = decoded_provider - model_id = decoded.get("model_id") + # Fall back to the model_id forwarded by the proxy when the container_id + # is a native upstream ID (e.g. Azure hex cntr_) that carries no LiteLLM + # routing payload, so deployment credentials (api_base, api_key) are applied. + model_id = decoded.get("model_id") or ( + _forwarded_model_id.strip() + if isinstance(_forwarded_model_id, str) and _forwarded_model_id.strip() + else None + ) if model_id: kwargs["model"] = model_id return await self._ageneric_api_call_with_fallbacks( diff --git a/litellm/types/interactions/__init__.py b/litellm/types/interactions/__init__.py index 0f934fa0152..78d0b04ef3b 100644 --- a/litellm/types/interactions/__init__.py +++ b/litellm/types/interactions/__init__.py @@ -36,9 +36,13 @@ from litellm.types.interactions.generated import ( GoogleSearchResultContent, ImageContent, Interaction, + InteractionCompleted, + InteractionCreated, InteractionEvent, InteractionEnvironment, + InteractionInProgress, InteractionInput, + InteractionRequiresAction, InteractionsAPIOptionalRequestParams, InteractionsAPIResponse, InteractionsAPIStreamingResponse, @@ -50,6 +54,9 @@ from litellm.types.interactions.generated import ( McpServerToolResultContent, ModelOption, ResponseModality, + StepDelta, + StepStart, + StepStop, ) from litellm.types.interactions.generated import ( Status3 as InteractionStatus, # Main request/response types; Content types; Turn for multi-turn conversations; Tool types; Config types; Usage; Status enum; Events for streaming; Agent configs; Model/Agent options; Response modality; Annotation; LiteLLM types; Backwards compat aliases @@ -115,6 +122,14 @@ __all__ = [ "AgentOption", "ResponseModality", "Annotation", + # New schema SSE event types (Api-Revision: 2026-05-20) + "StepStart", + "StepDelta", + "StepStop", + "InteractionCreated", + "InteractionInProgress", + "InteractionCompleted", + "InteractionRequiresAction", # LiteLLM types "InteractionEnvironment", "InteractionInput", diff --git a/litellm/types/interactions/generated.py b/litellm/types/interactions/generated.py index 0677163791e..b38cd8f58b9 100644 --- a/litellm/types/interactions/generated.py +++ b/litellm/types/interactions/generated.py @@ -1153,9 +1153,114 @@ class InteractionEvent(BaseModel): ) +# --------------------------------------------------------------- +# New schema SSE event types (Api-Revision: 2026-05-20) +# These replace the legacy content.* / interaction.start|complete +# events and will become the only events after June 8, 2026. +# --------------------------------------------------------------- + + +class StepStart(BaseModel): + """Emitted when a new step begins (replaces content.start).""" + + event_type: Literal["step.start"] = "step.start" + index: Optional[int] = None + step: Optional[Dict[str, Any]] = Field( + None, + description="The initial step data (type, content, signature, etc.).", + ) + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + +class StepDelta(BaseModel): + """Emitted for incremental step content (replaces content.delta).""" + + event_type: Literal["step.delta"] = "step.delta" + index: Optional[int] = None + delta: Optional[Dict[str, Any]] = Field( + None, + description="Incremental content delta (e.g. text, arguments_delta for function calls).", + ) + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + +class StepStop(BaseModel): + """Emitted when a step finishes (replaces content.stop).""" + + event_type: Literal["step.stop"] = "step.stop" + index: Optional[int] = None + status: Optional[str] = Field( + None, + description="Step completion status (e.g. 'done').", + ) + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + +class InteractionCreated(BaseModel): + """Emitted when the interaction is first created (replaces interaction.start).""" + + event_type: Literal["interaction.created"] = "interaction.created" + interaction: Optional[Dict[str, Any]] = None + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + +class InteractionInProgress(BaseModel): + """Emitted while the interaction is running.""" + + event_type: Literal["interaction.in_progress"] = "interaction.in_progress" + interaction_id: Optional[str] = None + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + +class InteractionCompleted(BaseModel): + """Emitted when the interaction finishes (replaces interaction.complete).""" + + event_type: Literal["interaction.completed"] = "interaction.completed" + interaction: Optional[Dict[str, Any]] = None + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + +class InteractionRequiresAction(BaseModel): + """Emitted when the interaction is paused waiting for a tool result.""" + + event_type: Literal["interaction.requires_action"] = "interaction.requires_action" + interaction_id: Optional[str] = None + event_id: Optional[str] = Field( + None, + description="The event_id token to be used to resume the interaction stream.", + ) + + class InteractionSseEvent( RootModel[ Union[ + # New schema events (Api-Revision: 2026-05-20) + StepStart, + StepDelta, + StepStop, + InteractionCreated, + InteractionInProgress, + InteractionCompleted, + InteractionRequiresAction, + # Legacy schema events (Api-Revision: 2026-05-07, removed June 8 2026) InteractionEvent, InteractionStatusUpdate, ContentStart, @@ -1166,6 +1271,15 @@ class InteractionSseEvent( ] ): root: Union[ + # New schema events (Api-Revision: 2026-05-20) + StepStart, + StepDelta, + StepStop, + InteractionCreated, + InteractionInProgress, + InteractionCompleted, + InteractionRequiresAction, + # Legacy schema events (Api-Revision: 2026-05-07, removed June 8 2026) InteractionEvent, InteractionStatusUpdate, ContentStart, @@ -1195,6 +1309,11 @@ class InteractionsAPIResponse(BaseLiteLLMOpenAIResponseObject): Response from the Interactions API. Wraps the API response with LiteLLM-specific hidden params. + + Schema notes: + - New schema (Api-Revision: 2026-05-20, default): response contains ``steps``. + - Legacy schema (Api-Revision: 2026-05-07, removed June 8 2026): response contains ``outputs``. + Both fields are kept here so callers work with either schema. """ id: Optional[str] = None @@ -1205,7 +1324,10 @@ class InteractionsAPIResponse(BaseLiteLLMOpenAIResponseObject): created: Optional[str] = None updated: Optional[str] = None role: Optional[str] = None + # Legacy schema field (Api-Revision: 2026-05-07). Remove after June 8, 2026. outputs: Optional[List[Dict[str, Any]]] = None + # New schema field (Api-Revision: 2026-05-20). + steps: Optional[List[Dict[str, Any]]] = None usage: Optional[Dict[str, Any]] = None _hidden_params: dict = PrivateAttr(default_factory=dict) @@ -1215,7 +1337,12 @@ class InteractionsAPIStreamingResponse(BaseLiteLLMOpenAIResponseObject): """ Streaming response chunk from the Interactions API. - Event types per OpenAPI spec: + New schema event types (Api-Revision: 2026-05-20): + - interaction.created, interaction.in_progress, interaction.completed, + interaction.requires_action + - step.start, step.delta, step.stop + + Legacy event types (Api-Revision: 2026-05-07, removed June 8 2026): - interaction.start, interaction.status_update, interaction.complete - content.start, content.delta, content.stop - error @@ -1230,9 +1357,17 @@ class InteractionsAPIStreamingResponse(BaseLiteLLMOpenAIResponseObject): created: Optional[str] = None updated: Optional[str] = None role: Optional[str] = None + # Legacy schema field (Api-Revision: 2026-05-07). Remove after June 8, 2026. outputs: Optional[List[Dict[str, Any]]] = None + # New schema field (Api-Revision: 2026-05-20). + steps: Optional[List[Dict[str, Any]]] = None usage: Optional[Dict[str, Any]] = None delta: Optional[Dict[str, Any]] = None + # New schema streaming fields + index: Optional[int] = None + step: Optional[Dict[str, Any]] = None + interaction_id: Optional[str] = None + interaction: Optional[Dict[str, Any]] = None _hidden_params: dict = PrivateAttr(default_factory=dict) diff --git a/litellm/types/llms/vertex_ai.py b/litellm/types/llms/vertex_ai.py index b357b64156d..a1d53978761 100644 --- a/litellm/types/llms/vertex_ai.py +++ b/litellm/types/llms/vertex_ai.py @@ -16,15 +16,15 @@ GeminiEmbeddingInput = Union[EmbeddingInput, List[List[str]]] class FunctionResponse(TypedDict, total=False): # `id` correlates this response with the originating `functionCall` part. - # Required by Gemini 3.5+ for strict function-calling response matching. + # Supported on Google AI Studio Gemini 3.5+; Vertex AI rejects this field. id: str name: Required[str] response: Optional[dict] class FunctionCall(TypedDict, total=False): - # `id` is returned by Gemini 3.5+ to correlate the corresponding - # `functionResponse`. Older Gemini models omit this field. + # `id` correlates the corresponding `functionResponse` on Google AI Studio + # Gemini 3.5+. Vertex AI and older Gemini models omit/reject this field. id: str name: Required[str] args: Optional[dict] @@ -52,8 +52,8 @@ class PartType(TypedDict, total=False): class HttpxFunctionCall(TypedDict, total=False): - # `id` is returned by Gemini 3.5+ to correlate the corresponding - # `functionResponse`. Older Gemini models omit this field. + # `id` correlates the corresponding `functionResponse` on Google AI Studio + # Gemini 3.5+. Vertex AI and older Gemini models omit/reject this field. id: str name: Required[str] args: dict diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 27d6a59740f..bda94e4768f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -27296,6 +27296,58 @@ "supports_web_search": true, "tpm": 800000 }, + "openrouter/google/gemini-3.1-flash-lite": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_per_audio_token": 5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_pdf_size_mb": 30, + "max_tokens": 65536, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 1.5e-06, + "output_cost_per_token": 1.5e-06, + "rpm": 2000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-3.1-flash-lite", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_audio_output": false, + "supports_code_execution": true, + "supports_file_search": true, + "supports_function_calling": true, + "supports_parallel_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_url_context": true, + "supports_video_input": true, + "supports_vision": true, + "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, @@ -28105,10 +28157,10 @@ "supports_tool_choice": true }, "openrouter/xiaomi/mimo-v2-flash": { - "input_cost_per_token": 9e-08, - "output_cost_per_token": 2.9e-07, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 3e-07, "cache_creation_input_token_cost": 0.0, - "cache_read_input_token_cost": 0.0, + "cache_read_input_token_cost": 1e-08, "litellm_provider": "openrouter", "max_input_tokens": 262144, "max_output_tokens": 16384, @@ -28118,7 +28170,43 @@ "supports_tool_choice": true, "supports_reasoning": true, "supports_vision": false, - "supports_prompt_caching": false + "supports_prompt_caching": true + }, + "openrouter/xiaomi/mimo-v2.5-pro": { + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3e-06, + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 2e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 1048576, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": false, + "supports_response_schema": true, + "supports_prompt_caching": true + }, + "openrouter/xiaomi/mimo-v2.5": { + "input_cost_per_token": 4e-07, + "output_cost_per_token": 2e-06, + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 8e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": true, + "supports_audio_input": true, + "supports_video_input": true, + "supports_response_schema": true, + "supports_prompt_caching": true }, "openrouter/z-ai/glm-4.7": { "input_cost_per_token": 4e-07, diff --git a/pyproject.toml b/pyproject.toml index 70681c4ed6c..b7bae873a46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -287,6 +287,12 @@ paths_to_mutate = [ ] tests_dir = [ "tests/test_litellm/proxy/management_endpoints/", + # PR1 (key Tier-1) behavior-pinning suite. Manual mutmut runs + # (.github/workflows/mutation-test.yml) include this directory so the + # behavior matrix contributes to mutation-score signal alongside the + # legacy mock suite. See tests/proxy_behavior/management/README.md + # for the G5 triage protocol. + "tests/proxy_behavior/management/", ] also_copy = [ "litellm/", diff --git a/tests/llm_responses_api_testing/test_anthropic_responses_api.py b/tests/llm_responses_api_testing/test_anthropic_responses_api.py index 6537f67acb9..68ff22e8938 100644 --- a/tests/llm_responses_api_testing/test_anthropic_responses_api.py +++ b/tests/llm_responses_api_testing/test_anthropic_responses_api.py @@ -3,7 +3,7 @@ import sys import pytest import asyncio from typing import Optional -from unittest.mock import patch, AsyncMock +from unittest.mock import patch, AsyncMock, MagicMock from litellm.responses.litellm_completion_transformation.handler import ( LiteLLMCompletionTransformationHandler, ) @@ -130,6 +130,26 @@ def test_multiturn_tool_calls(): print("follow_up_response=", follow_up_response) +def test_response_api_handler_merges_metadata_and_service_tier_without_error(): + """Sync path must merge kwargs like async; double-splat raises TypeError.""" + handler = LiteLLMCompletionTransformationHandler() + + with patch("litellm.completion", new_callable=MagicMock) as mock_completion: + mock_completion.return_value = ModelResponse( + id="id", created=0, model="test", object="chat.completion", choices=[] + ) + handler.response_api_handler( + model="test", + input="hi", + responses_api_request={}, + metadata={"trace": "abc"}, + service_tier="auto", + ) + assert mock_completion.call_count == 1 + assert mock_completion.call_args.kwargs["metadata"] == {"trace": "abc"} + assert mock_completion.call_args.kwargs["service_tier"] == "auto" + + @pytest.mark.asyncio async def test_async_response_api_handler_merges_trace_id_without_error(): handler = LiteLLMCompletionTransformationHandler() @@ -158,3 +178,39 @@ async def test_async_response_api_handler_merges_trace_id_without_error(): assert ( mock_acompletion.call_args.kwargs["litellm_trace_id"] == "session-trace" ) + + +@pytest.mark.asyncio +async def test_aresponses_forwards_timeout_to_acompletion(): + """Regression test: timeout passed to aresponses() must reach acompletion() + on the completion transformation path (Anthropic, Bedrock, Vertex etc.). + + Previously, `timeout` was a named param of `responses()` but was NOT + forwarded to `litellm_completion_transformation_handler.response_api_handler`, + so it was silently dropped — `Router(timeout=N)` was a no-op for Anthropic + and similar providers, with calls falling back to the provider SDK default + (~600s for Anthropic). + """ + with patch("litellm.acompletion", new_callable=AsyncMock) as mock_acompletion: + mock_acompletion.return_value = ModelResponse( + id="id", + created=0, + model="anthropic/claude-sonnet-4-5", + object="chat.completion", + choices=[], + ) + + await litellm.aresponses( + model="anthropic/claude-sonnet-4-5", + input="hello", + timeout=42, + api_key="sk-ant-fake", + ) + + assert mock_acompletion.call_count == 1 + forwarded_timeout = mock_acompletion.call_args.kwargs.get("timeout") + assert forwarded_timeout == 42, ( + f"timeout was not forwarded to acompletion (got {forwarded_timeout!r}); " + "this means Router(timeout=N) silently fails for providers on the " + "completion transformation path." + ) diff --git a/tests/llm_translation/realtime/base_realtime_tests.py b/tests/llm_translation/realtime/base_realtime_tests.py index 1d55f13b00d..f1c42659007 100644 --- a/tests/llm_translation/realtime/base_realtime_tests.py +++ b/tests/llm_translation/realtime/base_realtime_tests.py @@ -79,7 +79,7 @@ class RealTimeWebSocketClient: def _is_initial_event(self, msg_type: str) -> bool: """Check if message type is an initial connection event""" - # OpenAI sends "session.created", xAI sends "conversation.created" + # OpenAI and xAI send "session.created"; some providers send "conversation.created" return msg_type in ["session.created", "conversation.created"] async def receive_text(self): diff --git a/tests/llm_translation/realtime/test_xai_realtime.py b/tests/llm_translation/realtime/test_xai_realtime.py index 0bb7a59bb1a..86d0ebe3a3c 100644 --- a/tests/llm_translation/realtime/test_xai_realtime.py +++ b/tests/llm_translation/realtime/test_xai_realtime.py @@ -19,8 +19,8 @@ class TestXAIRealtime(BaseRealtimeTest): """ E2E tests for xAI Realtime API. - xAI's Grok Voice Agent API is OpenAI-compatible but uses: - - Different initial event: "conversation.created" instead of "session.created" + xAI's Grok Voice Agent API is OpenAI-compatible: + - Initial event: "session.created" (matches OpenAI) - Different endpoint: wss://api.x.ai/v1/realtime - Model: grok-4-1-fast-non-reasoning """ @@ -32,4 +32,4 @@ class TestXAIRealtime(BaseRealtimeTest): return "XAI_API_KEY" def get_initial_event_type(self) -> str: - return "conversation.created" + return "session.created" diff --git a/tests/local_testing/conftest.py b/tests/local_testing/conftest.py index 401a1fb85ec..3c959ee85d1 100644 --- a/tests/local_testing/conftest.py +++ b/tests/local_testing/conftest.py @@ -31,10 +31,8 @@ import litellm # cassette state the branch is being tested with. from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap -_local_cost_map = GetModelCostMap.load_local_model_cost_map() -for _k, _v in _local_cost_map.items(): +for _k, _v in GetModelCostMap.load_local_model_cost_map().items(): litellm.model_cost.setdefault(_k, _v) -del _local_cost_map from tests._vcr_conftest_common import ( # noqa: E402,F401 VerboseReporterState, diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py index 1274cad66af..59ffb6b15cf 100644 --- a/tests/mcp_tests/test_mcp_server.py +++ b/tests/mcp_tests/test_mcp_server.py @@ -382,6 +382,11 @@ async def test_mcp_http_transport_tool_not_found(): } ) + # Mapping populated for this server but not for the requested tool + test_manager.tool_name_to_mcp_server_name_mapping["gmail_send_email"] = ( + "test_http_server" + ) + # Try to call a tool that doesn't exist in mapping with pytest.raises(ValueError, match="Tool nonexistent_tool not found"): await test_manager.call_tool( @@ -944,6 +949,7 @@ async def test_get_tools_from_mcp_servers(): extra_headers=None, add_prefix=False, raw_headers=None, + user_api_key_auth=None, ): if server.server_id == "server1_id": return [mock_tool_1] @@ -1919,6 +1925,7 @@ async def test_get_tools_for_single_server(): extra_headers=None, add_prefix=False, raw_headers=None, + user_api_key_auth=None, ) # Verify the result diff --git a/tests/proxy_behavior/__init__.py b/tests/proxy_behavior/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/proxy_behavior/management/__init__.py b/tests/proxy_behavior/management/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/proxy_behavior/management/actors.py b/tests/proxy_behavior/management/actors.py new file mode 100644 index 00000000000..1bcf8ed474d --- /dev/null +++ b/tests/proxy_behavior/management/actors.py @@ -0,0 +1,257 @@ +"""8-actor read-world seed for the authz matrix tests.""" + +import enum +import uuid +from dataclasses import dataclass +from typing import Any, Dict + +from prisma import Json + +from litellm.proxy._types import LitellmUserRoles +from litellm.proxy.utils import PrismaClient, hash_token + + +class Actor(str, enum.Enum): + PROXY_ADMIN = "proxy_admin" + ORG_ADMIN = "org_admin" + TEAM_ADMIN = "team_admin" + INTERNAL_USER = "internal_user" + OWNER = "owner" + UNRELATED_SAME_ORG = "unrelated_same_org" + CROSS_ORG_USER = "cross_org_user" + SERVICE_ACCOUNT = "service_account" + + +PREFIX = "behavior-pin-" +ORG_A = PREFIX + "org-a" +ORG_B = PREFIX + "org-b" +TEAM_ALPHA = PREFIX + "team-alpha" +TEAM_BETA = PREFIX + "team-beta" +BUDGET_ID = PREFIX + "budget" + + +@dataclass(frozen=True) +class SeededKey: + user_id: str + cleartext: str + hashed: str + + +@dataclass(frozen=True) +class World: + org_a_id: str + org_b_id: str + team_alpha_id: str + team_beta_id: str + keys: Dict[Actor, SeededKey] + + +def _new_clear_key() -> str: + return "sk-" + uuid.uuid4().hex + + +def _actor_profile() -> Dict[Actor, Dict[str, Any]]: + return { + Actor.PROXY_ADMIN: { + "user_role": LitellmUserRoles.PROXY_ADMIN.value, + "team_id": None, + "organization_id": None, + }, + Actor.ORG_ADMIN: { + "user_role": LitellmUserRoles.ORG_ADMIN.value, + "team_id": None, + "organization_id": ORG_A, + }, + Actor.TEAM_ADMIN: { + "user_role": LitellmUserRoles.INTERNAL_USER.value, + "team_id": TEAM_ALPHA, + "organization_id": ORG_A, + }, + Actor.INTERNAL_USER: { + "user_role": LitellmUserRoles.INTERNAL_USER.value, + "team_id": TEAM_ALPHA, + "organization_id": ORG_A, + }, + Actor.OWNER: { + "user_role": LitellmUserRoles.INTERNAL_USER.value, + "team_id": TEAM_ALPHA, + "organization_id": ORG_A, + }, + Actor.UNRELATED_SAME_ORG: { + "user_role": LitellmUserRoles.INTERNAL_USER.value, + "team_id": TEAM_ALPHA, + "organization_id": ORG_A, + }, + Actor.CROSS_ORG_USER: { + "user_role": LitellmUserRoles.INTERNAL_USER.value, + "team_id": TEAM_BETA, + "organization_id": ORG_B, + }, + Actor.SERVICE_ACCOUNT: { + "user_role": LitellmUserRoles.INTERNAL_USER.value, + "team_id": TEAM_ALPHA, + "organization_id": ORG_A, + }, + } + + +async def _wipe_world(prisma: PrismaClient) -> None: + await prisma.db.litellm_verificationtoken.delete_many( + where={"user_id": {"startswith": PREFIX}} + ) + await prisma.db.litellm_organizationmembership.delete_many( + where={"user_id": {"startswith": PREFIX}} + ) + await prisma.db.litellm_teammembership.delete_many( + where={"user_id": {"startswith": PREFIX}} + ) + await prisma.db.litellm_usertable.delete_many( + where={"user_id": {"startswith": PREFIX}} + ) + await prisma.db.litellm_teamtable.delete_many( + where={"team_id": {"startswith": PREFIX}} + ) + await prisma.db.litellm_organizationtable.delete_many( + where={"organization_id": {"startswith": PREFIX}} + ) + await prisma.db.litellm_budgettable.delete_many(where={"budget_id": BUDGET_ID}) + + +async def seed_world(prisma: PrismaClient) -> World: + await _wipe_world(prisma) + + await prisma.db.litellm_budgettable.create( + data={ + "budget_id": BUDGET_ID, + "created_by": "behavior-pin-seeder", + "updated_by": "behavior-pin-seeder", + } + ) + + for org_id, alias in [(ORG_A, "alpha"), (ORG_B, "beta")]: + await prisma.db.litellm_organizationtable.create( + data={ + "organization_id": org_id, + "organization_alias": alias, + "budget_id": BUDGET_ID, + "created_by": "behavior-pin-seeder", + "updated_by": "behavior-pin-seeder", + } + ) + + profiles = _actor_profile() + user_ids: Dict[Actor, str] = {actor: PREFIX + actor.value for actor in Actor} + + for actor, profile in profiles.items(): + teams_list = [profile["team_id"]] if profile["team_id"] else [] + await prisma.db.litellm_usertable.create( + data={ + "user_id": user_ids[actor], + "user_role": profile["user_role"], + "team_id": profile["team_id"], + "organization_id": profile["organization_id"], + "teams": teams_list, + } + ) + + # _get_user_in_team in key_management_endpoints.py walks members_with_roles + # (a JSON list of {user_id, role}), not the String[] members column — + # populate both to match what /team/new produces. + await prisma.db.litellm_teamtable.create( + data={ + "team_id": TEAM_ALPHA, + "team_alias": "alpha-1", + "organization_id": ORG_A, + "admins": [user_ids[Actor.TEAM_ADMIN]], + "members": [ + user_ids[Actor.TEAM_ADMIN], + user_ids[Actor.INTERNAL_USER], + user_ids[Actor.OWNER], + user_ids[Actor.UNRELATED_SAME_ORG], + user_ids[Actor.SERVICE_ACCOUNT], + ], + "members_with_roles": Json( + [ + {"user_id": user_ids[Actor.TEAM_ADMIN], "role": "admin"}, + {"user_id": user_ids[Actor.INTERNAL_USER], "role": "user"}, + {"user_id": user_ids[Actor.OWNER], "role": "user"}, + {"user_id": user_ids[Actor.UNRELATED_SAME_ORG], "role": "user"}, + {"user_id": user_ids[Actor.SERVICE_ACCOUNT], "role": "user"}, + ] + ), + } + ) + await prisma.db.litellm_teamtable.create( + data={ + "team_id": TEAM_BETA, + "team_alias": "beta-1", + "organization_id": ORG_B, + "admins": [], + "members": [user_ids[Actor.CROSS_ORG_USER]], + "members_with_roles": Json( + [ + {"user_id": user_ids[Actor.CROSS_ORG_USER], "role": "user"}, + ] + ), + } + ) + + for actor, org_id, role in [ + (Actor.ORG_ADMIN, ORG_A, "org_admin"), + (Actor.TEAM_ADMIN, ORG_A, "internal_user"), + (Actor.INTERNAL_USER, ORG_A, "internal_user"), + (Actor.OWNER, ORG_A, "internal_user"), + (Actor.UNRELATED_SAME_ORG, ORG_A, "internal_user"), + (Actor.SERVICE_ACCOUNT, ORG_A, "internal_user"), + (Actor.CROSS_ORG_USER, ORG_B, "internal_user"), + ]: + await prisma.db.litellm_organizationmembership.create( + data={ + "user_id": user_ids[actor], + "organization_id": org_id, + "user_role": role, + } + ) + + for actor, team_id in [ + (Actor.TEAM_ADMIN, TEAM_ALPHA), + (Actor.INTERNAL_USER, TEAM_ALPHA), + (Actor.OWNER, TEAM_ALPHA), + (Actor.UNRELATED_SAME_ORG, TEAM_ALPHA), + (Actor.SERVICE_ACCOUNT, TEAM_ALPHA), + (Actor.CROSS_ORG_USER, TEAM_BETA), + ]: + await prisma.db.litellm_teammembership.create( + data={"user_id": user_ids[actor], "team_id": team_id} + ) + + keys: Dict[Actor, SeededKey] = {} + for actor, profile in profiles.items(): + cleartext = _new_clear_key() + hashed = hash_token(cleartext) + token_data: Dict[str, Any] = { + "token": hashed, + "key_name": PREFIX + actor.value + "-key", + "user_id": user_ids[actor], + # LiteLLM_VerificationTokenView's models field rejects NULL even + # though the column is nullable in Postgres. + "models": [], + } + if profile["team_id"]: + token_data["team_id"] = profile["team_id"] + if profile["organization_id"]: + token_data["organization_id"] = profile["organization_id"] + if actor == Actor.SERVICE_ACCOUNT: + token_data["metadata"] = Json({"service_account_id": user_ids[actor]}) + await prisma.db.litellm_verificationtoken.create(data=token_data) + keys[actor] = SeededKey( + user_id=user_ids[actor], cleartext=cleartext, hashed=hashed + ) + + return World( + org_a_id=ORG_A, + org_b_id=ORG_B, + team_alpha_id=TEAM_ALPHA, + team_beta_id=TEAM_BETA, + keys=keys, + ) diff --git a/tests/proxy_behavior/management/conftest.py b/tests/proxy_behavior/management/conftest.py new file mode 100644 index 00000000000..d69067ae5df --- /dev/null +++ b/tests/proxy_behavior/management/conftest.py @@ -0,0 +1,156 @@ +"""Session-scoped async ASGI client for HTTP-boundary behavior tests.""" + +import os +import tempfile +import uuid +from dataclasses import dataclass +from typing import Any, AsyncIterator, Dict, Optional + +import httpx +import pytest_asyncio +import yaml + + +MASTER_KEY = "sk-1234" +SCRATCH_PREFIX = "scratch-" + + +def _write_minimal_proxy_config() -> str: + config = { + "general_settings": {"master_key": MASTER_KEY}, + "litellm_settings": {}, + } + database_url = os.environ.get("DATABASE_URL") + if database_url: + config["general_settings"]["database_url"] = database_url + f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) + yaml.dump(config, f) + f.close() + return f.name + + +@pytest_asyncio.fixture(scope="session") +async def proxy_app(): + from litellm.proxy import proxy_server + from litellm.proxy.proxy_server import ( + app, + cleanup_router_config_variables, + initialize, + proxy_startup_event, + ) + + cleanup_router_config_variables() + config_path = _write_minimal_proxy_config() + + # proxy_startup_event re-reads master_key from LITELLM_MASTER_KEY and + # unconditionally overwrites the global, even when initialize() already + # set it from the config YAML. Force (not setdefault) both vars: an + # ambient LITELLM_MASTER_KEY with a different value would make the proxy + # authenticate on that key while the tests still send MASTER_KEY. + os.environ["LITELLM_MASTER_KEY"] = MASTER_KEY + os.environ["CONFIG_FILE_PATH"] = config_path + + await initialize(config=config_path) + + # /key/regenerate is gated behind premium_user; flipping it lets the matrix + # pin authz behavior instead of the licensing gate. + proxy_server.premium_user = True + + async with proxy_startup_event(app): + proxy_server.premium_user = True # lifespan re-runs _license_check + # The lifespan fires check_view_exists() as a background task; on a + # fresh DB the first auth call races it and resolves user_id=None. + if proxy_server.prisma_client is not None: + await proxy_server.prisma_client.check_view_exists() + yield app + + +@pytest_asyncio.fixture(scope="session") +async def proxy_client(proxy_app) -> AsyncIterator[httpx.AsyncClient]: + transport = httpx.ASGITransport(app=proxy_app) + async with httpx.AsyncClient( + transport=transport, base_url="http://testserver" + ) as client: + yield client + + +@pytest_asyncio.fixture(scope="session") +async def prisma(proxy_app): + from litellm.proxy import proxy_server + + assert proxy_server.prisma_client is not None + return proxy_server.prisma_client + + +@pytest_asyncio.fixture(scope="session") +async def world(prisma): + from .actors import seed_world + + return await seed_world(prisma) + + +@dataclass(frozen=True) +class Scratch: + prefix: str + + def tag(self, suffix: str = "") -> str: + return f"{self.prefix}-{suffix}" if suffix else self.prefix + + +async def create_scratch_key( + proxy_client, + seeder_cleartext: str, + scratch_prefix: str, + *, + user_id: str, + team_id: Optional[str] = None, + organization_id: Optional[str] = None, +) -> str: + """Seed a scratch-tagged key via /key/generate; returns its cleartext. + + Shared by the write-scenario matrices (key update/regenerate/delete). + """ + body: Dict[str, Any] = {"key_alias": scratch_prefix, "user_id": user_id} + if team_id is not None: + body["team_id"] = team_id + if organization_id is not None: + body["organization_id"] = organization_id + resp = await proxy_client.post( + "/key/generate", + headers={"Authorization": f"Bearer {seeder_cleartext}"}, + json=body, + ) + assert resp.status_code == 200, f"setup failed: {resp.text}" + return resp.json()["key"] + + +@pytest_asyncio.fixture +async def scratch(prisma): + handle = Scratch(prefix=f"{SCRATCH_PREFIX}{uuid.uuid4().hex[:12]}") + try: + yield handle + finally: + # Children before parents to avoid FK violations. + await prisma.db.litellm_verificationtoken.delete_many( + where={ + "OR": [ + {"key_alias": {"startswith": handle.prefix}}, + {"key_name": {"startswith": handle.prefix}}, + ] + } + ) + await prisma.db.litellm_teammembership.delete_many( + where={"team_id": {"startswith": handle.prefix}} + ) + await prisma.db.litellm_organizationmembership.delete_many( + where={"user_id": {"startswith": handle.prefix}} + ) + await prisma.db.litellm_teamtable.delete_many( + where={"team_id": {"startswith": handle.prefix}} + ) + await prisma.db.litellm_usertable.delete_many( + where={"user_id": {"startswith": handle.prefix}} + ) + await prisma.db.litellm_budgettable.delete_many( + where={"budget_id": {"startswith": handle.prefix}} + ) diff --git a/tests/proxy_behavior/management/test_key_delete.py b/tests/proxy_behavior/management/test_key_delete.py new file mode 100644 index 00000000000..05844ac0031 --- /dev/null +++ b/tests/proxy_behavior/management/test_key_delete.py @@ -0,0 +1,101 @@ +import pytest + +from litellm.proxy.utils import hash_token + +from .actors import TEAM_ALPHA, TEAM_BETA, Actor +from .conftest import create_scratch_key + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# Same-team peers can READ each other's keys (see test_key_info) but cannot +# DELETE them — delete is stricter than read. +_SCENARIOS = [ + ("self/proxy_admin", Actor.PROXY_ADMIN, "self", 200), + ("self/org_admin", Actor.ORG_ADMIN, "self", 401), + ("self/team_admin", Actor.TEAM_ADMIN, "self", 200), + ("self/internal_user", Actor.INTERNAL_USER, "self", 200), + ("self/owner", Actor.OWNER, "self", 200), + ("self/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "self", 200), + ("self/cross_org_user", Actor.CROSS_ORG_USER, "self", 200), + ("self/service_account", Actor.SERVICE_ACCOUNT, "self", 200), + ("owner_target/proxy_admin", Actor.PROXY_ADMIN, "owner", 200), + ("owner_target/org_admin", Actor.ORG_ADMIN, "owner", 401), + ("owner_target/team_admin", Actor.TEAM_ADMIN, "owner", 200), + ("owner_target/internal_user", Actor.INTERNAL_USER, "owner", 403), + ("owner_target/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "owner", 403), + ("owner_target/cross_org_user", Actor.CROSS_ORG_USER, "owner", 403), + ("owner_target/service_account", Actor.SERVICE_ACCOUNT, "owner", 403), + ("cross_org_target/proxy_admin", Actor.PROXY_ADMIN, "cross_org", 200), + ("cross_org_target/org_admin", Actor.ORG_ADMIN, "cross_org", 401), + ("cross_org_target/team_admin", Actor.TEAM_ADMIN, "cross_org", 403), + ("cross_org_target/owner", Actor.OWNER, "cross_org", 403), + ("cross_org_target/cross_org_user", Actor.CROSS_ORG_USER, "cross_org", 200), + ("cross_org_target/service_account", Actor.SERVICE_ACCOUNT, "cross_org", 403), +] + + +@pytest.mark.parametrize( + "actor,target_shape,expected_status", + [(a, t, s) for (_id, a, t, s) in _SCENARIOS], + ids=[s[0] for s in _SCENARIOS], +) +async def test_key_delete_authz_matrix( + actor: Actor, + target_shape: str, + expected_status: int, + proxy_client, + prisma, + scratch, + world, +): + caller = world.keys[actor] + seeder = world.keys[Actor.PROXY_ADMIN].cleartext + + if target_shape == "self": + target_cleartext = await create_scratch_key( + proxy_client, seeder, scratch.prefix, user_id=caller.user_id + ) + elif target_shape == "owner": + target_cleartext = await create_scratch_key( + proxy_client, + seeder, + scratch.prefix, + user_id=world.keys[Actor.OWNER].user_id, + team_id=TEAM_ALPHA, + ) + elif target_shape == "cross_org": + target_cleartext = await create_scratch_key( + proxy_client, + seeder, + scratch.prefix, + user_id=world.keys[Actor.CROSS_ORG_USER].user_id, + team_id=TEAM_BETA, + ) + else: + pytest.fail(f"unknown target_shape={target_shape}") + + target_hashed = hash_token(target_cleartext) + + resp = await proxy_client.post( + "/key/delete", + headers={"Authorization": f"Bearer {caller.cleartext}"}, + json={"keys": [target_cleartext]}, + ) + assert ( + resp.status_code == expected_status + ), f"{actor.value} {target_shape}: {resp.status_code} {resp.text}" + + row = await prisma.db.litellm_verificationtoken.find_unique( + where={"token": target_hashed} + ) + auth_check = await proxy_client.get( + "/key/info", headers={"Authorization": f"Bearer {target_cleartext}"} + ) + + if expected_status == 200: + # Hard- or soft-delete both produce a 401 on subsequent auth. + assert auth_check.status_code == 401 + else: + assert row is not None, f"{actor.value}: denied but row vanished" + assert auth_check.status_code == 200 diff --git a/tests/proxy_behavior/management/test_key_generate.py b/tests/proxy_behavior/management/test_key_generate.py new file mode 100644 index 00000000000..851de33d3ff --- /dev/null +++ b/tests/proxy_behavior/management/test_key_generate.py @@ -0,0 +1,70 @@ +from typing import Any, Dict + +import pytest + +from .actors import TEAM_ALPHA, TEAM_BETA, Actor + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# (id, actor, body_extras, expected_status). Status codes pinned to observed +# handler behavior — heterogeneous (200, 400, 401) because the handler routes +# denials through three different gates (role gate, user_id mismatch, team +# member permission). +_SCENARIOS = [ + ("self/proxy_admin", Actor.PROXY_ADMIN, {}, 200), + ("self/org_admin", Actor.ORG_ADMIN, {}, 401), + ("self/team_admin", Actor.TEAM_ADMIN, {}, 200), + ("self/internal_user", Actor.INTERNAL_USER, {}, 200), + ("self/owner", Actor.OWNER, {}, 200), + ("self/unrelated_same_org", Actor.UNRELATED_SAME_ORG, {}, 200), + ("self/cross_org_user", Actor.CROSS_ORG_USER, {}, 200), + ("self/service_account", Actor.SERVICE_ACCOUNT, {}, 200), + ("team_alpha/proxy_admin", Actor.PROXY_ADMIN, {"team_id": TEAM_ALPHA}, 200), + ("team_alpha/org_admin", Actor.ORG_ADMIN, {"team_id": TEAM_ALPHA}, 401), + ("team_alpha/team_admin", Actor.TEAM_ADMIN, {"team_id": TEAM_ALPHA}, 200), + ("team_alpha/internal_user", Actor.INTERNAL_USER, {"team_id": TEAM_ALPHA}, 401), + ("team_alpha/cross_org_user", Actor.CROSS_ORG_USER, {"team_id": TEAM_ALPHA}, 400), + ("team_beta/proxy_admin", Actor.PROXY_ADMIN, {"team_id": TEAM_BETA}, 200), + ("team_beta/org_admin", Actor.ORG_ADMIN, {"team_id": TEAM_BETA}, 401), + ("team_beta/team_admin", Actor.TEAM_ADMIN, {"team_id": TEAM_BETA}, 400), + ("team_beta/internal_user", Actor.INTERNAL_USER, {"team_id": TEAM_BETA}, 400), + ("team_beta/cross_org_user", Actor.CROSS_ORG_USER, {"team_id": TEAM_BETA}, 401), +] + + +@pytest.mark.parametrize( + "actor,body_extras,expected_status", + [(actor, body, expected) for (_id, actor, body, expected) in _SCENARIOS], + ids=[s[0] for s in _SCENARIOS], +) +async def test_key_generate_authz_matrix( + actor: Actor, + body_extras: Dict[str, Any], + expected_status: int, + proxy_client, + prisma, + scratch, + world, +): + seeded = world.keys[actor] + body: Dict[str, Any] = {"key_alias": scratch.prefix, **body_extras} + + resp = await proxy_client.post( + "/key/generate", + headers={"Authorization": f"Bearer {seeded.cleartext}"}, + json=body, + ) + assert ( + resp.status_code == expected_status + ), f"{actor.value} {body!r} → {resp.status_code}: {resp.text}" + + rows = await prisma.db.litellm_verificationtoken.find_many( + where={"key_alias": scratch.prefix} + ) + if expected_status == 200: + cleartext = resp.json()["key"] + assert cleartext.startswith("sk-") + assert len(rows) == 1 + else: + assert rows == [], f"{actor.value}: denied but row leaked" diff --git a/tests/proxy_behavior/management/test_key_info.py b/tests/proxy_behavior/management/test_key_info.py new file mode 100644 index 00000000000..ddcef9fd27b --- /dev/null +++ b/tests/proxy_behavior/management/test_key_info.py @@ -0,0 +1,74 @@ +import pytest + +from .actors import Actor + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# (id, actor, target_actor, expected_status). Targets are 3 fixed seeded keys +# representing the canonical relations: own, OWNER (same org_a/team_alpha), +# and CROSS_ORG_USER (org_b/team_beta). +# +# Notable pinned behaviors (intentionally surfaced, not endorsed): +# - ORG_ADMIN 403s on individual key info even within its own org — +# visibility is "your own keys" + "your team's keys", not "your org's keys". +# - Same-team peers (internal_user, unrelated_same_org, service_account) DO +# see each other's keys. +_SCENARIOS = [ + ("own/proxy_admin", Actor.PROXY_ADMIN, Actor.PROXY_ADMIN, 200), + ("own/org_admin", Actor.ORG_ADMIN, Actor.ORG_ADMIN, 200), + ("own/team_admin", Actor.TEAM_ADMIN, Actor.TEAM_ADMIN, 200), + ("own/internal_user", Actor.INTERNAL_USER, Actor.INTERNAL_USER, 200), + ("own/owner", Actor.OWNER, Actor.OWNER, 200), + ("own/unrelated_same_org", Actor.UNRELATED_SAME_ORG, Actor.UNRELATED_SAME_ORG, 200), + ("own/cross_org_user", Actor.CROSS_ORG_USER, Actor.CROSS_ORG_USER, 200), + ("own/service_account", Actor.SERVICE_ACCOUNT, Actor.SERVICE_ACCOUNT, 200), + ("owner_key/proxy_admin", Actor.PROXY_ADMIN, Actor.OWNER, 200), + ("owner_key/org_admin", Actor.ORG_ADMIN, Actor.OWNER, 403), + ("owner_key/team_admin", Actor.TEAM_ADMIN, Actor.OWNER, 200), + ("owner_key/internal_user", Actor.INTERNAL_USER, Actor.OWNER, 200), + ("owner_key/owner", Actor.OWNER, Actor.OWNER, 200), + ("owner_key/unrelated_same_org", Actor.UNRELATED_SAME_ORG, Actor.OWNER, 200), + ("owner_key/cross_org_user", Actor.CROSS_ORG_USER, Actor.OWNER, 403), + ("owner_key/service_account", Actor.SERVICE_ACCOUNT, Actor.OWNER, 200), + ("cross_org/proxy_admin", Actor.PROXY_ADMIN, Actor.CROSS_ORG_USER, 200), + ("cross_org/org_admin", Actor.ORG_ADMIN, Actor.CROSS_ORG_USER, 403), + ("cross_org/team_admin", Actor.TEAM_ADMIN, Actor.CROSS_ORG_USER, 403), + ("cross_org/internal_user", Actor.INTERNAL_USER, Actor.CROSS_ORG_USER, 403), + ("cross_org/owner", Actor.OWNER, Actor.CROSS_ORG_USER, 403), + ( + "cross_org/unrelated_same_org", + Actor.UNRELATED_SAME_ORG, + Actor.CROSS_ORG_USER, + 403, + ), + ("cross_org/cross_org_user", Actor.CROSS_ORG_USER, Actor.CROSS_ORG_USER, 200), + ("cross_org/service_account", Actor.SERVICE_ACCOUNT, Actor.CROSS_ORG_USER, 403), +] + + +@pytest.mark.parametrize( + "actor,target_actor,expected_status", + [(a, t, s) for (_id, a, t, s) in _SCENARIOS], + ids=[s[0] for s in _SCENARIOS], +) +async def test_key_info_authz_matrix( + actor: Actor, target_actor: Actor, expected_status: int, proxy_client, world +): + caller = world.keys[actor] + target = world.keys[target_actor] + + resp = await proxy_client.get( + f"/key/info?key={target.cleartext}", + headers={"Authorization": f"Bearer {caller.cleartext}"}, + ) + assert ( + resp.status_code == expected_status + ), f"{actor.value} → {target_actor.value}: {resp.status_code} {resp.text}" + + if expected_status == 200: + body = resp.json() + # The handler echoes back whatever ?key was passed (cleartext here), + # so accept either form — info.user_id is the canonical identity check. + assert body.get("key") in (target.cleartext, target.hashed) + assert body["info"].get("user_id") == target.user_id diff --git a/tests/proxy_behavior/management/test_key_list.py b/tests/proxy_behavior/management/test_key_list.py new file mode 100644 index 00000000000..bda8788c9a7 --- /dev/null +++ b/tests/proxy_behavior/management/test_key_list.py @@ -0,0 +1,63 @@ +from typing import FrozenSet + +import pytest + +from .actors import Actor + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# Pinned default visibility for /key/list (no filter params): each actor's +# expected set of seeded actor keys. +_VISIBILITY = { + Actor.PROXY_ADMIN: frozenset(Actor), + Actor.ORG_ADMIN: frozenset({Actor.ORG_ADMIN}), + Actor.TEAM_ADMIN: frozenset({Actor.TEAM_ADMIN}), + Actor.INTERNAL_USER: frozenset({Actor.INTERNAL_USER}), + Actor.OWNER: frozenset({Actor.OWNER}), + Actor.UNRELATED_SAME_ORG: frozenset({Actor.UNRELATED_SAME_ORG}), + Actor.CROSS_ORG_USER: frozenset({Actor.CROSS_ORG_USER}), + Actor.SERVICE_ACCOUNT: frozenset({Actor.SERVICE_ACCOUNT}), +} + + +async def _all_visible_hashes(proxy_client, caller_cleartext) -> set: + """Walk every /key/list page — size is capped at 100 by the endpoint, so a + single request can truncate PROXY_ADMIN's view on a non-fresh DB.""" + hashes: set = set() + page = 1 + while True: + resp = await proxy_client.get( + f"/key/list?page={page}&size=100", + headers={"Authorization": f"Bearer {caller_cleartext}"}, + ) + assert resp.status_code == 200, resp.text + body = resp.json() + for entry in body.get("keys", []): + tok = entry.get("token") if isinstance(entry, dict) else entry + if tok: + hashes.add(tok) + if page >= (body.get("total_pages") or 1): + return hashes + page += 1 + + +@pytest.mark.parametrize( + "actor,expected_visible", + list(_VISIBILITY.items()), + ids=[a.value for a in _VISIBILITY], +) +async def test_key_list_visibility( + actor: Actor, expected_visible: FrozenSet[Actor], proxy_client, world +): + caller = world.keys[actor] + hashed_to_actor = {world.keys[a].hashed: a for a in Actor} + + returned_hashes = await _all_visible_hashes(proxy_client, caller.cleartext) + visible_seeded = { + hashed_to_actor[h] for h in returned_hashes if h in hashed_to_actor + } + assert visible_seeded == set(expected_visible), ( + f"{actor.value}: expected {sorted(a.value for a in expected_visible)}, " + f"got {sorted(a.value for a in visible_seeded)}" + ) diff --git a/tests/proxy_behavior/management/test_key_regenerate.py b/tests/proxy_behavior/management/test_key_regenerate.py new file mode 100644 index 00000000000..a3289144eef --- /dev/null +++ b/tests/proxy_behavior/management/test_key_regenerate.py @@ -0,0 +1,117 @@ +import pytest + +from .actors import TEAM_ALPHA, TEAM_BETA, Actor +from .conftest import create_scratch_key + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# Most denials route through team_member_permission (401), unlike /key/update +# which goes through user_id-mismatch (403). The matrix surfaces that +# divergence between the two endpoints. +_SCENARIOS = [ + ("self/proxy_admin", Actor.PROXY_ADMIN, "self", 200), + ("self/org_admin", Actor.ORG_ADMIN, "self", 401), + ("self/team_admin", Actor.TEAM_ADMIN, "self", 200), + ("self/internal_user", Actor.INTERNAL_USER, "self", 200), + ("self/owner", Actor.OWNER, "self", 200), + ("self/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "self", 200), + ("self/cross_org_user", Actor.CROSS_ORG_USER, "self", 200), + ("self/service_account", Actor.SERVICE_ACCOUNT, "self", 200), + ("owner_target/proxy_admin", Actor.PROXY_ADMIN, "owner", 200), + ("owner_target/org_admin", Actor.ORG_ADMIN, "owner", 401), + ("owner_target/team_admin", Actor.TEAM_ADMIN, "owner", 200), + ("owner_target/internal_user", Actor.INTERNAL_USER, "owner", 401), + ("owner_target/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "owner", 401), + ("owner_target/cross_org_user", Actor.CROSS_ORG_USER, "owner", 401), + ("owner_target/service_account", Actor.SERVICE_ACCOUNT, "owner", 401), + ("cross_org_target/proxy_admin", Actor.PROXY_ADMIN, "cross_org", 200), + ("cross_org_target/org_admin", Actor.ORG_ADMIN, "cross_org", 401), + ("cross_org_target/team_admin", Actor.TEAM_ADMIN, "cross_org", 401), + ("cross_org_target/owner", Actor.OWNER, "cross_org", 401), + ("cross_org_target/cross_org_user", Actor.CROSS_ORG_USER, "cross_org", 401), + ("cross_org_target/service_account", Actor.SERVICE_ACCOUNT, "cross_org", 401), +] + + +async def _info(proxy_client, cleartext: str): + return await proxy_client.get( + "/key/info", headers={"Authorization": f"Bearer {cleartext}"} + ) + + +@pytest.mark.parametrize( + "actor,target_shape,expected_status", + [(a, t, s) for (_id, a, t, s) in _SCENARIOS], + ids=[s[0] for s in _SCENARIOS], +) +async def test_key_regenerate_authz_matrix( + actor: Actor, + target_shape: str, + expected_status: int, + proxy_client, + scratch, + world, +): + caller = world.keys[actor] + seeder = world.keys[Actor.PROXY_ADMIN].cleartext + + if target_shape == "self": + target_cleartext = await create_scratch_key( + proxy_client, seeder, scratch.prefix, user_id=caller.user_id + ) + elif target_shape == "owner": + target_cleartext = await create_scratch_key( + proxy_client, + seeder, + scratch.prefix, + user_id=world.keys[Actor.OWNER].user_id, + team_id=TEAM_ALPHA, + ) + elif target_shape == "cross_org": + target_cleartext = await create_scratch_key( + proxy_client, + seeder, + scratch.prefix, + user_id=world.keys[Actor.CROSS_ORG_USER].user_id, + team_id=TEAM_BETA, + ) + else: + pytest.fail(f"unknown target_shape={target_shape}") + + resp = await proxy_client.post( + "/key/regenerate", + headers={"Authorization": f"Bearer {caller.cleartext}"}, + json={"key": target_cleartext}, + ) + assert ( + resp.status_code == expected_status + ), f"{actor.value} {target_shape}: {resp.status_code} {resp.text}" + + if expected_status == 200: + new_cleartext = resp.json()["key"] + assert new_cleartext.startswith("sk-") and new_cleartext != target_cleartext + assert (await _info(proxy_client, target_cleartext)).status_code == 401 + assert (await _info(proxy_client, new_cleartext)).status_code == 200 + else: + # Denied: rotation must not have leaked — old cleartext still works. + assert (await _info(proxy_client, target_cleartext)).status_code == 200 + + +async def test_key_path_regenerate_smoke(proxy_client, scratch, world): + """Pins that POST /key/{key:path}/regenerate shares the same handler.""" + caller = world.keys[Actor.PROXY_ADMIN] + target_cleartext = await create_scratch_key( + proxy_client, caller.cleartext, scratch.prefix, user_id=caller.user_id + ) + + resp = await proxy_client.post( + f"/key/{target_cleartext}/regenerate", + headers={"Authorization": f"Bearer {caller.cleartext}"}, + json={}, + ) + assert resp.status_code == 200, resp.text + new_cleartext = resp.json()["key"] + assert new_cleartext.startswith("sk-") and new_cleartext != target_cleartext + assert (await _info(proxy_client, target_cleartext)).status_code == 401 + assert (await _info(proxy_client, new_cleartext)).status_code == 200 diff --git a/tests/proxy_behavior/management/test_key_update.py b/tests/proxy_behavior/management/test_key_update.py new file mode 100644 index 00000000000..36ddefa5750 --- /dev/null +++ b/tests/proxy_behavior/management/test_key_update.py @@ -0,0 +1,100 @@ +import pytest + +from litellm.proxy.utils import hash_token + +from .actors import TEAM_ALPHA, TEAM_BETA, Actor +from .conftest import create_scratch_key + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# (id, actor, target_shape, expected_status). Pinned against current gating: +# proxy_admin bypasses; org_admin is blocked by an early role gate (401); +# every other (INTERNAL_USER-roled) actor hits user_id-mismatch 403, no-team- +# admin 403, or team_member_permission 401 depending on target / membership. +_SCENARIOS = [ + ("self/proxy_admin", Actor.PROXY_ADMIN, "self", 200), + ("self/org_admin", Actor.ORG_ADMIN, "self", 401), + ("self/team_admin", Actor.TEAM_ADMIN, "self", 403), + ("self/internal_user", Actor.INTERNAL_USER, "self", 403), + ("self/owner", Actor.OWNER, "self", 403), + ("self/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "self", 403), + ("self/cross_org_user", Actor.CROSS_ORG_USER, "self", 403), + ("self/service_account", Actor.SERVICE_ACCOUNT, "self", 403), + ("owner_target/proxy_admin", Actor.PROXY_ADMIN, "owner", 200), + ("owner_target/org_admin", Actor.ORG_ADMIN, "owner", 401), + ("owner_target/team_admin", Actor.TEAM_ADMIN, "owner", 403), + ("owner_target/internal_user", Actor.INTERNAL_USER, "owner", 403), + ("owner_target/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "owner", 403), + ("owner_target/cross_org_user", Actor.CROSS_ORG_USER, "owner", 403), + ("owner_target/service_account", Actor.SERVICE_ACCOUNT, "owner", 403), + ("cross_org_target/proxy_admin", Actor.PROXY_ADMIN, "cross_org", 200), + ("cross_org_target/org_admin", Actor.ORG_ADMIN, "cross_org", 401), + ("cross_org_target/team_admin", Actor.TEAM_ADMIN, "cross_org", 403), + ("cross_org_target/owner", Actor.OWNER, "cross_org", 403), + ("cross_org_target/cross_org_user", Actor.CROSS_ORG_USER, "cross_org", 401), + ("cross_org_target/service_account", Actor.SERVICE_ACCOUNT, "cross_org", 403), +] + +MARKER_MODEL = "behavior-pin-update-marker-model" + + +@pytest.mark.parametrize( + "actor,target_shape,expected_status", + [(a, t, s) for (_id, a, t, s) in _SCENARIOS], + ids=[s[0] for s in _SCENARIOS], +) +async def test_key_update_authz_matrix( + actor: Actor, + target_shape: str, + expected_status: int, + proxy_client, + prisma, + scratch, + world, +): + caller = world.keys[actor] + seeder = world.keys[Actor.PROXY_ADMIN].cleartext + + if target_shape == "self": + target_cleartext = await create_scratch_key( + proxy_client, seeder, scratch.prefix, user_id=caller.user_id + ) + elif target_shape == "owner": + target_cleartext = await create_scratch_key( + proxy_client, + seeder, + scratch.prefix, + user_id=world.keys[Actor.OWNER].user_id, + team_id=TEAM_ALPHA, + ) + elif target_shape == "cross_org": + target_cleartext = await create_scratch_key( + proxy_client, + seeder, + scratch.prefix, + user_id=world.keys[Actor.CROSS_ORG_USER].user_id, + team_id=TEAM_BETA, + ) + else: + pytest.fail(f"unknown target_shape={target_shape}") + + target_hashed = hash_token(target_cleartext) + + resp = await proxy_client.post( + "/key/update", + headers={"Authorization": f"Bearer {caller.cleartext}"}, + json={"key": target_cleartext, "models": [MARKER_MODEL]}, + ) + assert ( + resp.status_code == expected_status + ), f"{actor.value} {target_shape}: {resp.status_code} {resp.text}" + + row = await prisma.db.litellm_verificationtoken.find_unique( + where={"token": target_hashed} + ) + assert row is not None + if expected_status == 200: + assert row.models == [MARKER_MODEL] + else: + assert row.models != [MARKER_MODEL], "denied but row mutated" diff --git a/tests/proxy_behavior/management/test_no_management_imports.py b/tests/proxy_behavior/management/test_no_management_imports.py new file mode 100644 index 00000000000..f8c52a1c37e --- /dev/null +++ b/tests/proxy_behavior/management/test_no_management_imports.py @@ -0,0 +1,46 @@ +import pathlib +import re + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +BEHAVIOR_DIR = REPO_ROOT / "tests" / "proxy_behavior" + +FORBIDDEN_IMPORT = re.compile(r"^\s*from\s+litellm\.proxy\.management_endpoints\b") +FORBIDDEN_AUTH_MOCK = re.compile( + r"(?:mock\.[A-Za-z_]+|patch[a-z_]*)\([^)]*user_api_key_auth" +) +# This file is the only place the forbidden patterns appear as regex source; +# exclude it so it can describe what it forbids. +SELF = pathlib.Path(__file__).resolve() + + +def _iter_py_files(): + for path in BEHAVIOR_DIR.rglob("*.py"): + if path.resolve() != SELF: + yield path + + +def _scan(pattern): + violations = [] + for path in _iter_py_files(): + for lineno, line in enumerate(path.read_text().splitlines(), start=1): + if pattern.search(line): + violations.append( + f"{path.relative_to(REPO_ROOT)}:{lineno}: {line.strip()}" + ) + return violations + + +def test_no_management_endpoint_imports(): + violations = _scan(FORBIDDEN_IMPORT) + assert not violations, ( + "tests/proxy_behavior/ must not import from litellm.proxy.management_endpoints. " + "Violations:\n " + "\n ".join(violations) + ) + + +def test_no_user_api_key_auth_mocking(): + violations = _scan(FORBIDDEN_AUTH_MOCK) + assert not violations, ( + "tests/proxy_behavior/ must not mock user_api_key_auth. " + "Violations:\n " + "\n ".join(violations) + ) diff --git a/tests/proxy_behavior/management/test_scratch_teardown.py b/tests/proxy_behavior/management/test_scratch_teardown.py new file mode 100644 index 00000000000..689c60fc78a --- /dev/null +++ b/tests/proxy_behavior/management/test_scratch_teardown.py @@ -0,0 +1,31 @@ +import pytest + +from .conftest import MASTER_KEY, SCRATCH_PREFIX + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +# The two tests run in file order: _a writes a scratch-tagged key and asserts +# it lands; _b runs after _a's fixture teardown and asserts no scratch row +# survived. A leak in either direction fails _b on the next collection. + + +async def test_a_scratch_key_lands_in_db(proxy_client, prisma, scratch): + resp = await proxy_client.post( + "/key/generate", + headers={"Authorization": f"Bearer {MASTER_KEY}"}, + json={"key_alias": scratch.prefix}, + ) + assert resp.status_code == 200, resp.text + + rows = await prisma.db.litellm_verificationtoken.find_many( + where={"key_alias": scratch.prefix} + ) + assert len(rows) == 1 + + +async def test_b_scratch_namespace_is_clean(prisma): + rows = await prisma.db.litellm_verificationtoken.find_many( + where={"key_alias": {"startswith": SCRATCH_PREFIX}} + ) + assert rows == [] diff --git a/tests/proxy_behavior/management/test_smoke.py b/tests/proxy_behavior/management/test_smoke.py new file mode 100644 index 00000000000..4e90986ad9f --- /dev/null +++ b/tests/proxy_behavior/management/test_smoke.py @@ -0,0 +1,28 @@ +import pytest + +from .conftest import MASTER_KEY + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +async def test_liveliness(proxy_client): + resp = await proxy_client.get("/health/liveliness") + assert resp.status_code == 200 + + +async def test_key_generate_lands_in_db(proxy_client, prisma, scratch): + from litellm.proxy.utils import hash_token + + resp = await proxy_client.post( + "/key/generate", + headers={"Authorization": f"Bearer {MASTER_KEY}"}, + json={"key_alias": scratch.prefix}, + ) + assert resp.status_code == 200, resp.text + cleartext = resp.json()["key"] + assert cleartext.startswith("sk-") + + hashed = hash_token(cleartext) + row = await prisma.db.litellm_verificationtoken.find_unique(where={"token": hashed}) + assert row is not None + assert row.token == hashed != cleartext diff --git a/tests/proxy_behavior/management/test_world_seed.py b/tests/proxy_behavior/management/test_world_seed.py new file mode 100644 index 00000000000..00f9540c9c3 --- /dev/null +++ b/tests/proxy_behavior/management/test_world_seed.py @@ -0,0 +1,30 @@ +import pytest + +from .actors import Actor + +pytestmark = pytest.mark.asyncio(loop_scope="session") + + +@pytest.mark.parametrize("actor", list(Actor), ids=[a.value for a in Actor]) +async def test_each_actor_can_self_info(actor, proxy_client, world): + seeded = world.keys[actor] + resp = await proxy_client.get( + "/key/info", + headers={"Authorization": f"Bearer {seeded.cleartext}"}, + ) + assert resp.status_code == 200, f"{actor.value}: {resp.text}" + body = resp.json() + assert body.get("key") == seeded.hashed + assert body["info"].get("user_id") == seeded.user_id + + +async def test_proxy_admin_actor_can_create_keys_for_others(proxy_client, world): + seeder = world.keys[Actor.PROXY_ADMIN] + target_user_id = world.keys[Actor.OWNER].user_id + + resp = await proxy_client.post( + "/key/generate", + headers={"Authorization": f"Bearer {seeder.cleartext}"}, + json={"key_alias": "smoke-proxy-admin-bypass", "user_id": target_user_id}, + ) + assert resp.status_code == 200, resp.text diff --git a/tests/proxy_unit_tests/test_user_api_key_auth.py b/tests/proxy_unit_tests/test_user_api_key_auth.py index 210347aaf94..958b028c542 100644 --- a/tests/proxy_unit_tests/test_user_api_key_auth.py +++ b/tests/proxy_unit_tests/test_user_api_key_auth.py @@ -915,6 +915,36 @@ async def test_user_api_key_auth_websocket(): ) +@pytest.mark.asyncio +async def test_user_api_key_auth_websocket_carries_asgi_path(): + """ + The synthetic Request must carry the ASGI scope's ``path`` so + ``get_request_route`` returns the real WebSocket path, not a value + reconstructed from the (Host-poisonable) ``websocket.url``. + """ + from litellm.proxy.auth.user_api_key_auth import user_api_key_auth_websocket + + mock_websocket = MagicMock(spec=WebSocket) + mock_websocket.query_params = {"model": "some_model"} + mock_websocket.headers = {"authorization": "Bearer some_api_key"} + mock_websocket.scope = { + "type": "websocket", + "path": "/v1/realtime", + "root_path": "", + "headers": [(b"authorization", b"Bearer some_api_key")], + } + mock_websocket.url = URL(url="/v1/realtime") + + with patch( + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True + ) as mock_user_api_key_auth: + await user_api_key_auth_websocket(mock_websocket) + + request_arg = mock_user_api_key_auth.call_args.kwargs["request"] + assert request_arg.scope.get("path") == "/v1/realtime" + assert request_arg.scope.get("root_path") == "" + + @pytest.mark.parametrize("enforce_rbac", [True, False]) @pytest.mark.asyncio async def test_jwt_user_api_key_auth_builder_enforce_rbac(enforce_rbac, monkeypatch): diff --git a/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py b/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py new file mode 100644 index 00000000000..25bf79cd575 --- /dev/null +++ b/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py @@ -0,0 +1,268 @@ +""" +Unit tests for the Responses-API streaming-fallback helpers added to Router +in PR #28215 (fix(router): wrap aresponses streaming iterator for mid-stream +fallbacks). + +Targets the four helpers introduced on Router: + - _extract_partial_responses_usage + - _combine_responses_fallback_usage + - _build_responses_continuation_input + - _aresponses_streaming_iterator +""" + +import os +import sys +from typing import Any, AsyncIterator, List +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +sys.path.insert(0, os.path.abspath("../..")) + +from litellm import Router +from litellm.types.llms.openai import ( + ResponseAPIUsage, + ResponseCompletedEvent, + ResponsesAPIResponse, + ResponsesAPIStreamEvents, +) + + +def _make_router() -> Router: + return Router( + model_list=[ + { + "model_name": "primary", + "litellm_params": { + "model": "openai/gpt-4o-mini", + "api_key": "sk-test", + }, + }, + { + "model_name": "fallback", + "litellm_params": { + "model": "openai/gpt-4o", + "api_key": "sk-test", + }, + }, + ] + ) + + +def _make_completed_event( + input_tokens: int, output_tokens: int, total_tokens: int +) -> ResponseCompletedEvent: + response = ResponsesAPIResponse.model_construct( + usage=ResponseAPIUsage( + input_tokens=input_tokens, + output_tokens=output_tokens, + total_tokens=total_tokens, + ) + ) + return ResponseCompletedEvent.model_construct( + type=ResponsesAPIStreamEvents.RESPONSE_COMPLETED, + response=response, + ) + + +# -------- _extract_partial_responses_usage -------- + + +def test_extract_partial_responses_usage_native_completed(): + """Native path: completed_response carries usage → returned as-is.""" + completed = _make_completed_event(11, 7, 18) + source = MagicMock() + source.completed_response = completed + + usage = Router._extract_partial_responses_usage(source) + assert usage is not None + assert usage.input_tokens == 11 + assert usage.output_tokens == 7 + assert usage.total_tokens == 18 + + +def test_extract_partial_responses_usage_no_completed_response(): + """Native path: no completed_response → returns None.""" + source = MagicMock() + source.completed_response = None + + usage = Router._extract_partial_responses_usage(source) + assert usage is None + + +# -------- _combine_responses_fallback_usage -------- + + +def test_combine_responses_fallback_usage_sums_completed_event(): + """Partial-stream usage is summed into the fallback event's usage.""" + fallback_event = _make_completed_event(5, 3, 8) + partial = ResponseAPIUsage(input_tokens=11, output_tokens=7, total_tokens=18) + + Router._combine_responses_fallback_usage(fallback_event, partial) + + combined = fallback_event.response.usage + assert combined is not None + assert combined.input_tokens == 16 + assert combined.output_tokens == 10 + assert combined.total_tokens == 26 + + +def test_combine_responses_fallback_usage_passthrough_for_unknown_event(): + """Events that are not completed/failed/incomplete are not mutated.""" + other = MagicMock() # not a ResponseCompletedEvent etc. → isinstance false + partial = ResponseAPIUsage(input_tokens=1, output_tokens=1, total_tokens=2) + Router._combine_responses_fallback_usage(other, partial) + # No mutation expected on the unknown event — call is a no-op. + + +# -------- _build_responses_continuation_input -------- + + +def test_build_responses_continuation_input_from_string(): + out = Router._build_responses_continuation_input( + "Hello world", "partial assistant text" + ) + assert len(out) == 3 + assert out[0]["role"] == "user" + assert out[0]["content"][0]["text"] == "Hello world" + assert out[1]["role"] == "developer" + assert out[2]["role"] == "assistant" + assert out[2]["content"][0]["text"] == "partial assistant text" + + +def test_build_responses_continuation_input_from_list_preserves_items(): + existing: List[Any] = [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "msg1"}], + } + ] + out = Router._build_responses_continuation_input(existing, "partial") + assert len(out) == 3 + assert out[0]["content"][0]["text"] == "msg1" + assert out[1]["role"] == "developer" + assert out[2]["role"] == "assistant" + + +def test_build_responses_continuation_input_from_none(): + out = Router._build_responses_continuation_input(None, "partial") + assert len(out) == 2 + assert out[0]["role"] == "developer" + assert out[1]["role"] == "assistant" + + +# -------- _aresponses_streaming_iterator (passthrough smoke test) -------- + + +@pytest.mark.asyncio +async def test_aresponses_streaming_iterator_passthrough(): + """ + Without MidStreamFallbackError, the wrapper yields source events + unchanged and returns a BaseResponsesAPIStreamingIterator subclass. + """ + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + + events = [_make_completed_event(1, 1, 2)] + + class _FakeSource: + """Minimal source iterator. Provides every attribute the wrapper + constructor reads from source_iterator.""" + + def __init__(self) -> None: + self._i = 0 + self.completed_response = None + self.response = MagicMock() + self.model = "openai/gpt-4o-mini" + self.logging_obj = MagicMock() + self.responses_api_provider_config = MagicMock() + self.start_time = 0.0 + self.litellm_metadata = {} + self.custom_llm_provider = "openai" + self.request_data = {} + self.call_type = "aresponses" + self._hidden_params: dict = {} + + def __aiter__(self) -> AsyncIterator[Any]: + return self + + async def __anext__(self): + if self._i >= len(events): + raise StopAsyncIteration + ev = events[self._i] + self._i += 1 + return ev + + async def aclose(self): + return None + + router = _make_router() + source = _FakeSource() + + wrapper = await router._aresponses_streaming_iterator( + source, initial_kwargs={"model": "primary"} + ) + assert isinstance(wrapper, BaseResponsesAPIStreamingIterator) + + collected = [ev async for ev in wrapper] + assert len(collected) == 1 + assert collected[0].type == ResponsesAPIStreamEvents.RESPONSE_COMPLETED + + +# -------- _aresponses_with_streaming_fallbacks -------- + + +@pytest.mark.asyncio +async def test_aresponses_with_streaming_fallbacks_non_streaming_passthrough(): + """Non-streaming response is returned unchanged, no wrap.""" + router = _make_router() + plain_response = MagicMock() + + async def fake_original(**_kwargs): + return plain_response + + with patch.object( + router, + "_ageneric_api_call_with_fallbacks", + new=AsyncMock(return_value=plain_response), + ): + out = await router._aresponses_with_streaming_fallbacks( + original_function=fake_original, + model="primary", + stream=False, + ) + assert out is plain_response + + +@pytest.mark.asyncio +async def test_aresponses_with_streaming_fallbacks_wraps_streaming_iterator(): + """Streaming response is wrapped via _aresponses_streaming_iterator.""" + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + + router = _make_router() + streaming_iter = MagicMock(spec=BaseResponsesAPIStreamingIterator) + wrapped = MagicMock(spec=BaseResponsesAPIStreamingIterator) + + async def fake_original(**_kwargs): + return streaming_iter + + with patch.object( + router, + "_ageneric_api_call_with_fallbacks", + new=AsyncMock(return_value=streaming_iter), + ), patch.object( + router, + "_aresponses_streaming_iterator", + new=AsyncMock(return_value=wrapped), + ) as mock_wrap: + out = await router._aresponses_with_streaming_fallbacks( + original_function=fake_original, + model="primary", + stream=True, + ) + assert out is wrapped + mock_wrap.assert_awaited_once() diff --git a/tests/test_litellm/containers/test_azure_container_transformation.py b/tests/test_litellm/containers/test_azure_container_transformation.py index 70181f6f03d..cdcccf7c04e 100644 --- a/tests/test_litellm/containers/test_azure_container_transformation.py +++ b/tests/test_litellm/containers/test_azure_container_transformation.py @@ -109,6 +109,31 @@ class TestAzureContainerConfig: assert "/openai/v1/containers" in url + def test_get_complete_url_strips_responses_path_and_preserves_api_version(self): + """When api_base is the responses endpoint URL, get_complete_url must: + - strip /openai/responses (no double-path) + - use the api-version from api_base query string, NOT the deployment's + older api_version (e.g. 2024-08-01-preview → containers need 2025-04-01-preview) + """ + api_base = "https://my-resource.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview" + + url = self.config.get_complete_url( + api_base=api_base, + litellm_params={"api_version": "2024-08-01-preview"}, + ) + + assert ( + "/openai/responses/openai/containers" not in url + ), "path must not double /openai/responses" + assert "my-resource.cognitiveservices.azure.com" in url + assert "/openai/containers" in url or "/openai/v1/containers" in url + assert ( + "2025-04-01-preview" in url + ), "must use version from api_base, not litellm_params" + assert ( + "2024-08-01-preview" not in url + ), "must not fall back to older chat api_version" + def test_get_complete_url_raises_without_api_base(self, monkeypatch): monkeypatch.delenv("AZURE_API_BASE", raising=False) monkeypatch.setattr(litellm, "api_base", None) @@ -531,6 +556,92 @@ class TestAzureContainerKnownFailureRegressions: assert qs.get("api-version") == ["v1"] assert qs.get("foo") == ["bar"] + @pytest.mark.asyncio + async def test_regression_no_container_id_does_not_use_user_supplied_model_id( + self, monkeypatch + ): + """Operations without container_id (create, list) must NOT route via + _ageneric_api_call_with_fallbacks using a caller-supplied model_id. + + Security boundary: only the path that holds a validated container_id + is trusted to fall back to the forwarded model_id. A caller setting + model_id without container_id on POST /v1/containers must not gain + access to an arbitrary deployment UUID. + """ + from litellm.router import Router + + router = Router( + model_list=[ + { + "model_name": "azure-model", + "litellm_params": { + "model": "azure/gpt-4", + "api_base": "https://my-resource.cognitiveservices.azure.com", + "api_key": "test-key", + "api_version": "2025-04-01-preview", + }, + "model_info": {"id": "deployment-uuid-123"}, + } + ] + ) + + fallback_called = {"called": False} + + async def _mock_fallback(original_function, **kwargs): + fallback_called["called"] = True + return {} + + monkeypatch.setattr(router, "_ageneric_api_call_with_fallbacks", _mock_fallback) + + original_called = {"called": False} + + async def _noop(**kwargs): + original_called["called"] = True + return {} + + # No container_id — simulates create/list; caller injects a model_id + await router._init_containers_api_endpoints( + original_function=_noop, + model_id="deployment-uuid-123", + custom_llm_provider="azure", + ) + + assert not fallback_called["called"], ( + "_ageneric_api_call_with_fallbacks must NOT be called when " + "container_id is absent, even if model_id is supplied" + ) + assert original_called["called"], "original_function must be called directly" + + def test_regression_httpx_empty_params_strips_query_string(self): + """httpx erases the URL query-string when params={} (empty dict) is passed. + + Root cause of the Azure container 404s on POST/DELETE: + _build_query_params returns {} when the endpoint has no extra params; + passing that {} as params= to httpx wiped ?api-version=2025-04-01-preview. + + Fix: every container httpx call now uses `params or None` so an empty + dict falls back to None, which tells httpx to leave the URL untouched. + """ + url = ( + "https://resource.cognitiveservices.azure.com" + "/openai/containers/cntr_123?api-version=2025-04-01-preview" + ) + client = httpx.AsyncClient() + + req_none = client.build_request("DELETE", url, params=None) + assert "api-version=2025-04-01-preview" in str(req_none.url) + + req_empty = client.build_request("DELETE", url, params={}) + assert "api-version" not in str( + req_empty.url + ), "Documents root cause: params={} strips the query string" + + effective: dict = {} + req_guarded = client.build_request("DELETE", url, params=effective or None) + assert "api-version=2025-04-01-preview" in str( + req_guarded.url + ), "`params or None` must preserve ?api-version" + def test_regression_proxy_resolves_azure_text_same_as_azure(self): """Router/proxy treat azure_text like azure for container config.""" from litellm.proxy.container_endpoints.handler_factory import ( @@ -770,3 +881,143 @@ class TestAzureContainerKnownFailureRegressions: assert captured["data"]["container_id"] == "cntr_123" assert captured["data"]["custom_llm_provider"] == "azure" assert captured["data"]["model_id"] == "model_abc123" + + @pytest.mark.asyncio + async def test_regression_get_container_forwarding_params_sets_model_id_for_managed_id( + self, + ): + """get_container_forwarding_params must extract model_id from a + LiteLLM-managed encoded container ID and include it in the forwarding + dict. This is the proxy-side half of the native-Azure-ID routing fix: + the router's _init_containers_api_endpoints reads kwargs["model_id"] + which is set here. + """ + from litellm.proxy.container_endpoints.ownership import ( + get_container_forwarding_params, + ) + + encoded_id = ResponsesAPIRequestUtils._build_container_id( + custom_llm_provider="azure", + model_id="deployment-uuid-123", + container_id="cntr_6a058b43d24c8190a226cfb1d35405b20115fb7875ff11df", + ) + + params = await get_container_forwarding_params( + container_id=encoded_id, + original_container_id="cntr_6a058b43d24c8190a226cfb1d35405b20115fb7875ff11df", + custom_llm_provider="azure", + ) + + assert ( + params.get("model_id") == "deployment-uuid-123" + ), "model_id must be forwarded to the router for managed container IDs" + assert params.get("container_id") == ( + "cntr_6a058b43d24c8190a226cfb1d35405b20115fb7875ff11df" + ) + assert params.get("custom_llm_provider") == "azure" + + @pytest.mark.asyncio + async def test_regression_get_container_forwarding_params_recovers_model_id_for_native_id( + self, monkeypatch + ): + """Native Azure IDs (``cntr_``) cannot be decoded, so model_id + must be recovered from the ownership row's ``unified_object_id`` — + the encoded form captured at create time when the router selected a + specific deployment. Without this, the router-side fallback for + native IDs in ``_init_containers_api_endpoints`` is dead code. + """ + from types import SimpleNamespace + from unittest.mock import AsyncMock + + from litellm.proxy.container_endpoints import ownership + from litellm.proxy.container_endpoints.ownership import ( + get_container_forwarding_params, + ) + + native_id = "cntr_6a058b43d24c8190a226cfb1d35405b20115fb7875ff11df" + encoded_stored_id = ResponsesAPIRequestUtils._build_container_id( + custom_llm_provider="azure", + model_id="deployment-uuid-123", + container_id=native_id, + ) + + ownership._CONTAINER_STORED_ID_CACHE.flush_cache() + ownership._CONTAINER_OWNER_CACHE.flush_cache() + + table = AsyncMock() + table.find_first.return_value = SimpleNamespace( + created_by="user-1", + file_purpose=ownership.CONTAINER_OBJECT_PURPOSE, + unified_object_id=encoded_stored_id, + ) + prisma_client = SimpleNamespace( + db=SimpleNamespace(litellm_managedobjecttable=table) + ) + monkeypatch.setattr( + ownership, + "_get_prisma_client", + AsyncMock(return_value=prisma_client), + ) + + params = await get_container_forwarding_params( + container_id=native_id, + original_container_id=native_id, + custom_llm_provider="azure", + ) + + assert params.get("model_id") == "deployment-uuid-123", ( + "model_id must be recovered from the stored unified_object_id " + "for native upstream container IDs" + ) + assert params.get("container_id") == native_id + assert params.get("custom_llm_provider") == "azure" + + @pytest.mark.asyncio + async def test_regression_native_azure_container_id_uses_forwarded_model_id( + self, monkeypatch + ): + """Native Azure container IDs (cntr_ + hex, no LiteLLM payload) must + still route through _ageneric_api_call_with_fallbacks using the + model_id forwarded from the proxy ownership check so that deployment + credentials (api_base) are applied.""" + from litellm.router import Router + + router = Router( + model_list=[ + { + "model_name": "azure-model", + "litellm_params": { + "model": "azure/gpt-4", + "api_base": "https://my-resource.cognitiveservices.azure.com", + "api_key": "test-key", + "api_version": "2025-04-01-preview", + }, + "model_info": {"id": "deployment-uuid-123"}, + } + ] + ) + + called_with: dict = {} + + async def _mock_fallback(original_function, **kwargs): + called_with.update(kwargs) + return {} + + monkeypatch.setattr(router, "_ageneric_api_call_with_fallbacks", _mock_fallback) + + native_azure_id = "cntr_6a058b43d24c8190a226cfb1d35405b20115fb7875ff11df" + + async def _noop(**kwargs): + return {} + + await router._init_containers_api_endpoints( + original_function=_noop, + container_id=native_azure_id, + model_id="deployment-uuid-123", + custom_llm_provider="azure", + ) + + assert called_with.get("model") == "deployment-uuid-123", ( + "_ageneric_api_call_with_fallbacks must be called with the forwarded " + "model_id when the container_id carries no LiteLLM routing payload" + ) diff --git a/tests/test_litellm/enterprise/proxy/test_batch_update_db_managed_output_file_id.py b/tests/test_litellm/enterprise/proxy/test_batch_update_db_managed_output_file_id.py new file mode 100644 index 00000000000..d8669960674 --- /dev/null +++ b/tests/test_litellm/enterprise/proxy/test_batch_update_db_managed_output_file_id.py @@ -0,0 +1,260 @@ +"""Regression: update_batch_in_database must not persist raw provider output_file_id.""" + +import json +from types import SimpleNamespace +from typing import Optional +import pytest +from unittest.mock import AsyncMock, MagicMock + +from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy.openai_files_endpoints.common_utils import ( + ensure_batch_response_managed_file_ids, + update_batch_in_database, +) +from litellm.types.utils import LiteLLMBatch + + +def _build_batch_response( + *, + batch_id: str = "batch_managed_ids_test", + status: str = "completed", + output_file_id: Optional[str] = "file-rawoutput789", + error_file_id: Optional[str] = None, + hidden_params: Optional[dict] = None, +) -> LiteLLMBatch: + batch = LiteLLMBatch( + id=batch_id, + object="batch", + status=status, + endpoint="/v1/chat/completions", + input_file_id="file-input123", + output_file_id=output_file_id, + error_file_id=error_file_id, + completion_window="24h", + created_at=1234567890, + ) + if hidden_params is not None: + batch._hidden_params = hidden_params # type: ignore[attr-defined] + return batch + + +def _build_managed_files_mock(unified_id: str = "file-bWFuYWdlZF9vdXRwdXRfaWQ="): + mock = MagicMock() + mock.get_unified_output_file_id = MagicMock(return_value=unified_id) + mock.store_unified_file_id = AsyncMock() + return mock + + +def _build_prisma_mock(): + mock = MagicMock() + mock.db.litellm_managedfiletable.find_first = AsyncMock(return_value=None) + mock.db.litellm_managedobjecttable.update = AsyncMock() + return mock + + +@pytest.mark.asyncio +async def test_update_batch_in_database_stores_unified_output_file_id(): + raw_output_file_id = "file-rawoutput789" + unified_output_file_id = "file-bWFuYWdlZF9vdXRwdXRfaWQ=" + batch_id = "batch_managed_ids_test" + unified_batch_id = ( + "litellm_proxy;model_id:my-model;llm_batch_id:batch_managed_ids_test" + ) + + response = _build_batch_response( + batch_id=batch_id, + output_file_id=raw_output_file_id, + hidden_params={"model_id": "my-model", "model_name": "openai/gpt-4o"}, + ) + + mock_managed_files = _build_managed_files_mock(unified_id=unified_output_file_id) + mock_prisma = _build_prisma_mock() + + await update_batch_in_database( + batch_id=batch_id, + unified_batch_id=unified_batch_id, + response=response, + managed_files_obj=mock_managed_files, + prisma_client=mock_prisma, + verbose_proxy_logger=MagicMock(), + user_api_key_dict=UserAPIKeyAuth(user_id="user-abc"), + ) + + stored = json.loads( + mock_prisma.db.litellm_managedobjecttable.update.call_args.kwargs["data"][ + "file_object" + ] + ) + assert stored["output_file_id"] == unified_output_file_id + assert stored["output_file_id"] != raw_output_file_id + + +@pytest.mark.asyncio +async def test_ensure_batch_response_normalizes_error_file_id(): + """Both output_file_id and error_file_id must be normalized to managed IDs.""" + unified_id = "file-bWFuYWdlZF9vdXRwdXRfaWQ=" + response = _build_batch_response( + output_file_id="file-raw-output", + error_file_id="file-raw-error", + hidden_params={"model_id": "my-model", "model_name": "openai/gpt-4o"}, + ) + + mock_managed_files = _build_managed_files_mock(unified_id=unified_id) + mock_prisma = _build_prisma_mock() + + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=mock_managed_files, + prisma_client=mock_prisma, + verbose_proxy_logger=MagicMock(), + user_api_key_dict=UserAPIKeyAuth(user_id="user-abc"), + ) + + assert response.output_file_id == unified_id + assert response.error_file_id == unified_id + assert mock_managed_files.get_unified_output_file_id.call_count == 2 + + +@pytest.mark.asyncio +async def test_ensure_batch_response_swallows_conversion_errors(): + """When the managed-files conversion raises, the failure is logged, not propagated.""" + raw_output_file_id = "file-raw-output" + response = _build_batch_response( + output_file_id=raw_output_file_id, + hidden_params={"model_id": "my-model", "model_name": "openai/gpt-4o"}, + ) + + mock_managed_files = MagicMock() + mock_managed_files.get_unified_output_file_id = MagicMock( + side_effect=RuntimeError("boom") + ) + mock_managed_files.store_unified_file_id = AsyncMock() + + mock_logger = MagicMock() + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=mock_managed_files, + prisma_client=_build_prisma_mock(), + verbose_proxy_logger=mock_logger, + user_api_key_dict=UserAPIKeyAuth(user_id="user-abc"), + ) + + assert response.output_file_id == raw_output_file_id + mock_logger.warning.assert_called() + + +@pytest.mark.asyncio +async def test_ensure_batch_response_builds_auth_from_db_batch_object(): + """If user_api_key_dict is omitted, fall back to created_by/team_id on db_batch_object.""" + unified_id = "file-bWFuYWdlZF9vdXRwdXRfaWQ=" + response = _build_batch_response( + output_file_id="file-raw-output", + hidden_params={"model_id": "my-model", "model_name": "openai/gpt-4o"}, + ) + + mock_managed_files = _build_managed_files_mock(unified_id=unified_id) + db_batch_object = SimpleNamespace( + created_by="user-from-db", team_id="team-from-db", status="completed" + ) + + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=mock_managed_files, + prisma_client=_build_prisma_mock(), + verbose_proxy_logger=MagicMock(), + db_batch_object=db_batch_object, + ) + + forwarded_auth = mock_managed_files.store_unified_file_id.call_args.kwargs[ + "user_api_key_dict" + ] + assert forwarded_auth.user_id == "user-from-db" + assert forwarded_auth.team_id == "team-from-db" + + +@pytest.mark.asyncio +async def test_ensure_batch_response_resolves_model_name_from_unified_file_id(): + """When hidden_params lacks model_name, derive it from unified_file_id.""" + unified_id = "file-bWFuYWdlZF9vdXRwdXRfaWQ=" + response = _build_batch_response( + output_file_id="file-raw-output", + hidden_params={ + "model_id": "my-model", + "unified_file_id": "litellm_proxy:application/octet-stream;unified_id,abc;target_model_names,gpt-4o-mini,gemini-2.0-flash", + }, + ) + + mock_managed_files = _build_managed_files_mock(unified_id=unified_id) + + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=mock_managed_files, + prisma_client=_build_prisma_mock(), + verbose_proxy_logger=MagicMock(), + user_api_key_dict=UserAPIKeyAuth(user_id="user-abc"), + ) + + assert ( + mock_managed_files.get_unified_output_file_id.call_args.kwargs["model_name"] + == "gpt-4o-mini,gemini-2.0-flash" + ) + + +@pytest.mark.asyncio +async def test_ensure_batch_response_returns_early_without_managed_files_obj(): + """Without managed_files_obj, the helper is a no-op (no conversion attempted).""" + response = _build_batch_response( + output_file_id="file-raw-output", + hidden_params={"model_id": "my-model", "model_name": "openai/gpt-4o"}, + ) + + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=None, + prisma_client=_build_prisma_mock(), + verbose_proxy_logger=MagicMock(), + user_api_key_dict=UserAPIKeyAuth(user_id="user-abc"), + ) + + assert response.output_file_id == "file-raw-output" + + +@pytest.mark.asyncio +async def test_ensure_batch_response_returns_early_without_model_id(): + """Without model_id in hidden_params, the helper cannot create managed IDs.""" + response = _build_batch_response( + output_file_id="file-raw-output", + hidden_params={"model_name": "openai/gpt-4o"}, + ) + mock_managed_files = _build_managed_files_mock() + + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=mock_managed_files, + prisma_client=_build_prisma_mock(), + verbose_proxy_logger=MagicMock(), + user_api_key_dict=UserAPIKeyAuth(user_id="user-abc"), + ) + + assert response.output_file_id == "file-raw-output" + mock_managed_files.get_unified_output_file_id.assert_not_called() + + +@pytest.mark.asyncio +async def test_ensure_batch_response_returns_early_without_auth(): + """Without user_api_key_dict or db_batch_object, no conversion is attempted.""" + response = _build_batch_response( + output_file_id="file-raw-output", + hidden_params={"model_id": "my-model", "model_name": "openai/gpt-4o"}, + ) + mock_managed_files = _build_managed_files_mock() + + await ensure_batch_response_managed_file_ids( + response=response, + managed_files_obj=mock_managed_files, + prisma_client=_build_prisma_mock(), + verbose_proxy_logger=MagicMock(), + ) + + assert response.output_file_id == "file-raw-output" + mock_managed_files.get_unified_output_file_id.assert_not_called() diff --git a/tests/test_litellm/integrations/test_opentelemetry.py b/tests/test_litellm/integrations/test_opentelemetry.py index 6de855262bd..b65e629c890 100644 --- a/tests/test_litellm/integrations/test_opentelemetry.py +++ b/tests/test_litellm/integrations/test_opentelemetry.py @@ -66,7 +66,7 @@ class TestOpenTelemetryGuardrails(unittest.TestCase): mock_span.set_attribute.assert_any_call("guardrail_name", "test_guardrail") mock_span.set_attribute.assert_any_call("guardrail_mode", "input") mock_span.set_attribute.assert_any_call( - "guardrail_response", "filtered_content" + "guardrail_response", safe_dumps("filtered_content") ) mock_span.set_attribute.assert_any_call( "masked_entity_count", safe_dumps({"CREDIT_CARD": 2}) @@ -87,6 +87,65 @@ class TestOpenTelemetryGuardrails(unittest.TestCase): # Verify that start_span was never called otel.tracer.start_span.assert_not_called() + @patch("litellm.integrations.opentelemetry.datetime") + def test_guardrail_response_dict_is_json_serialized(self, mock_datetime): + """Dict guardrail_response (e.g. OpenAI moderation result) must reach + the span as a JSON string so downstream pipelines can parse it for + metric extraction — this is the bug the PR fixes.""" + otel = OpenTelemetry() + otel.tracer = MagicMock() + mock_span = MagicMock() + otel.tracer.start_span.return_value = mock_span + + moderation_payload = { + "id": "modr-7740", + "model": "omni-moderation-latest", + "results": [{"categories": {"harassment": False}}], + } + guardrail_info = { + "guardrail_name": "test_guardrail", + "guardrail_mode": "input", + "guardrail_response": moderation_payload, + "start_time": 1609459200.0, + "end_time": 1609459201.0, + } + kwargs = { + "standard_logging_object": {"guardrail_information": [guardrail_info]} + } + + otel._create_guardrail_span(kwargs=kwargs, context=None) + + mock_span.set_attribute.assert_any_call( + "guardrail_response", safe_dumps(moderation_payload) + ) + + @patch("litellm.integrations.opentelemetry.datetime") + def test_guardrail_response_none_is_skipped(self, mock_datetime): + """When guardrail_response is None, the attribute must not be set — + guards against round-tripping ``"null"`` into traces.""" + otel = OpenTelemetry() + otel.tracer = MagicMock() + mock_span = MagicMock() + otel.tracer.start_span.return_value = mock_span + + guardrail_info = { + "guardrail_name": "test_guardrail", + "guardrail_mode": "input", + "guardrail_response": None, + "start_time": 1609459200.0, + "end_time": 1609459201.0, + } + kwargs = { + "standard_logging_object": {"guardrail_information": [guardrail_info]} + } + + otel._create_guardrail_span(kwargs=kwargs, context=None) + + attribute_keys = [ + call.args[0] for call in mock_span.set_attribute.call_args_list + ] + self.assertNotIn("guardrail_response", attribute_keys) + class TestOpenTelemetryTeamAttributesOnChildSpans(unittest.TestCase): """team_id / team_alias must land on every child span of a @@ -1169,7 +1228,7 @@ class TestOpenTelemetry(unittest.TestCase): mock_span.set_attribute.assert_any_call("guardrail_name", "test_guardrail") mock_span.set_attribute.assert_any_call("guardrail_mode", "input") mock_span.set_attribute.assert_any_call( - "guardrail_response", "filtered_content" + "guardrail_response", safe_dumps("filtered_content") ) mock_span.set_attribute.assert_any_call( "masked_entity_count", safe_dumps({"CREDIT_CARD": 2}) diff --git a/tests/test_litellm/interactions/test_gemini_interactions_transformation.py b/tests/test_litellm/interactions/test_gemini_interactions_transformation.py index 2e596a72158..524589abf5e 100644 --- a/tests/test_litellm/interactions/test_gemini_interactions_transformation.py +++ b/tests/test_litellm/interactions/test_gemini_interactions_transformation.py @@ -1,10 +1,11 @@ """ Tests for Gemini Interactions API transformation. -Covers credential leak prevention changes: -- validate_environment sets x-goog-api-key header -- get_complete_url excludes API key from URL -- get/delete/cancel interaction request URLs exclude API key +Covers: +- validate_environment: x-goog-api-key header, Api-Revision schema selection +- get_complete_url: API key excluded from URL +- get/delete/cancel interaction request URLs +- transform_request: response_mime_type coalescing, image_config migration """ import os @@ -15,6 +16,7 @@ import pytest sys.path.insert(0, os.path.abspath("../../..")) +import litellm from litellm.interactions.litellm_responses_transformation.streaming_iterator import ( LiteLLMResponsesInteractionsStreamingIterator, ) @@ -22,7 +24,6 @@ from litellm.llms.gemini.interactions.transformation import ( GoogleAIStudioInteractionsConfig, ) from litellm.types.llms.openai import ( - ContentPartAddedEvent, OutputTextDeltaEvent, ResponseCompletedEvent, ResponseCreatedEvent, @@ -85,6 +86,30 @@ class TestValidateEnvironment: assert headers["X-Custom"] == "value" assert headers["x-goog-api-key"] == "test-key" + def test_api_revision_new_schema_by_default(self, config): + # Default: use_legacy_interactions_schema=False → new steps schema + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = False + headers = config.validate_environment( + headers={}, model="gemini-2.5-flash", litellm_params=None + ) + assert headers["Api-Revision"] == "2026-05-20" + finally: + litellm.use_legacy_interactions_schema = original + + def test_api_revision_legacy_schema_when_flag_set(self, config): + # Flag on → legacy outputs schema until June 8, 2026 + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = True + headers = config.validate_environment( + headers={}, model="gemini-2.5-flash", litellm_params=None + ) + assert headers["Api-Revision"] == "2026-05-07" + finally: + litellm.use_legacy_interactions_schema = original + class TestGetCompleteUrl: def test_url_excludes_api_key(self, config): @@ -127,7 +152,12 @@ class TestTransformRequest: request_body = config.transform_request( model=None, agent="my-custom-slides-agent", - input=[{"type": "text", "text": "Create a 5-slide presentation about AI trends."}], + input=[ + { + "type": "text", + "text": "Create a 5-slide presentation about AI trends.", + } + ], optional_params={ "environment": "remote", "stream": False, @@ -172,158 +202,7 @@ class TestTransformRequest: ) assert request_body["environment"] == env_id -class TestStreamingIterator: - def _make_iterator(self) -> LiteLLMResponsesInteractionsStreamingIterator: - return LiteLLMResponsesInteractionsStreamingIterator( - model="gpt-5.4", - litellm_custom_stream_wrapper=MagicMock(), - request_input="hi", - optional_params={}, - ) - def _make_text_delta( - self, text: str, item_id: str = "item_1" - ) -> OutputTextDeltaEvent: - event = MagicMock(spec=OutputTextDeltaEvent) - event.delta = text - event.item_id = item_id - return event - - def _make_part_added(self, item_id: str = "item_1") -> ContentPartAddedEvent: - event = MagicMock(spec=ContentPartAddedEvent) - event.item_id = item_id - return event - - def _make_response_created(self) -> ResponseCreatedEvent: - event = MagicMock(spec=ResponseCreatedEvent) - event.response = MagicMock(id="resp_123") - return event - - def test_content_delta_includes_type_field(self): - """content.delta events must carry delta.type='text' so the UI can display them.""" - it = self._make_iterator() - it.sent_interaction_start = True - it.sent_content_start = True - - chunk = it._transform_responses_chunk_to_interactions_chunk( - self._make_text_delta("Hello") - ) - - assert chunk is not None - assert chunk.event_type == "content.delta" - assert chunk.delta == {"type": "text", "text": "Hello"} - - def test_response_part_added_emits_content_start(self): - """ContentPartAddedEvent (arrives before text deltas) should emit content.start - so the first OutputTextDeltaEvent immediately emits content.delta without dropping text. - """ - it = self._make_iterator() - it.sent_interaction_start = True - - chunk = it._transform_responses_chunk_to_interactions_chunk( - self._make_part_added() - ) - - assert chunk is not None - assert chunk.event_type == "content.start" - assert it.sent_content_start is True - - def test_first_text_delta_not_dropped_when_part_added_seen(self): - """After ContentPartAddedEvent, the first text delta must yield content.delta - (not content.start), preserving the token text.""" - it = self._make_iterator() - it.sent_interaction_start = True - it._transform_responses_chunk_to_interactions_chunk(self._make_part_added()) - - chunk = it._transform_responses_chunk_to_interactions_chunk( - self._make_text_delta("Hello") - ) - - assert chunk is not None - assert chunk.event_type == "content.delta" - assert chunk.delta is not None - assert chunk.delta.get("text") == "Hello" - - def test_part_added_emits_interaction_start_fallback_when_not_sent(self): - """If ContentPartAddedEvent arrives before any ResponseCreatedEvent, - the iterator must emit interaction.start before content.start to honor - the documented event ordering contract.""" - it = self._make_iterator() - - chunk = it._transform_responses_chunk_to_interactions_chunk( - self._make_part_added(item_id="item_42") - ) - - assert chunk is not None - assert chunk.event_type == "interaction.start" - assert chunk.id == "item_42" - assert chunk.status == "in_progress" - assert chunk.model == "gpt-5.4" - assert it.sent_interaction_start is True - assert it.sent_content_start is False - - def test_part_added_returns_none_when_already_started(self): - """A second ContentPartAddedEvent (after content.start was already emitted) - should be a no-op so we don't re-emit content.start.""" - it = self._make_iterator() - it.sent_interaction_start = True - it.sent_content_start = True - - chunk = it._transform_responses_chunk_to_interactions_chunk( - self._make_part_added() - ) - - assert chunk is None - - def test_part_added_without_item_id_falls_back_to_self_id(self): - """When ContentPartAddedEvent has no item_id and we emit the interaction.start - fallback, the id must default to an interaction_ string.""" - it = self._make_iterator() - event = MagicMock(spec=ContentPartAddedEvent) - event.item_id = None - - chunk = it._transform_responses_chunk_to_interactions_chunk(event) - - assert chunk is not None - assert chunk.event_type == "interaction.start" - assert chunk.id == f"interaction_{id(it)}" - - def test_first_text_delta_not_dropped_when_no_prior_start_events(self): - """When OutputTextDeltaEvent arrives before any ResponseCreatedEvent or - ContentPartAddedEvent, the iterator must emit interaction.start *and* - immediately follow with a content.start that carries this delta's text, - so the first token is never silently dropped from the stream.""" - events = [ - self._make_text_delta("Hello"), - self._make_text_delta(" World"), - ] - wrapper = MagicMock() - wrapper.__iter__ = lambda self: iter(events) - wrapper.__next__ = lambda self, _it=iter(events): next(_it) - it = LiteLLMResponsesInteractionsStreamingIterator( - model="gpt-5.4", - litellm_custom_stream_wrapper=wrapper, - request_input="hi", - optional_params={}, - ) - - first = it._transform_responses_chunk_to_interactions_chunk(events[0]) - assert first is not None - assert first.event_type == "interaction.start" - assert it.sent_interaction_start is True - assert it.sent_content_start is True - assert len(it._pending_events) == 1 - pending = it._pending_events[0] - assert pending.event_type == "content.start" - assert pending.delta == {"type": "text", "text": "Hello"} - - second = it._transform_responses_chunk_to_interactions_chunk(events[1]) - assert second is not None - assert second.event_type == "content.delta" - assert second.delta == {"type": "text", "text": " World"} - - -class TestTransformRequest: def test_stream_param_included_in_request_body(self, config): """When stream=True is in optional_params, the request body must include it so the proxy forwards the SSE streaming flag to Google's backend.""" @@ -352,6 +231,273 @@ class TestTransformRequest: assert "stream" not in body +class TestStreamingIterator: + def _make_iterator( + self, use_legacy: bool = False + ) -> LiteLLMResponsesInteractionsStreamingIterator: + original = litellm.use_legacy_interactions_schema + litellm.use_legacy_interactions_schema = use_legacy + try: + return LiteLLMResponsesInteractionsStreamingIterator( + model="gpt-5.4", + litellm_custom_stream_wrapper=MagicMock(), + request_input="hi", + optional_params={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + def _make_text_delta( + self, text: str, item_id: str = "item_1" + ) -> OutputTextDeltaEvent: + event = MagicMock(spec=OutputTextDeltaEvent) + event.delta = text + event.item_id = item_id + return event + + def _make_response_created(self) -> ResponseCreatedEvent: + event = MagicMock(spec=ResponseCreatedEvent) + event.response = MagicMock(id="resp_123") + return event + + def test_step_delta_includes_type_field(self): + """step.delta events must carry delta.type='text' so the UI can display them.""" + it = self._make_iterator(use_legacy=False) + it.sent_interaction_start = True + it.sent_content_start = True + + chunk = it._transform_responses_chunk_to_interactions_chunk( + self._make_text_delta("Hello") + ) + + assert chunk is not None + assert chunk.event_type == "step.delta" + assert chunk.delta == {"type": "text", "text": "Hello"} + + def test_content_delta_legacy_schema(self): + """Legacy schema emits content.delta with type and text fields.""" + it = self._make_iterator(use_legacy=True) + it.sent_interaction_start = True + it.sent_content_start = True + + chunk = it._transform_responses_chunk_to_interactions_chunk( + self._make_text_delta("Hello") + ) + + assert chunk is not None + assert chunk.event_type == "content.delta" + assert chunk.delta == {"type": "text", "text": "Hello"} + + def test_response_created_emits_interaction_created(self): + it = self._make_iterator(use_legacy=False) + + chunk = it._transform_responses_chunk_to_interactions_chunk( + self._make_response_created() + ) + + assert chunk is not None + assert chunk.event_type == "interaction.created" + assert chunk.id == "resp_123" + assert it.sent_interaction_start is True + + def test_response_created_emits_interaction_start_legacy(self): + it = self._make_iterator(use_legacy=True) + + chunk = it._transform_responses_chunk_to_interactions_chunk( + self._make_response_created() + ) + + assert chunk is not None + assert chunk.event_type == "interaction.start" + assert chunk.id == "resp_123" + + def test_text_delta_sequence_new_schema(self): + """First chunk yields created + step.start + step.delta; later chunks yield step.delta.""" + it = self._make_iterator(use_legacy=False) + + first_events = it._events_for_chunk(self._make_text_delta("Hello")) + assert [e.event_type for e in first_events] == [ + "interaction.created", + "step.start", + "step.delta", + ] + assert first_events[-1].delta == {"type": "text", "text": "Hello"} + assert it.sent_interaction_start is True + assert it.sent_content_start is True + + second_events = it._events_for_chunk(self._make_text_delta(" World")) + assert [e.event_type for e in second_events] == ["step.delta"] + assert second_events[0].delta == {"type": "text", "text": " World"} + + third_events = it._events_for_chunk(self._make_text_delta("!")) + assert [e.event_type for e in third_events] == ["step.delta"] + assert third_events[0].delta == {"type": "text", "text": "!"} + + def test_text_delta_sequence_legacy_schema(self): + """Legacy: first chunk yields interaction.start + content.start + content.delta.""" + it = self._make_iterator(use_legacy=True) + + first_events = it._events_for_chunk(self._make_text_delta("Hello")) + assert [e.event_type for e in first_events] == [ + "interaction.start", + "content.start", + "content.delta", + ] + assert first_events[-1].delta == {"type": "text", "text": "Hello"} + + second_events = it._events_for_chunk(self._make_text_delta(" World")) + assert [e.event_type for e in second_events] == ["content.delta"] + assert second_events[0].delta == {"type": "text", "text": " World"} + + def test_first_text_delta_without_item_id_uses_fallback_id(self): + it = self._make_iterator(use_legacy=False) + event = self._make_text_delta("Hi") + event.item_id = None + + events = it._events_for_chunk(event) + + assert events[0].event_type == "interaction.created" + assert events[0].id == f"interaction_{id(it)}" + + def test_first_text_delta_emits_text_via_compat_shim(self): + """The legacy single-chunk shim must surface the synthetic events AND the delta.""" + it = self._make_iterator(use_legacy=False) + + first = it._transform_responses_chunk_to_interactions_chunk( + self._make_text_delta("Hello") + ) + assert first is not None + assert first.event_type == "interaction.created" + + second = it.__next__() if it._pending_events else None + assert second is not None + assert second.event_type == "step.start" + + third = it.__next__() if it._pending_events else None + assert third is not None + assert third.event_type == "step.delta" + assert third.delta == {"type": "text", "text": "Hello"} + + def test_response_created_then_text_delta_emits_step_start_and_delta(self): + """Realistic flow: response.created arrives first, then text delta.""" + it = self._make_iterator(use_legacy=False) + + first = it._events_for_chunk(self._make_response_created()) + assert [e.event_type for e in first] == ["interaction.created"] + + second = it._events_for_chunk(self._make_text_delta("Hello")) + assert [e.event_type for e in second] == ["step.start", "step.delta"] + assert second[-1].delta == {"type": "text", "text": "Hello"} + + def test_no_text_token_is_dropped_during_streaming(self): + """Concatenated step.delta payloads must equal the upstream text.""" + it = self._make_iterator(use_legacy=False) + + chunks = ["Hello", " ", "world", "!"] + emitted_text = "" + for c in chunks: + for ev in it._events_for_chunk(self._make_text_delta(c)): + if ev.event_type == "step.delta": + assert ev.delta is not None + emitted_text += ev.delta["text"] + + assert emitted_text == "Hello world!" + + def test_stop_iteration_fallback_emits_completion_event(self): + """If upstream ends without ResponseCompletedEvent, terminal events still flow.""" + from unittest.mock import MagicMock + + text_event = self._make_text_delta("hi") + sync_iter = MagicMock() + sync_iter.__iter__ = lambda self: self + sync_iter.__next__ = MagicMock(side_effect=[text_event, StopIteration]) + + original = litellm.use_legacy_interactions_schema + litellm.use_legacy_interactions_schema = False + try: + it = LiteLLMResponsesInteractionsStreamingIterator( + model="gpt-5.4", + litellm_custom_stream_wrapper=sync_iter, + request_input="hi", + optional_params={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + emitted: list = [] + try: + while True: + emitted.append(next(it)) + except StopIteration: + pass + + event_types = [e.event_type for e in emitted] + assert event_types == [ + "interaction.created", + "step.start", + "step.delta", + "step.stop", + "interaction.completed", + ] + terminal = emitted[-1] + assert terminal.steps == [ + { + "type": "model_output", + "content": [{"type": "text", "text": "hi"}], + } + ] + # EOF-flushed terminal event must carry the same id as interaction.created. + assert terminal.id == emitted[0].id == "item_1" + + def test_response_completed_emits_stop_then_completion(self): + """ResponseCompletedEvent expands into step.stop + interaction.completed.""" + from unittest.mock import MagicMock + + text_event = self._make_text_delta("hi") + completed = MagicMock(spec=ResponseCompletedEvent) + completed.response = MagicMock(id="resp_999") + + sync_iter = MagicMock() + sync_iter.__iter__ = lambda self: self + sync_iter.__next__ = MagicMock(side_effect=[text_event, completed]) + + original = litellm.use_legacy_interactions_schema + litellm.use_legacy_interactions_schema = False + try: + it = LiteLLMResponsesInteractionsStreamingIterator( + model="gpt-5.4", + litellm_custom_stream_wrapper=sync_iter, + request_input="hi", + optional_params={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + emitted: list = [] + try: + while True: + emitted.append(next(it)) + except StopIteration: + pass + + event_types = [e.event_type for e in emitted] + assert event_types == [ + "interaction.created", + "step.start", + "step.delta", + "step.stop", + "interaction.completed", + ] + # StopIteration fallback path must NOT add a duplicate completion event. + assert event_types.count("interaction.completed") == 1 + # When the stream starts directly with a text delta (no preceding + # response.created), the terminal events must reuse the id derived from + # the first chunk's item_id rather than switching to response.id, so + # consumers can correlate the start and completion events by id. + assert emitted[0].id == "item_1" + assert emitted[-1].id == "item_1" + + class TestInteractionOperationUrls: """Test that get/delete/cancel interaction URLs exclude API key.""" @@ -410,3 +556,152 @@ class TestInteractionOperationUrls: litellm_params=GenericLiteLLMParams(api_key=None), headers={}, ) + + +class TestTransformRequestSchemaCoalescing: + """Test new-schema request coalescing (Api-Revision: 2026-05-20).""" + + def test_response_mime_type_folded_into_response_format(self, config): + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = False + body = config.transform_request( + model="gemini/gemini-2.5-flash", + agent=None, + input="summarise", + optional_params={ + "response_mime_type": "application/json", + "response_format": {"type": "object", "properties": {}}, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + # response_mime_type must not appear as a top-level body key + assert "response_mime_type" not in body + rf = body["response_format"] + assert rf["type"] == "text" + assert rf["mime_type"] == "application/json" + assert "schema" in rf + + def test_image_config_moved_to_response_format(self, config): + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = False + body = config.transform_request( + model="gemini/gemini-2.5-flash", + agent=None, + input="draw a sunset", + optional_params={ + "generation_config": { + "temperature": 0.7, + "image_config": {"aspect_ratio": "1:1", "image_size": "1K"}, + } + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + # image_config removed from generation_config + assert "image_config" not in body.get("generation_config", {}) + # moved into response_format with type=image + rf = body["response_format"] + assert rf["type"] == "image" + assert rf["aspect_ratio"] == "1:1" + + def test_response_mime_type_skipped_when_response_format_is_list(self, config): + """Lists are already polymorphic; do not wrap them into schema.""" + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = False + rf_list = [ + {"type": "text", "mime_type": "application/json"}, + {"type": "image", "aspect_ratio": "1:1"}, + ] + body = config.transform_request( + model="gemini/gemini-2.5-flash", + agent=None, + input="multimodal", + optional_params={ + "response_format": rf_list, + "response_mime_type": "application/json", + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + assert body["response_format"] == rf_list + assert "response_mime_type" not in body + + def test_image_config_appended_to_response_format_list_without_mutating_input( + self, config + ): + """When response_format is already a list, image_config must not mutate optional_params.""" + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = False + text_rf = {"type": "text", "mime_type": "application/json"} + optional_params = { + "response_format": [text_rf], + "generation_config": { + "image_config": {"aspect_ratio": "16:9", "image_size": "2K"}, + }, + } + original_rf = optional_params["response_format"] + + body = config.transform_request( + model="gemini/gemini-2.5-flash", + agent=None, + input="draw and summarise", + optional_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert optional_params["response_format"] is original_rf + assert len(optional_params["response_format"]) == 1 + assert body["response_format"] == [ + text_rf, + {"type": "image", "aspect_ratio": "16:9", "image_size": "2K"}, + ] + + # Retry must not append a second image entry into the caller's list. + body_retry = config.transform_request( + model="gemini/gemini-2.5-flash", + agent=None, + input="draw and summarise", + optional_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + assert len(optional_params["response_format"]) == 1 + assert body_retry["response_format"] == body["response_format"] + finally: + litellm.use_legacy_interactions_schema = original + + def test_legacy_schema_passes_fields_unchanged(self, config): + original = litellm.use_legacy_interactions_schema + try: + litellm.use_legacy_interactions_schema = True + body = config.transform_request( + model="gemini/gemini-2.5-flash", + agent=None, + input="hello", + optional_params={ + "response_mime_type": "application/json", + "generation_config": {"image_config": {"aspect_ratio": "16:9"}}, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + finally: + litellm.use_legacy_interactions_schema = original + + assert body["response_mime_type"] == "application/json" + assert body["generation_config"]["image_config"]["aspect_ratio"] == "16:9" diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index a19752dc648..7d9e4768303 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -2476,6 +2476,120 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models(): ), f"output_config should not be set for {model}" +@pytest.mark.parametrize( + "reasoning_effort_value", + [ + # String shape — what callers send when using `reasoning_effort="low"` directly. + "low", + # Dict shape with `effort` only — what the Responses->Chat parser produces + # when `reasoning={"effort": "low"}` is set without `summary`. + {"effort": "low"}, + # Dict shape with `effort` AND `summary` — what the Responses->Chat parser + # produces when callers send `Reasoning(effort="low", summary="concise")`. + # PR #25359 added the dict-keeping branch for this case, but the Anthropic + # transformation must coerce the dict back to a string before mapping. + {"effort": "low", "summary": "concise"}, + {"effort": "low", "summary": "detailed"}, + ], +) +def test_reasoning_effort_accepts_dict_shape_for_adaptive_model(reasoning_effort_value): + """ + Adaptive-thinking (Claude 4.6+) branch: dict-shape reasoning_effort must + map to ``thinking.type='adaptive'`` + ``output_config.effort``. + + Regression test for the dict-shape ``reasoning_effort`` produced by the + Responses->Chat parser when ``summary`` is set on the request's + ``reasoning`` field. Before this fix, the Anthropic transformation guarded + on ``isinstance(value, str)`` and silently dropped the param — disabling + extended thinking entirely. + """ + config = AnthropicConfig() + + result = config.map_openai_params( + non_default_params={"reasoning_effort": reasoning_effort_value}, + optional_params={}, + model="claude-sonnet-4-6-20260219", + drop_params=False, + ) + + # thinking must be set (adaptive for 4.6+) + assert "thinking" in result, ( + f"thinking missing for reasoning_effort={reasoning_effort_value!r}" + ) + assert result["thinking"]["type"] == "adaptive" + # output_config must carry the mapped effort + assert "output_config" in result, ( + f"output_config missing for reasoning_effort={reasoning_effort_value!r}" + ) + assert result["output_config"]["effort"] == "low" + + +@pytest.mark.parametrize( + "reasoning_effort_value", + [ + "low", + {"effort": "low"}, + {"effort": "low", "summary": "concise"}, + ], +) +def test_reasoning_effort_accepts_dict_shape_for_non_adaptive_model(reasoning_effort_value): + """ + Non-adaptive (pre-4.6) branch: dict-shape reasoning_effort must still map + to ``thinking.type='enabled'`` + ``budget_tokens``. ``output_config`` must + NOT be set on these models. + """ + config = AnthropicConfig() + + result = config.map_openai_params( + non_default_params={"reasoning_effort": reasoning_effort_value}, + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + assert "thinking" in result, ( + f"thinking missing for reasoning_effort={reasoning_effort_value!r}" + ) + assert result["thinking"]["type"] == "enabled" + assert "budget_tokens" in result["thinking"] + assert result["thinking"]["budget_tokens"] > 0 + # Older models must not get adaptive-thinking output_config + assert "output_config" not in result, ( + f"output_config should not be set for non-adaptive model " + f"(reasoning_effort={reasoning_effort_value!r})" + ) + + +@pytest.mark.parametrize( + "bad_value", + [ + {"summary": "concise"}, # missing effort + {"effort": None}, # explicit None effort + {"effort": 123}, # non-string effort + ], +) +def test_reasoning_effort_unparseable_dict_is_dropped(bad_value): + """ + A dict shape that doesn't carry a usable ``effort`` key (e.g. only + ``summary`` is set, or the value is some other unexpected type) should be + silently dropped — not crash, not partially apply. + """ + config = AnthropicConfig() + + result = config.map_openai_params( + non_default_params={"reasoning_effort": bad_value}, + optional_params={}, + model="claude-sonnet-4-6-20260219", + drop_params=False, + ) + assert "thinking" not in result, ( + f"thinking should not be set for bad value {bad_value!r}" + ) + assert "output_config" not in result, ( + f"output_config should not be set for bad value {bad_value!r}" + ) + + @pytest.mark.parametrize( "model", [ diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index db993245426..45b9f4293fa 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -2097,6 +2097,125 @@ def test_is_gemini_3_or_newer(): assert VertexGeminiConfig._is_gemini_3_or_newer("") == False +def test_forward_gemini_function_call_id_vertex_vs_google_ai_studio(): + """Vertex AI rejects `id` on function_call/function_response; Google AI Studio accepts it on Gemini 3.5+.""" + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + model = "gemini-3.5-flash" + assert ( + VertexGeminiConfig._forward_gemini_function_call_id(model, "vertex_ai") is False + ) + assert ( + VertexGeminiConfig._forward_gemini_function_call_id(model, "vertex_ai_beta") + is False + ) + assert VertexGeminiConfig._forward_gemini_function_call_id(model, "gemini") is True + assert VertexGeminiConfig._forward_gemini_function_call_id(model, None) is False + assert ( + VertexGeminiConfig._forward_gemini_function_call_id( + "gemini-2.5-flash", "gemini" + ) + is False + ) + + +def test_vertex_ai_gemini_35_tool_calls_omit_function_call_id(): + """Regression: Vertex must not send OpenAI tool_call id inside Gemini function_call parts.""" + from litellm.llms.vertex_ai.gemini.transformation import ( + _gemini_convert_messages_with_history, + ) + + messages = [ + {"role": "user", "content": "Explore this directory"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_50e7e0fe0989464a89f188eda443", + "type": "function", + "function": { + "name": "read", + "arguments": '{"filePath": "/tmp"}', + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_50e7e0fe0989464a89f188eda443", + "content": "ok", + }, + ] + + contents = _gemini_convert_messages_with_history( + messages=messages, + model="gemini-3.5-flash", + custom_llm_provider="vertex_ai", + ) + + for content in contents: + for part in content.get("parts", []): + fc = part.get("function_call") + if fc is not None: + assert "id" not in fc, f"Vertex payload must not include id: {fc}" + fr = part.get("function_response") + if fr is not None: + assert "id" not in fr, f"Vertex payload must not include id: {fr}" + + +def test_google_ai_studio_gemini_35_tool_calls_include_function_call_id(): + from litellm.llms.vertex_ai.gemini.transformation import ( + _gemini_convert_messages_with_history, + ) + + tool_call_id = "call_50e7e0fe0989464a89f188eda443" + messages = [ + {"role": "user", "content": "hi"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": tool_call_id, + "type": "function", + "function": { + "name": "read", + "arguments": '{"filePath": "/tmp"}', + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": tool_call_id, + "content": "ok", + }, + ] + + contents = _gemini_convert_messages_with_history( + messages=messages, + model="gemini-3.5-flash", + custom_llm_provider="gemini", + ) + + function_call_ids = [] + function_response_ids = [] + for content in contents: + for part in content.get("parts", []): + fc = part.get("function_call") + if fc is not None: + function_call_ids.append(fc.get("id")) + fr = part.get("function_response") + if fr is not None: + function_response_ids.append(fr.get("id")) + + assert function_call_ids == [tool_call_id] + assert function_response_ids == [tool_call_id] + + def test_reasoning_effort_maps_to_thinking_level_gemini_3(): """Test that reasoning_effort maps to thinking_level AND includeThoughts for Gemini 3+ models""" from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( @@ -3531,7 +3650,12 @@ def test_video_metadata_supported_for_all_gemini_models(): } ] - for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-pro-preview"]: + for model in [ + "gemini-1.5-pro", + "gemini-2.5-flash", + "gemini-2.5-pro", + "gemini-3-pro-preview", + ]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = None @@ -3541,19 +3665,25 @@ def test_video_metadata_supported_for_all_gemini_models(): break assert file_part is not None, f"{model}: file part should exist" - assert "video_metadata" in file_part, f"{model}: video_metadata should be present" + assert ( + "video_metadata" in file_part + ), f"{model}: video_metadata should be present" assert file_part["video_metadata"]["fps"] == 5, f"{model}: fps should be 5" # Per-part media_resolution is Gemini 3+ only; 2.x uses generation_config global for model in ["gemini-3-pro-preview"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = next(p for p in contents[0]["parts"] if "file_data" in p) - assert "media_resolution" in file_part, f"{model}: media_resolution should be present" + assert ( + "media_resolution" in file_part + ), f"{model}: media_resolution should be present" for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = next(p for p in contents[0]["parts"] if "file_data" in p) - assert "media_resolution" not in file_part, f"{model}: per-part media_resolution should not be set" + assert ( + "media_resolution" not in file_part + ), f"{model}: per-part media_resolution should not be set" def test_chunk_parser_handles_prompt_feedback_block(): @@ -4186,8 +4316,9 @@ def test_vertex_ai_usage_metadata_with_document_tokens_in_prompt(): # DOCUMENT tokens should be included in text_tokens: 8 (TEXT) + 774 (DOCUMENT) = 782 assert result.prompt_tokens_details is not None - assert result.prompt_tokens_details.text_tokens == 782, \ - "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)" + assert ( + result.prompt_tokens_details.text_tokens == 782 + ), "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)" # Verify completion token details assert result.completion_tokens_details is not None @@ -4222,8 +4353,9 @@ def test_vertex_ai_usage_metadata_with_document_tokens_cached(): # DOCUMENT cached tokens map to cached_text_tokens, so: # text_tokens = (8 TEXT + 774 DOCUMENT) - 400 cached = 382 - assert result.prompt_tokens_details.text_tokens == 382, \ - "text_tokens should be (8 + 774) - 400 cached = 382" + assert ( + result.prompt_tokens_details.text_tokens == 382 + ), "text_tokens should be (8 + 774) - 400 cached = 382" assert result.prompt_tokens_details.cached_tokens == 400 @@ -4693,7 +4825,9 @@ def test_mid_stream_429_error_raises_during_iteration(): { "content": { "role": "model", - "parts": [{"text": "Let me think about this...", "thought": True}], + "parts": [ + {"text": "Let me think about this...", "thought": True} + ], }, "index": 0, } @@ -4713,7 +4847,9 @@ def test_mid_stream_429_error_raises_during_iteration(): { "content": { "role": "model", - "parts": [{"text": "I'll generate the image now.", "thought": True}], + "parts": [ + {"text": "I'll generate the image now.", "thought": True} + ], }, "index": 0, } 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 index 66b96785f69..9f2feddb0e3 100644 --- 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 @@ -1229,6 +1229,143 @@ def test_validate_trusted_redirect_uri_rejects_fragment_and_bad_scheme(): assert exc.value.status_code == 400, uri +def test_validate_trusted_redirect_uri_accepts_cursor_native_callback(): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + req = _make_trusted_request("http://localhost:4000/") + validate_trusted_redirect_uri(req, "cursor://anysphere.cursor-mcp/oauth/callback") + + +def test_validate_trusted_redirect_uri_rejects_unlisted_native_callback( + monkeypatch, +): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + monkeypatch.setenv("MCP_TRUSTED_NATIVE_REDIRECT_URIS", "") + # Clear defaults by patching — env-only path for this test + monkeypatch.setattr( + "litellm.proxy._experimental.mcp_server.oauth_utils._DEFAULT_NATIVE_REDIRECT_URIS", + [], + ) + req = _make_trusted_request("http://localhost:4000/") + with pytest.raises(HTTPException) as exc: + validate_trusted_redirect_uri( + req, "cursor://anysphere.cursor-mcp/oauth/callback" + ) + assert exc.value.status_code == 400 + + +def test_validate_trusted_redirect_uri_accepts_env_native_redirect_uri( + monkeypatch, +): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + monkeypatch.setattr( + "litellm.proxy._experimental.mcp_server.oauth_utils._DEFAULT_NATIVE_REDIRECT_URIS", + [], + ) + monkeypatch.setenv( + "MCP_TRUSTED_NATIVE_REDIRECT_URIS", + "vscode://my-app/oauth/callback", + ) + req = _make_trusted_request("http://localhost:4000/") + validate_trusted_redirect_uri(req, "vscode://my-app/oauth/callback") + + +def test_validate_trusted_redirect_uri_rejects_native_callback_with_fragment(): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + req = _make_trusted_request("http://localhost:4000/") + with pytest.raises(HTTPException) as exc: + validate_trusted_redirect_uri( + req, "cursor://anysphere.cursor-mcp/oauth/callback#frag" + ) + assert exc.value.status_code == 400 + + +def test_validate_trusted_redirect_uri_rejects_native_callback_with_query(): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + req = _make_trusted_request("http://localhost:4000/") + with pytest.raises(HTTPException) as exc: + validate_trusted_redirect_uri( + req, + "cursor://anysphere.cursor-mcp/oauth/callback?injected=anything", + ) + assert exc.value.status_code == 400 + + +def test_validate_trusted_redirect_uri_native_path_case_insensitive(monkeypatch): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + monkeypatch.setattr( + "litellm.proxy._experimental.mcp_server.oauth_utils._DEFAULT_NATIVE_REDIRECT_URIS", + [], + ) + monkeypatch.setenv( + "MCP_TRUSTED_NATIVE_REDIRECT_URIS", + "myapp://host/MyPath", + ) + req = _make_trusted_request("http://localhost:4000/") + validate_trusted_redirect_uri(req, "myapp://host/MyPath") + + +def test_validate_trusted_redirect_uri_native_wildcard_respects_path_boundary( + monkeypatch, +): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + monkeypatch.setattr( + "litellm.proxy._experimental.mcp_server.oauth_utils._DEFAULT_NATIVE_REDIRECT_URIS", + [], + ) + monkeypatch.setenv( + "MCP_TRUSTED_NATIVE_REDIRECT_URIS", + "cursor://anysphere.cursor-mcp/oauth/callback*", + ) + req = _make_trusted_request("http://localhost:4000/") + validate_trusted_redirect_uri( + req, "cursor://anysphere.cursor-mcp/oauth/callback/extra" + ) + with pytest.raises(HTTPException): + validate_trusted_redirect_uri( + req, "cursor://anysphere.cursor-mcp/oauth/callback-2" + ) + + +def test_validate_trusted_redirect_uri_native_wildcard_directory_prefix( + monkeypatch, +): + from litellm.proxy._experimental.mcp_server.oauth_utils import ( + validate_trusted_redirect_uri, + ) + + monkeypatch.setattr( + "litellm.proxy._experimental.mcp_server.oauth_utils._DEFAULT_NATIVE_REDIRECT_URIS", + [], + ) + monkeypatch.setenv( + "MCP_TRUSTED_NATIVE_REDIRECT_URIS", + "cursor://anysphere.cursor-mcp/oauth/*", + ) + req = _make_trusted_request("http://localhost:4000/") + validate_trusted_redirect_uri(req, "cursor://anysphere.cursor-mcp/oauth/callback") + + def test_validate_trusted_redirect_uri_rejects_scheme_mismatch_on_same_host(): """Regression: an attacker who can serve http on the proxy's own host (e.g. by MITMing an unencrypted LAN hop) must not be able to 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 83a5fea8758..bb6134a5bfd 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 @@ -890,6 +890,7 @@ async def test_get_tools_from_mcp_servers_continues_when_one_server_fails(): extra_headers=None, add_prefix=True, raw_headers=None, + user_api_key_auth=None, ): if server.name == "working_server": # Working server returns tools @@ -995,6 +996,7 @@ async def test_get_tools_from_mcp_servers_handles_all_servers_failing(): extra_headers=None, add_prefix=True, raw_headers=None, + user_api_key_auth=None, ): # All servers fail raise Exception(f"Server {server.name} connection failed") @@ -1455,6 +1457,7 @@ async def test_list_tools_single_server_unprefixed_names(): extra_headers=None, add_prefix=False, raw_headers=None, + user_api_key_auth=None, ): tool = MagicMock() tool.name = f"{server.alias}-toolA" if add_prefix else "toolA" @@ -1536,6 +1539,7 @@ async def test_list_tools_multiple_servers_prefixed_names(): extra_headers=None, add_prefix=True, raw_headers=None, + user_api_key_auth=None, ): tool = MagicMock() # When multiple servers, add_prefix should be True -> prefixed names @@ -1802,6 +1806,7 @@ async def test_list_tools_filters_by_key_team_permissions(): extra_headers=None, add_prefix=False, raw_headers=None, + user_api_key_auth=None, ): # Return 4 tools, but only 2 should be allowed tool1 = MagicMock() @@ -1911,6 +1916,7 @@ async def test_list_tools_with_team_tool_permissions_inheritance(): extra_headers=None, add_prefix=False, raw_headers=None, + user_api_key_auth=None, ): # Return 4 tools tool1 = MagicMock() @@ -2006,6 +2012,7 @@ async def test_list_tools_with_no_tool_permissions_shows_all(): extra_headers=None, add_prefix=False, raw_headers=None, + user_api_key_auth=None, ): # Return 3 tools tool1 = MagicMock() @@ -2104,6 +2111,7 @@ async def test_list_tools_strips_prefix_when_matching_permissions(): extra_headers=None, add_prefix=True, raw_headers=None, + user_api_key_auth=None, ): # Return tools WITH prefix (as they come from MCP server) tool1 = MagicMock() diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index ef1c09aa815..d7078412a44 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -322,6 +322,7 @@ class TestMCPServerManager: mcp_auth_header=None, mcp_protocol_version=None, raw_headers=None, + user_api_key_auth=None, ): if server.name == "github": tool1 = MagicMock() @@ -376,6 +377,7 @@ class TestMCPServerManager: mcp_auth_header=None, mcp_protocol_version=None, raw_headers=None, + user_api_key_auth=None, ): assert mcp_auth_header == "legacy-token" # Should use legacy header tool = MagicMock() @@ -414,6 +416,7 @@ class TestMCPServerManager: mcp_auth_header=None, mcp_protocol_version=None, raw_headers=None, + user_api_key_auth=None, ): assert ( mcp_auth_header == "server-specific-token" @@ -1004,6 +1007,7 @@ class TestMCPServerManager: mcp_auth_header=None, mcp_protocol_version=None, raw_headers=None, + user_api_key_auth=None, ): assert ( mcp_auth_header == "server-specific-token" @@ -1801,6 +1805,258 @@ class TestMCPServerManager: assert len(tools_unprefixed) == 1 assert tools_unprefixed[0].name == "send_email" + @pytest.mark.asyncio + async def test_get_tools_from_server_jwt_skipped_when_mcp_auth_header_set(self): + """When a per-user mcp_auth_header is resolved, JWT injection must be skipped. + + MCPClient._get_auth_headers() applies extra_headers AFTER writing + Authorization from auth_value, so an injected JWT would clobber the + user's per-server OAuth token. Regression test for that interaction. + """ + from litellm.proxy._types import UserAPIKeyAuth + + manager = MCPServerManager() + server = MCPServer( + server_id="zapier", + name="zapier", + transport=MCPTransport.http, + ) + + manager._create_mcp_client = AsyncMock(return_value=object()) + manager._fetch_tools_with_timeout = AsyncMock(return_value=[]) + + user_auth = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + + with ( + patch( + "litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer.get_mcp_jwt_signer", + return_value=MagicMock(), + ), + patch( + "litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer.inject_mcp_jwt_headers_for_upstream", + new=AsyncMock(return_value={"Authorization": "Bearer signed-jwt"}), + ) as mock_inject, + ): + # Case A: mcp_auth_header present -> JWT must NOT be injected + await manager._get_tools_from_server( + server, + mcp_auth_header="oauth-user-token", + user_api_key_auth=user_auth, + ) + mock_inject.assert_not_called() + + # Case B: no mcp_auth_header -> JWT injection runs as before + await manager._get_tools_from_server( + server, + user_api_key_auth=user_auth, + ) + mock_inject.assert_awaited_once() + + def test_resolve_mcp_server_for_tool_call_via_prefixed_name(self): + """Resolution succeeds when the prefixed tool name is in the mapping.""" + manager = MCPServerManager() + server = MCPServer( + server_id="jira", + name="jira", + transport=MCPTransport.http, + ) + manager.registry = {"jira": server} + manager.tool_name_to_mcp_server_name_mapping["jira-search_issues"] = "jira" + manager.tool_name_to_mcp_server_name_mapping["search_issues"] = "jira" + + resolved = manager._resolve_mcp_server_for_tool_call("jira", "search_issues") + assert resolved is server + + def test_resolve_mcp_server_for_tool_call_via_alias(self): + """Resolution falls back to alias/server_name match in the registry.""" + manager = MCPServerManager() + server = MCPServer( + server_id="srv-uuid-123", + name="zapier", + alias="zapier-alias", + transport=MCPTransport.http, + ) + manager.registry = {"srv-uuid-123": server} + manager.tool_name_to_mcp_server_name_mapping["create_zap"] = "zapier" + + resolved = manager._resolve_mcp_server_for_tool_call( + "zapier-alias", "create_zap" + ) + assert resolved is server + + def test_resolve_mcp_server_for_tool_call_unknown_tool_with_empty_mapping(self): + """Server-name match alone must not let unknown tools through when the + mapping has no entries for that server (e.g. listing has not completed + or the server is OAuth2 and the user has not yet listed tools). + """ + manager = MCPServerManager() + server = MCPServer( + server_id="srv-uuid-123", + name="zapier", + alias="zapier-alias", + transport=MCPTransport.http, + ) + manager.registry = {"srv-uuid-123": server} + + with pytest.raises(ValueError, match="Tool create_zap not found"): + manager._resolve_mcp_server_for_tool_call("zapier-alias", "create_zap") + + def test_resolve_mcp_server_for_tool_call_fallback_to_unprefixed_lookup(self): + """Fallback to unprefixed _get_mcp_server_from_tool_name when other paths fail.""" + manager = MCPServerManager() + server = MCPServer( + server_id="linear", + name="linear", + transport=MCPTransport.http, + ) + manager.registry = {"linear": server} + manager.tool_name_to_mcp_server_name_mapping["create_issue"] = "linear" + + # server_name is empty so the fallback unprefixed lookup runs and matches. + resolved = manager._resolve_mcp_server_for_tool_call("", "create_issue") + assert resolved is server + + def test_resolve_mcp_server_for_tool_call_raises_when_not_found(self): + """ValueError is raised when no resolution path finds the tool.""" + manager = MCPServerManager() + with pytest.raises(ValueError, match="Tool .* not found"): + manager._resolve_mcp_server_for_tool_call("nonexistent", "ghost_tool") + + def test_resolve_mcp_server_for_tool_call_unknown_tool_with_known_server(self): + """Server-name match alone must not let unknown tools slip through. + + If the registry has tools for this server but neither the prefixed nor + unprefixed tool name is in the mapping, raise rather than returning the + server (would otherwise allow tool enumeration via name spoofing). + """ + manager = MCPServerManager() + server = MCPServer( + server_id="github", + name="github", + transport=MCPTransport.http, + ) + manager.registry = {"github": server} + # Mapping has *some* tools for github but not "missing_tool". + manager.tool_name_to_mcp_server_name_mapping["github-list_repos"] = "github" + manager.tool_name_to_mcp_server_name_mapping["list_repos"] = "github" + + with pytest.raises(ValueError, match="Tool missing_tool not found"): + manager._resolve_mcp_server_for_tool_call("github", "missing_tool") + + @pytest.mark.asyncio + async def test_resolve_oauth2_headers_skipped_when_not_user_oauth(self): + """Returns input headers unchanged when server does not need user OAuth.""" + from litellm.proxy._types import UserAPIKeyAuth + + manager = MCPServerManager() + server = MCPServer( + server_id="plain", + name="plain", + transport=MCPTransport.http, + ) + # needs_user_oauth_token defaults to False. + user_auth = UserAPIKeyAuth(api_key="sk-test", user_id="bob") + + result = await manager._resolve_oauth2_headers_for_tool_call( + server, oauth2_headers=None, user_api_key_auth=user_auth + ) + assert result is None + + @pytest.mark.asyncio + async def test_resolve_oauth2_headers_returns_client_supplied_token(self): + """Returns the client's oauth2_headers as-is when already set.""" + from litellm.proxy._types import UserAPIKeyAuth + + manager = MCPServerManager() + server = MCPServer( + server_id="oauth-srv", + name="oauth-srv", + transport=MCPTransport.http, + auth_type=MCPAuth.oauth2, + ) + assert server.needs_user_oauth_token is True + user_auth = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + supplied = {"Authorization": "Bearer client-supplied"} + + result = await manager._resolve_oauth2_headers_for_tool_call( + server, oauth2_headers=supplied, user_api_key_auth=user_auth + ) + assert result is supplied + + @pytest.mark.asyncio + async def test_resolve_oauth2_headers_looks_up_stored_token(self): + """Falls back to stored per-user OAuth headers when no token is supplied.""" + from litellm.proxy._types import UserAPIKeyAuth + + manager = MCPServerManager() + server = MCPServer( + server_id="oauth-srv", + name="oauth-srv", + transport=MCPTransport.http, + auth_type=MCPAuth.oauth2, + ) + user_auth = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + stored = {"Authorization": "Bearer stored-user-token"} + + with patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new=AsyncMock(return_value=stored), + ) as mock_lookup: + result = await manager._resolve_oauth2_headers_for_tool_call( + server, oauth2_headers=None, user_api_key_auth=user_auth + ) + + assert result == stored + mock_lookup.assert_awaited_once() + + @pytest.mark.asyncio + async def test_resolve_oauth2_headers_swallows_lookup_exception(self): + """Returns supplied headers (None) when the stored-token lookup raises.""" + from litellm.proxy._types import UserAPIKeyAuth + + manager = MCPServerManager() + server = MCPServer( + server_id="oauth-srv", + name="oauth-srv", + transport=MCPTransport.http, + auth_type=MCPAuth.oauth2, + ) + user_auth = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + + with patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new=AsyncMock(side_effect=RuntimeError("redis down")), + ): + result = await manager._resolve_oauth2_headers_for_tool_call( + server, oauth2_headers=None, user_api_key_auth=user_auth + ) + assert result is None + + @pytest.mark.asyncio + async def test_resolve_oauth2_headers_no_user_id(self): + """Skip lookup entirely when user_api_key_auth has no user_id.""" + from litellm.proxy._types import UserAPIKeyAuth + + manager = MCPServerManager() + server = MCPServer( + server_id="oauth-srv", + name="oauth-srv", + transport=MCPTransport.http, + auth_type=MCPAuth.oauth2, + ) + # user_id is None -> lookup must not happen + user_auth = UserAPIKeyAuth(api_key="sk-test") + + with patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new=AsyncMock(return_value={"Authorization": "Bearer x"}), + ) as mock_lookup: + result = await manager._resolve_oauth2_headers_for_tool_call( + server, oauth2_headers=None, user_api_key_auth=user_auth + ) + assert result is None + mock_lookup.assert_not_called() + def test_create_prefixed_tools_updates_mapping_for_both_forms(self): """_create_prefixed_tools should populate mapping for prefixed and original names even when not adding prefix in output.""" manager = MCPServerManager() diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py index f4feac68fcc..593facd9279 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py @@ -1,5 +1,6 @@ import json from typing import Any, Dict, Optional +from unittest.mock import MagicMock import pytest from fastapi import HTTPException @@ -796,6 +797,25 @@ class TestCallToolRestAPI: raising=False, ) + mock_server = MagicMock() + mock_server.server_id = "server-1" + + def fake_get_mcp_server_by_id(server_id): + return mock_server if server_id == "server-1" else None + + monkeypatch.setattr( + rest_endpoints.global_mcp_server_manager, + "get_mcp_server_by_id", + fake_get_mcp_server_by_id, + raising=False, + ) + monkeypatch.setattr( + rest_endpoints.global_mcp_server_manager, + "get_mcp_server_by_name", + lambda *args, **kwargs: None, + raising=False, + ) + request_payload = { "server_id": "server-1", "name": "demo-tool", diff --git a/tests/test_litellm/proxy/auth/test_model_checks.py b/tests/test_litellm/proxy/auth/test_model_checks.py index 77aa03032a7..f38ac5c2000 100644 --- a/tests/test_litellm/proxy/auth/test_model_checks.py +++ b/tests/test_litellm/proxy/auth/test_model_checks.py @@ -249,3 +249,241 @@ def test_get_complete_model_list_byok_wildcard_expansion(): assert len(result) > 0 assert all(m.startswith("openai/") for m in result) assert "openai/*" not in result + + +def test_get_complete_model_list_expands_team_scoped_wildcard_with_stored_credential( + monkeypatch, +): + """ + Team-scoped BYOK wildcard deployments are stored under an internal model_name, + with the public wildcard name in model_info.team_public_model_name. + """ + import litellm + from litellm import Router + from litellm.proxy.auth import model_checks + from litellm.proxy.auth.model_checks import get_complete_model_list + from litellm.types.utils import CredentialItem + + monkeypatch.setattr( + litellm, + "credential_list", + [ + CredentialItem( + credential_name="openai-credential", + credential_info={"provider": "openai"}, + credential_values={ + "api_key": "stored-openai-key", + "api_base": "https://example.openai.test/v1", + }, + ) + ], + ) + + captured_params = {} + + def fake_get_provider_models(provider, litellm_params=None): + captured_params["provider"] = provider + captured_params["api_key"] = litellm_params.api_key + captured_params["api_base"] = litellm_params.api_base + captured_params["credential_name"] = litellm_params.litellm_credential_name + return ["gpt-4o"] + + monkeypatch.setattr(model_checks, "get_provider_models", fake_get_provider_models) + + router = Router( + model_list=[ + { + "model_name": "model_name_team-1_generated", + "litellm_params": { + "model": "openai/*", + "custom_llm_provider": "openai", + "litellm_credential_name": "openai-credential", + }, + "model_info": { + "team_id": "team-1", + "team_public_model_name": "openai/*", + }, + } + ] + ) + + result = get_complete_model_list( + key_models=[], + team_models=["openai/*"], + proxy_model_list=[], + user_model=None, + infer_model_from_keys=False, + llm_router=router, + team_id="team-1", + ) + + assert "openai/gpt-4o" in result + assert captured_params == { + "provider": "openai", + "api_key": "stored-openai-key", + "api_base": "https://example.openai.test/v1", + "credential_name": None, + } + + +def test_wildcard_credential_hydration_preserves_deployment_params( + monkeypatch, +): + import litellm + from litellm.proxy.auth import model_checks + from litellm.proxy.auth.model_checks import get_known_models_from_wildcard + from litellm.types.router import LiteLLM_Params + from litellm.types.utils import CredentialItem + + monkeypatch.setattr( + litellm, + "credential_list", + [ + CredentialItem( + credential_name="openai-credential", + credential_info={"provider": "openai"}, + credential_values={ + "api_key": "stored-openai-key", + "api_version": "credential-version", + "model": "openai/wrong-model", + "unexpected_field": "unexpected-value", + }, + ) + ], + ) + + captured_params = {} + + def fake_get_provider_models(provider, litellm_params=None): + captured_params["provider"] = provider + captured_params["model"] = litellm_params.model + captured_params["api_key"] = litellm_params.api_key + captured_params["api_version"] = litellm_params.api_version + captured_params["credential_name"] = litellm_params.litellm_credential_name + captured_params["has_unexpected_field"] = hasattr( + litellm_params, "unexpected_field" + ) + return ["gpt-4o"] + + monkeypatch.setattr(model_checks, "get_provider_models", fake_get_provider_models) + + result = get_known_models_from_wildcard( + wildcard_model="openai/*", + litellm_params=LiteLLM_Params( + model="openai/*", + custom_llm_provider="openai", + api_version="deployment-version", + litellm_credential_name="openai-credential", + ), + ) + + assert result == ["openai/gpt-4o"] + assert captured_params == { + "provider": "openai", + "model": "openai/*", + "api_key": "stored-openai-key", + "api_version": "deployment-version", + "credential_name": None, + "has_unexpected_field": False, + } + + +def test_wildcard_credential_hydration_preserves_missing_credential_name( + monkeypatch, +): + import litellm + from litellm.proxy.auth import model_checks + from litellm.proxy.auth.model_checks import get_known_models_from_wildcard + from litellm.types.router import LiteLLM_Params + + monkeypatch.setattr(litellm, "credential_list", []) + + captured_params = {} + + def fake_get_provider_models(provider, litellm_params=None): + captured_params["provider"] = provider + captured_params["api_key"] = litellm_params.api_key + captured_params["credential_name"] = litellm_params.litellm_credential_name + return ["gpt-4o"] + + monkeypatch.setattr(model_checks, "get_provider_models", fake_get_provider_models) + + result = get_known_models_from_wildcard( + wildcard_model="openai/*", + litellm_params=LiteLLM_Params( + model="openai/*", + custom_llm_provider="openai", + api_key=None, + litellm_credential_name="missing-credential", + ), + ) + + assert result == ["openai/gpt-4o"] + assert captured_params == { + "provider": "openai", + "api_key": None, + "credential_name": "missing-credential", + } + + +@pytest.mark.asyncio +async def test_get_available_models_for_user_expands_query_team_wildcard( + monkeypatch, +): + import litellm + from litellm import Router + from litellm.proxy.auth import model_checks + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.utils import get_available_models_for_user + from litellm.types.utils import CredentialItem + + monkeypatch.setattr( + litellm, + "credential_list", + [ + CredentialItem( + credential_name="openai-credential", + credential_info={"provider": "openai"}, + credential_values={"api_key": "stored-openai-key"}, + ) + ], + ) + + def fake_get_provider_models(provider, litellm_params=None): + assert litellm_params.api_key == "stored-openai-key" + assert litellm_params.litellm_credential_name is None + return ["gpt-4o-mini"] + + monkeypatch.setattr(model_checks, "get_provider_models", fake_get_provider_models) + + router = Router( + model_list=[ + { + "model_name": "model_name_team-1_generated", + "litellm_params": { + "model": "openai/*", + "custom_llm_provider": "openai", + "litellm_credential_name": "openai-credential", + }, + "model_info": { + "team_id": "team-1", + "team_public_model_name": "openai/*", + }, + } + ] + ) + + result = await get_available_models_for_user( + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-test", + models=[], + team_id="team-1", + team_models=["openai/*"], + ), + llm_router=router, + general_settings={}, + user_model=None, + team_id="team-1", + ) + + assert "openai/gpt-4o-mini" in result diff --git a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py index b4343f6b2e1..3d7cb1e35f3 100644 --- a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py @@ -16,6 +16,7 @@ sys.path.insert( import litellm from litellm.proxy._types import ProxyException from litellm.proxy.common_utils.http_parsing_utils import ( + _is_form_content_type, _read_request_body, _safe_get_request_headers, _safe_get_request_parsed_body, @@ -853,3 +854,145 @@ class TestGetTagsFromRequestBodyStringCoerce: tags = get_tags_from_request_body({"metadata": {"tags": ["x"]}}) assert tags == ["x"] + + +class TestIsFormContentType: + @pytest.mark.parametrize( + "content_type", + [ + "application/x-www-form-urlencoded", + "multipart/form-data", + "multipart/form-data; boundary=----WebKitFormBoundary", + "Application/X-WWW-Form-Urlencoded", + " multipart/form-data ", + "application/x-www-form-urlencoded; charset=utf-8", + ], + ) + def test_form_types_match(self, content_type): + assert _is_form_content_type(content_type) is True + + @pytest.mark.parametrize( + "content_type", + [ + "", + "application/json", + "application/json; charset=utf-8", + "application/form-json", + "multiform/anything", + "application/json; xform=1", + "application/xml-with-form-data-but-not-actually", + "text/plain", + "form", + ], + ) + def test_non_form_types_rejected(self, content_type): + assert _is_form_content_type(content_type) is False + + +class TestReadRequestBodyNonCanonicalContentType: + """A JSON body with a ``"form"``-substring Content-Type must parse as JSON.""" + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "content_type", + [ + "application/form-json", + "application/json; xform=1", + "multiform/anything", + ], + ) + async def test_json_body_with_formlike_content_type_parses_as_json( + self, content_type + ): + payload = {"user_config": {"model_list": []}, "model": "x"} + + mock_request = MagicMock() + mock_request.body = AsyncMock(return_value=orjson.dumps(payload)) + mock_request.form = AsyncMock(return_value={}) + mock_request.headers = {"content-type": content_type} + mock_request.scope = {} + + result = await _read_request_body(mock_request) + assert result == payload + mock_request.form.assert_not_called() + + @pytest.mark.asyncio + async def test_real_form_post_still_parsed_as_form(self): + mock_request = MagicMock() + mock_request.form = AsyncMock(return_value={"k": "v"}) + mock_request.body = AsyncMock(return_value=b"") + mock_request.headers = {"content-type": "application/x-www-form-urlencoded"} + mock_request.scope = {} + + result = await _read_request_body(mock_request) + assert result == {"k": "v"} + mock_request.form.assert_awaited_once() + + +class TestReadRequestBodyFormParseFailure: + """ + A failed ``request.form()`` parse (e.g. multipart with missing boundary) + must surface as a 400, not silently return ``{}`` — otherwise the + auth-time pre-read sees an empty body while a later raw-body re-read + sees the original payload, defeating every banned-param check. + """ + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "raised_exception", + [ + ValueError("Missing boundary in multipart."), + AssertionError("malformed chunk"), + RuntimeError("form parser exploded"), + ], + ) + async def test_form_parse_failure_raises_400(self, raised_exception): + mock_request = MagicMock() + mock_request.form = AsyncMock(side_effect=raised_exception) + mock_request.headers = {"content-type": "multipart/form-data"} + mock_request.scope = {} + + with pytest.raises(ProxyException) as exc_info: + await _read_request_body(mock_request) + assert str(exc_info.value.code) == "400" + + +class TestGetRequestBody: + @pytest.mark.asyncio + async def test_json_with_charset_param_parses_as_json(self): + payload = {"k": "v"} + mock_request = MagicMock() + mock_request.method = "POST" + mock_request.body = AsyncMock(return_value=orjson.dumps(payload)) + mock_request.headers = {"content-type": "application/json; charset=utf-8"} + mock_request.scope = {} + + result = await get_request_body(mock_request) + assert result == payload + + @pytest.mark.asyncio + async def test_form_post_routes_to_form_data(self): + mock_request = MagicMock() + mock_request.method = "POST" + mock_request.headers = {"content-type": "multipart/form-data; boundary=x"} + mock_request.form = AsyncMock(return_value={"k": "v"}) + mock_request.scope = {} + + result = await get_request_body(mock_request) + assert result == {"k": "v"} + + @pytest.mark.asyncio + async def test_substring_match_no_longer_accepted(self): + mock_request = MagicMock() + mock_request.method = "POST" + mock_request.headers = {"content-type": "application/form-json"} + mock_request.scope = {} + + with pytest.raises(ValueError, match="Unsupported content type"): + await get_request_body(mock_request) + + @pytest.mark.asyncio + async def test_non_post_returns_empty(self): + mock_request = MagicMock() + mock_request.method = "GET" + assert await get_request_body(mock_request) == {} diff --git a/tests/test_litellm/proxy/guardrails/test_mcp_jwt_signer.py b/tests/test_litellm/proxy/guardrails/test_mcp_jwt_signer.py index b17b3270787..cb2276ab39d 100644 --- a/tests/test_litellm/proxy/guardrails/test_mcp_jwt_signer.py +++ b/tests/test_litellm/proxy/guardrails/test_mcp_jwt_signer.py @@ -219,7 +219,7 @@ def test_build_claims_scope_with_tool(): def test_build_claims_scope_without_tool(): - """_build_claims() includes mcp:tools/list when no specific tool is called.""" + """_build_claims() emits only mcp:tools/list when no specific tool is called.""" signer = _make_signer() user_dict = _make_user_api_key_dict() data: Dict[str, Any] = {} @@ -227,10 +227,11 @@ def test_build_claims_scope_without_tool(): claims = signer._build_claims(user_dict, data) scopes = set(claims["scope"].split()) - assert "mcp:tools/call" in scopes assert "mcp:tools/list" in scopes + # List-only JWTs must NOT carry mcp:tools/call — least-privilege + assert "mcp:tools/call" not in scopes # No per-tool call scope when no tool name was given - assert not any(s.endswith(":call") and s != "mcp:tools/call" for s in scopes) + assert not any(s.endswith(":call") for s in scopes) def test_build_claims_act_fallback_to_litellm_proxy(): @@ -338,7 +339,7 @@ async def test_hook_skips_non_mcp_call_types(): user_dict = _make_user_api_key_dict() data = {"messages": [{"role": "user", "content": "hello"}]} - for call_type in ("completion", "acompletion", "embedding", "list_mcp_tools"): + for call_type in ("completion", "acompletion", "embedding"): original_data = {**data} result = await signer.async_pre_call_hook( user_api_key_dict=user_dict, @@ -351,6 +352,33 @@ async def test_hook_skips_non_mcp_call_types(): ), f"extra_headers should not be set for {call_type}" +@pytest.mark.asyncio +async def test_hook_signs_list_mcp_tools(): + """async_pre_call_hook() signs JWT for list_mcp_tools with list scope.""" + signer = _make_signer( + issuer="https://litellm.example.com", audience="mcp", ttl_seconds=300 + ) + user_dict = _make_user_api_key_dict(user_id="alice", team_id="backend") + data = {"mcp_tool_name": "should_be_cleared"} + + result = await signer.async_pre_call_hook( + user_api_key_dict=user_dict, + cache=MagicMock(), + data=data, + call_type="list_mcp_tools", + ) + + assert isinstance(result, dict) + assert "extra_headers" in result + assert result["extra_headers"]["Authorization"].startswith("Bearer ") + token = result["extra_headers"]["Authorization"].removeprefix("Bearer ") + decoded = _decode_unverified(token) + scopes = set(decoded["scope"].split()) + assert "mcp:tools/list" in scopes + # List-only JWTs must NOT carry mcp:tools/call — least-privilege + assert "mcp:tools/call" not in scopes + + @pytest.mark.asyncio async def test_signed_token_is_verifiable(): """The JWT injected by the hook can be verified against the JWKS public key.""" @@ -1128,3 +1156,116 @@ async def test_hook_raises_401_when_jwt_verification_fails(): ) assert exc_info.value.status_code == 401 + + +# --- _build_scope branches: call_mcp_tool with empty tool name, list_mcp_tools --- + + +def test_build_scope_call_type_call_mcp_tool_without_tool_name(): + """call_mcp_tool with empty tool name emits a generic mcp:tools/call only.""" + signer = _make_signer() + scope = signer._build_scope("", call_type="call_mcp_tool") + scopes = set(scope.split()) + assert scopes == {"mcp:tools/call"} + + +def test_build_scope_call_type_list_mcp_tools_only_list(): + """list_mcp_tools (no tool) emits only mcp:tools/list, never tools/call.""" + signer = _make_signer() + scope = signer._build_scope("", call_type="list_mcp_tools") + scopes = set(scope.split()) + assert scopes == {"mcp:tools/list"} + + +def test_build_scope_default_is_list_only_when_no_call_type(): + """No call_type and no tool falls through to tools/list (least-privilege default).""" + signer = _make_signer() + scope = signer._build_scope("") + scopes = set(scope.split()) + assert "mcp:tools/list" in scopes + assert "mcp:tools/call" not in scopes + + +# --- inject_mcp_jwt_headers_for_upstream --- + + +@pytest.mark.asyncio +async def test_inject_mcp_jwt_returns_unchanged_when_signer_not_configured(): + """No signer configured -> return a fresh copy of extra_headers untouched.""" + import litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer as mod + from litellm.proxy._types import UserAPIKeyAuth + + mod._mcp_jwt_signer_instance = None + headers = {"X-Trace-Id": "abc"} + user_dict = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + + result = await mod.inject_mcp_jwt_headers_for_upstream( + user_api_key_dict=user_dict, + extra_headers=headers, + ) + assert result == headers + assert result is not headers # must be a copy + + +@pytest.mark.asyncio +async def test_inject_mcp_jwt_returns_unchanged_when_user_dict_none(): + """No user_api_key_dict -> short-circuit without invoking the signer.""" + from litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer import ( + inject_mcp_jwt_headers_for_upstream, + ) + + _make_signer() # ensure instance is created + result = await inject_mcp_jwt_headers_for_upstream( + user_api_key_dict=None, + extra_headers={"X-Trace-Id": "abc"}, + ) + assert result == {"X-Trace-Id": "abc"} + + +@pytest.mark.asyncio +async def test_inject_mcp_jwt_signs_for_list_tools_path(): + """When for_list_tools=True, signer is invoked with list_mcp_tools call_type.""" + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer import ( + inject_mcp_jwt_headers_for_upstream, + ) + + _make_signer(issuer="https://litellm.example.com", audience="mcp", ttl_seconds=300) + user_dict = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + + result = await inject_mcp_jwt_headers_for_upstream( + user_api_key_dict=user_dict, + extra_headers={"X-Trace": "1"}, + raw_headers={"Authorization": "Bearer incoming.opaque.token"}, + for_list_tools=True, + ) + assert result["X-Trace"] == "1" + assert result["Authorization"].startswith("Bearer ") + token = result["Authorization"].removeprefix("Bearer ") + decoded = _decode_unverified(token) + scopes = set(decoded["scope"].split()) + assert scopes == {"mcp:tools/list"} + + +@pytest.mark.asyncio +async def test_inject_mcp_jwt_signs_for_tool_call_path(): + """for_list_tools=False with a tool name signs a call_mcp_tool JWT.""" + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer import ( + inject_mcp_jwt_headers_for_upstream, + ) + + _make_signer(issuer="https://litellm.example.com", audience="mcp", ttl_seconds=300) + user_dict = UserAPIKeyAuth(api_key="sk-test", user_id="alice") + + result = await inject_mcp_jwt_headers_for_upstream( + user_api_key_dict=user_dict, + for_list_tools=False, + mcp_tool_name="search_web", + ) + assert result["Authorization"].startswith("Bearer ") + token = result["Authorization"].removeprefix("Bearer ") + decoded = _decode_unverified(token) + scopes = set(decoded["scope"].split()) + assert "mcp:tools/call" in scopes + assert "mcp:tools/search_web:call" in scopes diff --git a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py index 83317157847..23216542f35 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py @@ -2218,6 +2218,7 @@ class TestCLIKeyRegenerationFlow: # Mock request mock_request = MagicMock(spec=Request) + mock_request.base_url = "http://internal-proxy.local/" # Test data session_key = "cli-session-4567890" @@ -2242,11 +2243,14 @@ class TestCLIKeyRegenerationFlow: "user_code_verified": False, "session_data": None, } - mock_request.url_for.return_value = ( - "https://test.example.com/sso/cli/complete/cli-session-4567890" - ) - with ( + patch.dict( + os.environ, + { + "PROXY_BASE_URL": "https://test.example.com", + "SERVER_ROOT_PATH": "", + }, + ), patch( "litellm.proxy.management_endpoints.ui_sso.get_user_info_from_db", return_value=mock_user_info, @@ -2290,6 +2294,10 @@ class TestCLIKeyRegenerationFlow: assert result.status_code == 200 # Verify response contains success message (response is HTML) assert result.body is not None + assert ( + 'action="https://test.example.com/sso/cli/complete/cli-session-4567890"' + in result.body.decode() + ) @pytest.mark.asyncio async def test_cli_poll_key_returns_teams_for_selection(self): diff --git a/tests/test_litellm/proxy/test_proxy_cli.py b/tests/test_litellm/proxy/test_proxy_cli.py index 46a55fc7468..2aacb0299e7 100644 --- a/tests/test_litellm/proxy/test_proxy_cli.py +++ b/tests/test_litellm/proxy/test_proxy_cli.py @@ -483,6 +483,136 @@ class TestProxyInitializationHelpers: assert appended_params["connection_limit"] == 5 assert appended_params["pool_timeout"] == expected_timeout + def test_build_db_connection_url_params_defaults(self): + from litellm.proxy.proxy_cli import _build_db_connection_url_params + + params = _build_db_connection_url_params(connection_limit=10, pool_timeout=60) + assert params == {"connection_limit": 10, "pool_timeout": 60} + + def test_build_db_connection_url_params_omits_none_timeouts(self): + from litellm.proxy.proxy_cli import _build_db_connection_url_params + + params = _build_db_connection_url_params( + connection_limit=10, + pool_timeout=60, + connect_timeout=None, + socket_timeout=None, + ) + assert "connect_timeout" not in params + assert "socket_timeout" not in params + + def test_build_db_connection_url_params_includes_optional_timeouts(self): + from litellm.proxy.proxy_cli import _build_db_connection_url_params + + params = _build_db_connection_url_params( + connection_limit=10, + pool_timeout=60, + connect_timeout=15, + socket_timeout=120, + ) + assert params["connect_timeout"] == 15 + assert params["socket_timeout"] == 120 + + def test_build_db_connection_url_params_extras_override_defaults(self): + from litellm.proxy.proxy_cli import _build_db_connection_url_params + + params = _build_db_connection_url_params( + connection_limit=10, + pool_timeout=60, + extra_params={ + "pgbouncer": "true", + "statement_cache_size": 0, + "pool_timeout": 5, + }, + ) + assert params["pgbouncer"] == "true" + assert params["statement_cache_size"] == 0 + assert params["pool_timeout"] == 5 + + @patch("subprocess.run") + @patch("atexit.register") + @patch("litellm.proxy.db.prisma_client.PrismaManager.setup_database") + @patch( + "litellm.proxy.db.prisma_client.should_update_prisma_schema", return_value=False + ) + def test_db_connection_extra_params_forwarded_to_url( + self, + mock_should_update, + mock_setup_db, + mock_atexit_register, + mock_subprocess_run, + ): + from click.testing import CliRunner + + from litellm.proxy.proxy_cli import run_server + + runner = CliRunner() + mock_subprocess_run.return_value = MagicMock(returncode=0) + + mock_proxy_module = MagicMock( + app=MagicMock(), + ProxyConfig=MagicMock(), + KeyManagementSettings=MagicMock(), + save_worker_config=MagicMock(), + ) + mock_proxy_module.ProxyConfig.return_value.get_config = AsyncMock( + return_value={ + "general_settings": { + "database_url": "postgresql://test:test@localhost:5432/test", + "database_connect_timeout": 15, + "database_socket_timeout": 120, + "database_extra_connection_params": { + "pgbouncer": "true", + "statement_cache_size": 0, + }, + } + } + ) + + clean_env = { + k: v + for k, v in os.environ.items() + if k not in ("DATABASE_URL", "DIRECT_URL") + } + + with ( + patch.dict(os.environ, clean_env, clear=True), + patch.dict( + "sys.modules", + { + "proxy_server": mock_proxy_module, + "litellm.proxy.proxy_server": mock_proxy_module, + }, + ), + patch( + "litellm.proxy.proxy_cli.ProxyInitializationHelpers._get_default_unvicorn_init_args" + ) as mock_get_args, + patch( + "litellm.proxy.proxy_cli.append_query_params", + side_effect=lambda url, params: str(url), + ) as mock_append_query_params, + ): + mock_get_args.return_value = { + "app": "litellm.proxy.proxy_server:app", + "host": "localhost", + "port": 8000, + } + + result = runner.invoke( + run_server, + ["--local", "--config", "test-config.yaml", "--skip_server_startup"], + ) + + assert ( + result.exit_code == 0 + ), f"exit_code={result.exit_code}, output={result.output}" + mock_append_query_params.assert_called() + appended_params = mock_append_query_params.call_args.args[1] + assert appended_params["connect_timeout"] == 15 + assert appended_params["socket_timeout"] == 120 + assert appended_params["pgbouncer"] == "true" + assert appended_params["statement_cache_size"] == 0 + @patch("uvicorn.run") @patch("atexit.register") @patch("litellm.proxy.db.prisma_client.PrismaManager.setup_database") diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 6d10d2a6353..ae0996d16d5 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -5708,6 +5708,7 @@ async def test_init_and_increment_spend_counter_reseeds_from_db_on_counter_miss( fake_redis = AsyncMock() fake_redis.async_increment = AsyncMock(side_effect=record_increment) fake_redis.async_get_cache = AsyncMock(return_value=None) # counter missing + fake_redis.async_set_cache = AsyncMock(return_value=True) # SET NX wins counter_cache.redis_cache = fake_redis # Prisma returns spend=42.0 (authoritative) while the stale cached @@ -5744,16 +5745,131 @@ async def test_init_and_increment_spend_counter_reseeds_from_db_on_counter_miss( fake_prisma.db.litellm_teamtable.find_unique.assert_awaited_once_with( where={"team_id": "team-9"} ) - # Two increments keyed on the counter: seed ($42) then request ($1.50). + # Seed uses SET NX with db_spend (42) — cross-pod safe, no INCR of 42. + # Only the per-request delta (1.5) goes through INCRBYFLOAT. + fake_redis.async_set_cache.assert_awaited_once_with( + key="spend:team:team-9", value=42.0, nx=True + ) writes = [(c["key"], c["value"]) for c in recorded_increments] - assert ("spend:team:team-9", 42.0) in writes - assert ("spend:team:team-9", 1.5) in writes + assert writes == [("spend:team:team-9", 1.5)] finally: ps.user_api_key_cache = orig_user ps.spend_counter_cache = orig_counter ps.prisma_client = orig_prisma +@pytest.mark.asyncio +async def test_primary_spend_counter_redis_concurrent_seed_does_not_double_seed(): + """Two pods both observing a missing Redis counter must not both + INCRBYFLOAT the full DB spend. SpendCounterReseed.coalesced uses SET NX + so the loser reads the winner's value; final Redis = db_spend, not + 2 * db_spend. + + The per-counter asyncio.Lock is per-process, so it does NOT coordinate + across pods. We simulate two pods by patching _get_lock to return a + fresh lock per call (each "pod" has its own lock registry in real life). + """ + from litellm.caching.dual_cache import DualCache + from litellm.proxy.db.spend_counter_reseed import SpendCounterReseed + + counter_key = "spend:team:team-concurrent-seed" + redis_store: dict = {} + db_read_count = 0 + set_results: list = [] + get_after_set_count = 0 + set_completed_count = 0 + + async def redis_set_cache(key, value, nx=False, **_): + # Yield BEFORE the membership check so two concurrent callers + # interleave the way real atomic Redis SET NX does: the first + # to resume runs check + write atomically and wins; the second + # resumes after the key exists and loses. Yielding *after* the + # check would let both callers pass the empty-store check before + # either writes, so neither would ever lose. + await asyncio.sleep(0) + if nx and key in redis_store: + set_results.append(False) + return False + redis_store[key] = float(value) + set_results.append(True) + nonlocal set_completed_count + set_completed_count += 1 + return True + + async def redis_get_cache(key): + # Track reads that happen after at least one SET NX has completed + # — those are the loser-path fallback reads we want to verify. + if set_completed_count > 0: + nonlocal get_after_set_count + get_after_set_count += 1 + return redis_store.get(key) + + fake_redis = AsyncMock() + fake_redis.async_get_cache = AsyncMock(side_effect=redis_get_cache) + fake_redis.async_set_cache = AsyncMock(side_effect=redis_set_cache) + + async def slow_find_unique(**_): + nonlocal db_read_count + db_read_count += 1 + # Both pods read DB before either's SET NX lands. + await asyncio.sleep(0) + row = MagicMock() + row.spend = 506.0 + return row + + fake_prisma = MagicMock() + fake_prisma.db.litellm_teamtable.find_unique = AsyncMock( + side_effect=slow_find_unique + ) + + pod_a = DualCache() + pod_a.redis_cache = fake_redis + pod_b = DualCache() + pod_b.redis_cache = fake_redis + + # Each "pod" has its own per-process lock registry. Patch _get_lock to + # always return a fresh lock so the two coalesced calls do not serialize + # via one in-process lock (which is what would happen across pods). + async def fresh_lock(_counter_key): + return asyncio.Lock() + + with patch.object(SpendCounterReseed, "_get_lock", side_effect=fresh_lock): + results = await asyncio.gather( + SpendCounterReseed.coalesced( + prisma_client=fake_prisma, + spend_counter_cache=pod_a, + counter_key=counter_key, + ), + SpendCounterReseed.coalesced( + prisma_client=fake_prisma, + spend_counter_cache=pod_b, + counter_key=counter_key, + ), + ) + + assert all(r == 506.0 for r in results), results + assert redis_store[counter_key] == pytest.approx(506.0), redis_store + # Both pods read the DB and both attempted SET NX; exactly one wrote + # (winner) and one was rejected (loser). + assert db_read_count == 2 + assert fake_redis.async_set_cache.await_count == 2 + nx_writes = [ + call + for call in fake_redis.async_set_cache.await_args_list + if call.kwargs.get("nx") is True + ] + assert len(nx_writes) == 2 + assert sorted(set_results) == [False, True], ( + f"expected exactly one SET NX winner and one loser, got {set_results}" + ) + # Loser path executed: after the winner's SET NX returned True, the + # losing coalesced() call falls back to async_get_cache to read the + # winner's value rather than re-seeding. + assert get_after_set_count >= 1, ( + "loser branch (else: read back winner's value) was never exercised" + ) + + @pytest.mark.asyncio async def test_reseed_spend_from_db_user_and_org_prefixes(): """User and org counters reseed from their own DB tables. @@ -5877,9 +5993,16 @@ async def test_init_spend_counter_redis_clean_miss_skips_stale_in_memory(): redis_store[key] = (redis_store.get(key) or 0.0) + value return redis_store[key] + async def redis_set_cache(key, value, nx=False, **_): + if nx and key in redis_store: + return False + redis_store[key] = float(value) + return True + fake_redis = AsyncMock() fake_redis.async_get_cache = AsyncMock(return_value=None) fake_redis.async_increment = AsyncMock(side_effect=redis_increment) + fake_redis.async_set_cache = AsyncMock(side_effect=redis_set_cache) counter_cache.redis_cache = fake_redis db_row = MagicMock() @@ -5907,6 +6030,7 @@ async def test_init_spend_counter_redis_clean_miss_skips_stale_in_memory(): fake_prisma.db.litellm_teamtable.find_unique.assert_awaited_once_with( where={"team_id": "team-stale-local"} ) + # Seed via SET NX (42) + delta via INCRBYFLOAT (1.5) = 43.5. assert redis_store[counter_key] == pytest.approx(43.5) assert counter_cache.in_memory_cache.get_cache( key=counter_key @@ -6297,14 +6421,14 @@ async def test_get_current_spend_reseeds_from_db_when_counter_missing(): from litellm.proxy.proxy_server import get_current_spend counter_cache = DualCache() - recorded_warms: list = [] + recorded_seeds: list = [] - async def record_increment(key, value, ttl=None, **kwargs): - recorded_warms.append({"key": key, "value": value}) - return value + async def record_set_cache(key, value, nx=False, **kwargs): + recorded_seeds.append({"key": key, "value": value, "nx": nx}) + return True fake_redis = AsyncMock() - fake_redis.async_increment = AsyncMock(side_effect=record_increment) + fake_redis.async_set_cache = AsyncMock(side_effect=record_set_cache) fake_redis.async_get_cache = AsyncMock(return_value=None) counter_cache.redis_cache = fake_redis @@ -6329,9 +6453,9 @@ async def test_get_current_spend_reseeds_from_db_when_counter_missing(): f"expected DB reseed to return 362.0, got {spend} " f"(fallback would have returned 30.0 and caused bypass)" ) - # Counter warmed so subsequent reads are fast - assert ("spend:team_member:user-1:team-1", 362.0) in [ - (w["key"], w["value"]) for w in recorded_warms + # Counter warmed via SET NX so subsequent reads are fast. + assert ("spend:team_member:user-1:team-1", 362.0, True) in [ + (s["key"], s["value"], s["nx"]) for s in recorded_seeds ] assert counter_cache.in_memory_cache.get_cache( key="spend:team_member:user-1:team-1" @@ -6408,8 +6532,15 @@ async def test_get_current_spend_coalesces_concurrent_reseeds(): redis_store[key] = (redis_store.get(key) or 0.0) + value return redis_store[key] + async def redis_set_cache(key, value, nx=False, **_): + if nx and key in redis_store: + return False + redis_store[key] = float(value) + return True + fake_redis.async_get_cache = AsyncMock(side_effect=redis_get) fake_redis.async_increment = AsyncMock(side_effect=redis_increment) + fake_redis.async_set_cache = AsyncMock(side_effect=redis_set_cache) counter_cache.redis_cache = fake_redis fake_prisma = MagicMock() @@ -6516,9 +6647,16 @@ async def test_concurrent_read_and_write_paths_share_one_db_query(): redis_store[key] = (redis_store.get(key) or 0.0) + value return redis_store[key] + async def redis_set_cache(key, value, nx=False, **_): + if nx and key in redis_store: + return False + redis_store[key] = float(value) + return True + fake_redis = AsyncMock() fake_redis.async_get_cache = AsyncMock(side_effect=redis_get) fake_redis.async_increment = AsyncMock(side_effect=redis_increment) + fake_redis.async_set_cache = AsyncMock(side_effect=redis_set_cache) counter_cache.redis_cache = fake_redis fake_prisma = MagicMock() @@ -6621,9 +6759,16 @@ async def test_reseed_warms_cache_even_on_zero_db_spend(): redis_store[key] = (redis_store.get(key) or 0.0) + value return redis_store[key] + async def redis_set_cache(key, value, nx=False, **_): + if nx and key in redis_store: + return False + redis_store[key] = float(value) + return True + fake_redis = AsyncMock() fake_redis.async_get_cache = AsyncMock(side_effect=redis_get) fake_redis.async_increment = AsyncMock(side_effect=redis_increment) + fake_redis.async_set_cache = AsyncMock(side_effect=redis_set_cache) counter_cache.redis_cache = fake_redis db_call_count = 0 diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 18ab8a2a07a..00902890da3 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -2390,3 +2390,34 @@ def test_custom_pricing_without_cache_keys_preserves_legacy_behavior(): expected = 1000 * 0.0000025 + 100 * 0.000015 assert cost == pytest.approx(expected) + + +def test_openrouter_gemini_3_1_flash_lite_stable_pricing(): + """ + Test that openrouter/google/gemini-3.1-flash-lite (stable, no -preview suffix) + has a pricing entry. + + Google promoted gemini-3.1-flash-lite to GA on 2026-05-07. PR #27933 added the + stable pricing for the bare, gemini/, and vertex_ai/ prefixes but missed the + openrouter/google/ variant — every other Gemini family in the file has an + openrouter/google/ sibling (2.0-flash-001, 2.5-flash, 2.5-pro, 3-flash-preview, + 3-pro-preview, 3.1-flash-lite-preview, 3.1-pro-preview), so the gap is a + consistency issue, not a design choice. Same shape as the preview-variant gap + fixed in PR #25610. + + Pricing matches the existing -preview entry one-for-one (input $0.25/M, output + $1.50/M, cache-read $0.025/M) — Google did not change costs at the GA cutover. + """ + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + model_name = "openrouter/google/gemini-3.1-flash-lite" + model_info = litellm.model_cost.get(model_name) + + assert model_info is not None, f"Missing model pricing entry: {model_name}" + assert model_info["litellm_provider"] == "openrouter" + assert model_info["input_cost_per_token"] == 2.5e-07 + assert model_info["output_cost_per_token"] == 1.5e-06 + assert model_info["cache_read_input_token_cost"] == 2.5e-08 + assert model_info["max_input_tokens"] == 1048576 + assert model_info["max_output_tokens"] == 65536 diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index d8be527689e..5e636b86ed6 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -1741,6 +1741,362 @@ async def test_acompletion_streaming_iterator_pre_first_chunk_skips_continuation assert fallback_kwargs["messages"] == messages +# --------------------------------------------------------------------------- +# Shared helpers for the _aresponses_streaming_iterator test suite. +# --------------------------------------------------------------------------- +def _make_responses_iterator( + *, + chunks=(), + error=None, + bridge=False, + model="gpt-4", + hidden_params=None, + chat_chunks=None, +): + """Build a minimal mock Responses-API streaming iterator. + + Bypasses BaseResponsesAPIStreamingIterator.__init__ but mirrors every + attribute production code reads. Yields *chunks*, then raises *error* + (or StopAsyncIteration). Set bridge=True to inherit from + LiteLLMCompletionStreamingIterator so the wrapper's bridge-path + isinstance check (used by usage extraction) matches. + """ + from litellm.responses.litellm_completion_transformation.streaming_iterator import ( + LiteLLMCompletionStreamingIterator, + ) + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + + base = ( + LiteLLMCompletionStreamingIterator + if bridge + else BaseResponsesAPIStreamingIterator + ) + + class _Iter(base): + def __init__(self): + self._chunks = list(chunks) + self._idx = 0 + self._hidden_params = hidden_params or {} + self.model = model + self.custom_llm_provider = "anthropic" + self.logging_obj = MagicMock() + self.litellm_metadata = None + self.responses_api_provider_config = None + self.finished = False + self.completed_response = None + self.response = None + self.start_time = None + self.request_data = {} + self.call_type = None + if chat_chunks is not None: + self.collected_chat_completion_chunks = chat_chunks + + def __aiter__(self): + return self + + async def __anext__(self): + if self._idx < len(self._chunks): + self._idx += 1 + return self._chunks[self._idx - 1] + if error is not None: + raise error + raise StopAsyncIteration + + return _Iter() + + +class _AsyncList: + """Generic async iterator over a list — used as the fallback response.""" + + def __init__(self, items=()): + self._items = list(items) + self._idx = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self._idx >= len(self._items): + raise StopAsyncIteration + item = self._items[self._idx] + self._idx += 1 + return item + + +def _make_router_with_fallback(primary="gpt-4", secondary="gpt-3.5-turbo"): + return litellm.Router( + model_list=[ + { + "model_name": primary, + "litellm_params": {"model": primary, "api_key": "k1"}, + }, + { + "model_name": secondary, + "litellm_params": {"model": secondary, "api_key": "k2"}, + }, + ], + fallbacks=[{primary: [secondary]}], + ) + + +@pytest.mark.asyncio +async def test_aresponses_streaming_iterator_fallback(): + """Catches MidStreamFallbackError, re-enters the fallback chain via + async_function_with_fallbacks_common_utils with the per-attempt helper + and original_generic_function preserved. Mirrors + test_acompletion_streaming_iterator for the aresponses path.""" + from litellm.exceptions import MidStreamFallbackError + from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + ) + + router = _make_router_with_fallback( + "anthropic/claude-sonnet-4-6", "vertex_ai/claude-sonnet-4-6" + ) + src = _make_responses_iterator( + chunks=[MagicMock(type="response.created")], + error=MidStreamFallbackError( + message="anthropic socket timeout", + model="anthropic/claude-sonnet-4-6", + llm_provider="anthropic", + is_pre_first_chunk=False, + generated_content="", + ), + model="anthropic/claude-sonnet-4-6", + hidden_params={"model_id": "src-deployment-1"}, + ) + fallback_chunks = [ + MagicMock(type="response.output_text.delta"), + MagicMock(type="response.completed"), + ] + + with patch.object( + router, + "async_function_with_fallbacks_common_utils", + return_value=_AsyncList(fallback_chunks), + ) as mock_fallback_utils: + wrapped = await router._aresponses_streaming_iterator( + response=src, + initial_kwargs={ + "model": "anthropic/claude-sonnet-4-6", + "stream": True, + "input": "Hi", + "original_generic_function": litellm.aresponses, + }, + ) + assert isinstance(wrapped, BaseResponsesAPIStreamingIterator) + assert wrapped._hidden_params.get("model_id") == "src-deployment-1" + collected = [c async for c in wrapped] + + assert len(collected) == 3 # 1 primary chunk + 2 fallback chunks + call_kwargs = mock_fallback_utils.call_args.kwargs + fbk = call_kwargs["kwargs"] + # Bound methods compare equal when they share the same instance + __func__. + assert fbk["original_function"] == router._ageneric_api_call_with_fallbacks_helper + assert fbk["original_generic_function"] is litellm.aresponses + assert call_kwargs["model_group"] == "anthropic/claude-sonnet-4-6" + assert call_kwargs["disable_fallbacks"] is False + + +@pytest.mark.asyncio +async def test_aresponses_streaming_iterator_writes_litellm_metadata_on_fallback(): + """Regression: model_group must land under "litellm_metadata" (the key + litellm.aresponses reads), not the default "metadata".""" + from litellm.exceptions import MidStreamFallbackError + + router = _make_router_with_fallback() + src = _make_responses_iterator( + error=MidStreamFallbackError( + message="boom", + model="gpt-4", + llm_provider="anthropic", + is_pre_first_chunk=True, + generated_content="", + ) + ) + + with patch.object( + router, + "async_function_with_fallbacks_common_utils", + return_value=_AsyncList(), + ) as mock_fallback_utils: + wrapped = await router._aresponses_streaming_iterator( + response=src, + initial_kwargs={ + "model": "gpt-4", + "stream": True, + "input": "Hello", + "original_generic_function": litellm.aresponses, + }, + ) + async for _ in wrapped: + pass + + fbk = mock_fallback_utils.call_args.kwargs["kwargs"] + assert "litellm_metadata" in fbk, "wrong metadata_variable_name" + assert fbk["litellm_metadata"]["model_group"] == "gpt-4" + assert "model_group" not in fbk.get( + "metadata", {} + ), "model_group leaked into 'metadata' instead of 'litellm_metadata'" + + +@pytest.mark.asyncio +async def test_aresponses_streaming_iterator_pre_first_chunk_skips_continuation(): + """Pre-first-chunk error: original input is preserved unchanged.""" + from litellm.exceptions import MidStreamFallbackError + + router = _make_router_with_fallback() + src = _make_responses_iterator( + error=MidStreamFallbackError( + message="socket timeout before first chunk", + model="gpt-4", + llm_provider="anthropic", + is_pre_first_chunk=True, + generated_content="", + ) + ) + + with patch.object( + router, + "async_function_with_fallbacks_common_utils", + return_value=_AsyncList(), + ) as mock_fallback_utils: + wrapped = await router._aresponses_streaming_iterator( + response=src, + initial_kwargs={ + "model": "gpt-4", + "stream": True, + "input": "Hello", + "original_generic_function": litellm.aresponses, + }, + ) + async for _ in wrapped: + pass + + fbk = mock_fallback_utils.call_args.kwargs["kwargs"] + assert fbk["input"] == "Hello" # original input, no continuation messages + + +@pytest.mark.asyncio +async def test_aresponses_streaming_iterator_partial_content_injects_continuation(): + """Mid-stream error: input is rewritten to include user prompt + + developer instruction + prior assistant message with partial output.""" + from litellm.exceptions import MidStreamFallbackError + + router = _make_router_with_fallback() + src = _make_responses_iterator( + chunks=[MagicMock(type="response.output_text.delta")], + error=MidStreamFallbackError( + message="socket reset mid-stream", + model="gpt-4", + llm_provider="anthropic", + is_pre_first_chunk=False, + generated_content="The capital of France is", + ), + ) + + with patch.object( + router, + "async_function_with_fallbacks_common_utils", + return_value=_AsyncList(), + ) as mock_fallback_utils: + wrapped = await router._aresponses_streaming_iterator( + response=src, + initial_kwargs={ + "model": "gpt-4", + "stream": True, + "input": "What's the capital of France?", + "original_generic_function": litellm.aresponses, + }, + ) + async for _ in wrapped: + pass + + new_input = mock_fallback_utils.call_args.kwargs["kwargs"]["input"] + assert isinstance(new_input, list) + assert new_input[0]["role"] == "user" + assert new_input[0]["content"][0]["text"] == "What's the capital of France?" + assert new_input[1]["role"] == "developer" + assert "do not repeat" in new_input[1]["content"][0]["text"].lower() + assert new_input[2]["role"] == "assistant" + assert new_input[2]["content"][0]["type"] == "output_text" + assert new_input[2]["content"][0]["text"] == "The capital of France is" + + +@pytest.mark.asyncio +async def test_aresponses_streaming_iterator_combines_partial_usage(): + """Partial usage from the bridge path is normalized to ResponseAPIUsage + and summed onto the fallback's response.completed event — no token-name + split, clean ResponseAPIUsage on output.""" + from types import SimpleNamespace + + from litellm.exceptions import MidStreamFallbackError + from litellm.types.llms.openai import ( + ResponseAPIUsage, + ResponseCompletedEvent, + ResponsesAPIResponse, + ResponsesAPIStreamEvents, + ) + + router = _make_router_with_fallback() + src = _make_responses_iterator( + bridge=True, + chat_chunks=[MagicMock()], + chunks=[MagicMock(type="response.output_text.delta")], + error=MidStreamFallbackError( + message="boom", + model="gpt-4", + llm_provider="anthropic", + is_pre_first_chunk=False, + generated_content="hello", + ), + ) + + fallback_response_object = ResponsesAPIResponse( + id="resp_test", created_at=0, model="gpt-4", object="response", output=[] + ) + fallback_response_object.usage = ResponseAPIUsage( + input_tokens=20, output_tokens=15, total_tokens=35 + ) + fallback_event = ResponseCompletedEvent( + type=ResponsesAPIStreamEvents.RESPONSE_COMPLETED, + response=fallback_response_object, + ) + + with ( + patch( + "litellm.main.stream_chunk_builder", + return_value=SimpleNamespace( + usage=SimpleNamespace(prompt_tokens=10, completion_tokens=4) + ), + ), + patch.object( + router, + "async_function_with_fallbacks_common_utils", + return_value=_AsyncList([fallback_event]), + ), + ): + wrapped = await router._aresponses_streaming_iterator( + response=src, + initial_kwargs={ + "model": "gpt-4", + "stream": True, + "input": "hi", + "original_generic_function": litellm.aresponses, + }, + ) + async for _ in wrapped: + pass + + merged = fallback_response_object.usage + assert isinstance(merged, ResponseAPIUsage) + assert merged.input_tokens == 30 # 10 (translated from prompt_tokens) + 20 + assert merged.output_tokens == 19 # 4 (translated from completion_tokens) + 15 + assert merged.total_tokens == 49 + + @pytest.mark.asyncio async def test_async_function_with_fallbacks_common_utils(): """Test the async_function_with_fallbacks_common_utils method""" @@ -3863,7 +4219,15 @@ def test_is_deployment_blocked_static_helper_reflects_blocked_flag(): # No model_info on deployment object → treated as not blocked assert litellm.Router._is_deployment_blocked(object()) is False missing_blocked = types.SimpleNamespace() - assert litellm.Router._is_deployment_blocked(types.SimpleNamespace(model_info=missing_blocked)) is False - assert litellm.Router._is_deployment_blocked( - types.SimpleNamespace(model_info=types.SimpleNamespace(blocked=True)) - ) is True + assert ( + litellm.Router._is_deployment_blocked( + types.SimpleNamespace(model_info=missing_blocked) + ) + is False + ) + assert ( + litellm.Router._is_deployment_blocked( + types.SimpleNamespace(model_info=types.SimpleNamespace(blocked=True)) + ) + is True + ) diff --git a/tests/test_spend_logs.py b/tests/test_spend_logs.py index 8aec1d5cc60..fcd2bbf4a1d 100644 --- a/tests/test_spend_logs.py +++ b/tests/test_spend_logs.py @@ -100,6 +100,9 @@ async def get_spend_logs(session, request_id=None, api_key=None): return await response.json() +@pytest.mark.skip( + reason="Flaky in CI: /spend/logs?request_id=... returns 500 even after a 20s wait for the spend log to be written. Spend-log accuracy is covered by tests/test_litellm/proxy/spend_tracking/ and the proxy_spend_accuracy_tests CircleCI job." +) @pytest.mark.asyncio async def test_spend_logs(): """ diff --git a/tests/test_team_members.py b/tests/test_team_members.py index a3d64eae803..415b3f07fc9 100644 --- a/tests/test_team_members.py +++ b/tests/test_team_members.py @@ -136,6 +136,9 @@ def test_add_single_member(api_client, new_team): ), f"Team size did not increase by 1 (was {initial_size}, now {updated_size})" +@pytest.mark.skip( + reason="Flaky in CI: /team/info?team_id=... intermittently returns 404/400 mid-loop after add_team_member calls. Single-member coverage in test_add_single_member is sufficient; team-member CRUD is also covered by tests/test_litellm/proxy/management_endpoints/." +) def test_add_multiple_members(api_client, new_team): """Test adding multiple members to a new team""" # Get initial team size diff --git a/ui/litellm-dashboard/e2e_tests/playwright.config.ts b/ui/litellm-dashboard/e2e_tests/playwright.config.ts index ec4d3a6ddb0..6964fe52a14 100644 --- a/ui/litellm-dashboard/e2e_tests/playwright.config.ts +++ b/ui/litellm-dashboard/e2e_tests/playwright.config.ts @@ -28,6 +28,11 @@ export default defineConfig({ /* Action timeout for clicks, fills, waitForSelector, etc. */ actionTimeout: 15 * 1000, navigationTimeout: 30 * 1000, + + /* Slow down actions when SLOWMO= is set, useful for headed local debugging */ + launchOptions: { + slowMo: process.env.SLOWMO ? (parseInt(process.env.SLOWMO, 10) || 0) : 0, + }, }, /* Configure projects for major browsers */ diff --git a/ui/litellm-dashboard/e2e_tests/run_e2e.sh b/ui/litellm-dashboard/e2e_tests/run_e2e.sh index 4e3a47edfbd..f8f570cda89 100755 --- a/ui/litellm-dashboard/e2e_tests/run_e2e.sh +++ b/ui/litellm-dashboard/e2e_tests/run_e2e.sh @@ -15,7 +15,7 @@ set -euo pipefail # In CI (CI=true), expects: # - PostgreSQL already running on 127.0.0.1:5432 # - DATABASE_URL already set -# - Python/Poetry already installed +# - Python/uv already installed # - Node.js/npx already available # ================================================================ @@ -48,7 +48,7 @@ cleanup() { trap cleanup EXIT INT TERM # --- Pre-flight checks --- -for cmd in python3 npx poetry; do +for cmd in python3 npx uv; do command -v "$cmd" >/dev/null 2>&1 || { echo "Error: $cmd not found."; exit 1; } done @@ -117,19 +117,15 @@ echo "UI build copied and restructured" # --- Python environment --- echo "=== Setting up Python environment ===" cd "$REPO_ROOT" -if ! poetry run python3 -c "import prisma" 2>/dev/null; then - echo "Installing Python dependencies (first run)..." - poetry install --with dev,proxy-dev --extras "proxy" --quiet - poetry run pip install nodejs-wheel-binaries 2>/dev/null || true - poetry run prisma generate --schema litellm/proxy/schema.prisma -fi +uv sync --group dev --group proxy-dev --extra proxy --frozen --quiet +uv run --no-sync python -m prisma generate --schema litellm/proxy/schema.prisma echo "=== Pushing Prisma schema to database ===" -poetry run prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss +uv run --no-sync python -m prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss # --- Mock LLM server --- echo "=== Starting mock LLM server ===" -poetry run python3 "$SCRIPT_DIR/fixtures/mock_llm_server/server.py" & +uv run --no-sync python "$SCRIPT_DIR/fixtures/mock_llm_server/server.py" & MOCK_PID=$! for i in $(seq 1 15); do @@ -140,7 +136,7 @@ done # --- LiteLLM proxy --- echo "=== Starting LiteLLM proxy ===" cd "$REPO_ROOT" -poetry run python3 -m litellm.proxy.proxy_cli \ +uv run --no-sync python -m litellm.proxy.proxy_cli \ --config "$SCRIPT_DIR/fixtures/config.yml" \ --port 4000 & PROXY_PID=$! diff --git a/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts index 14ceb1a4a6b..1e44d9a25a0 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts @@ -126,4 +126,84 @@ test.describe("Proxy Admin - Keys", () => { await expect(page.getByText(E2E_INTERNAL_USER_KEY_ALIAS)).toBeVisible({ timeout: 10_000 }); }); + + test("Create a key with All Proxy Models (no team)", async ({ page }) => { + await navigateToPage(page, Page.ApiKeys); + await dismissFeedbackPopup(page); + + await page.getByRole("button", { name: /Create New Key/i }).click(); + + await expect(page.getByText("Key Ownership")).toBeVisible({ timeout: 10_000 }); + + const keyName = `e2e-admin-allproxy-${Date.now()}`; + await page.getByTestId("base-input").fill(keyName); + + // No team selection — leave team dropdown empty so the key is owned by the admin user + + // Select models — open the multi-select and pick the all-models meta-option. + // The Create Key modal labels this "All Team Models" even when no team is selected + // (see src/components/organisms/create_key_button.tsx:944), unlike the team/user + // settings screens which use "All Proxy Models". + await page.locator(".ant-select-selection-overflow").click(); + await page.locator(".ant-select-dropdown:visible").getByText("All Team Models").click(); + await page.keyboard.press("Escape"); + + await page.getByRole("button", { name: "Create Key", exact: true }).click(); + + await expect(page.getByText("Save your Key")).toBeVisible({ timeout: 10_000 }); + await page.keyboard.press("Escape"); + + await expect(page.getByText(keyName)).toBeVisible({ timeout: 10_000 }); + }); + + test("Create a key with a specific proxy model (no team)", async ({ page }) => { + await navigateToPage(page, Page.ApiKeys); + await dismissFeedbackPopup(page); + + await page.getByRole("button", { name: /Create New Key/i }).click(); + + await expect(page.getByText("Key Ownership")).toBeVisible({ timeout: 10_000 }); + + const keyName = `e2e-admin-specific-${Date.now()}`; + await page.getByTestId("base-input").fill(keyName); + + // Open the model multi-select and pick a single specific model. Use + // getByRole("option", ...) to avoid the strict-mode collision between + // the option container and its inner text node. + const modelName = "fake-openai-gpt-4"; + await page.locator(".ant-select-selection-overflow").click(); + const option = page.locator(".ant-select-dropdown:visible").getByRole("option", { name: modelName, exact: true }); + await option.waitFor({ state: "attached" }); + // Dispatch the click via the DOM — antd's dropdown can render the option + // off-viewport during the open animation, which trips Playwright's + // visibility/stability checks. The click handler fires regardless. + await option.evaluate((el: HTMLElement) => el.click()); + await page.keyboard.press("Escape"); + + await page.getByRole("button", { name: "Create Key", exact: true }).click(); + + await expect(page.getByText("Save your Key")).toBeVisible({ timeout: 10_000 }); + + // Grab the new key from the success modal (rendered inside a
) and
+    // verify it can call /chat/completions for the model it was scoped to.
+    // The mock LLM server (fixtures/mock_llm_server/server.py) replies with
+    // a fixed "This is a mock response." body.
+    const apiKey = (await page.locator(".ant-modal:visible pre").innerText()).trim();
+    expect(apiKey).toMatch(/^sk-/);
+
+    const response = await page.request.post("/chat/completions", {
+      headers: { Authorization: `Bearer ${apiKey}` },
+      data: {
+        model: modelName,
+        messages: [{ role: "user", content: "ping" }],
+      },
+    });
+    expect(response.status()).toBe(200);
+    const body = await response.json();
+    expect(body.choices?.[0]?.message?.content).toBe("This is a mock response.");
+
+    await page.keyboard.press("Escape");
+
+    await expect(page.getByText(keyName)).toBeVisible({ timeout: 10_000 });
+  });
 });
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
index 5431c196883..2626ace86d5 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
@@ -7,7 +7,7 @@ import { columns } from "@/components/molecules/models/columns";
 import { getDisplayModelName } from "@/components/view_model/model_name_display";
 import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
 import NotificationsManager from "@/components/molecules/notifications_manager";
-import { modelDeleteCall } from "@/components/networking";
+import { modelDeleteCall, modelPatchUpdateCall } from "@/components/networking";
 import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons";
 import { PaginationState, SortingState } from "@tanstack/react-table";
 import { useQueryClient } from "@tanstack/react-query";
@@ -220,6 +220,25 @@ const AllModelsTab = ({
     }
   };
 
+  const [pausingModelId, setPausingModelId] = useState(null);
+
+  const handleTogglePause = async (modelId: string, blocked: boolean) => {
+    if (!accessToken) return;
+    try {
+      setPausingModelId(modelId);
+      await modelPatchUpdateCall(accessToken, { blocked }, modelId);
+      NotificationsManager.success(blocked ? "Model paused" : "Model resumed");
+      // invalidateQueries already schedules a refetch for active observers
+      // on this key — no need to also call refetchModels() (would double-fetch).
+      queryClient.invalidateQueries({ queryKey: ["models", "list"] });
+    } catch (error) {
+      console.error("Error toggling model pause state:", error);
+      NotificationsManager.fromBackend(error);
+    } finally {
+      setPausingModelId(null);
+    }
+  };
+
   return (
     
       
@@ -536,6 +555,8 @@ const AllModelsTab = ({
                 expandedRows,
                 setExpandedRows,
                 setDeleteModalModelId,
+                handleTogglePause,
+                pausingModelId,
               )}
               data={filteredData}
               isLoading={isLoadingModelsInfo}
diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx
index b9305e4723a..da00ad911b0 100644
--- a/ui/litellm-dashboard/src/components/OldTeams.tsx
+++ b/ui/litellm-dashboard/src/components/OldTeams.tsx
@@ -45,6 +45,7 @@ import OrganizationDropdown from "./common_components/OrganizationDropdown";
 import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
 import { teamListCall as v2TeamListCall, type TeamsResponse } from "@/app/(dashboard)/hooks/teams/useTeams";
 import AccessGroupSelector from "./common_components/AccessGroupSelector";
+import PassThroughRoutesSelector from "./common_components/PassThroughRoutesSelector";
 import AgentSelector from "./agent_management/AgentSelector";
 import ModelAliasManager from "./common_components/ModelAliasManager";
 import PremiumLoggingSettings from "./common_components/PremiumLoggingSettings";
@@ -1446,6 +1447,30 @@ const Teams: React.FC = ({
                           placeholder="Select vector stores (optional)"
                         />
                       
+                      
+                        
+                           form.setFieldValue("allowed_passthrough_routes", values)}
+                            value={form.getFieldValue("allowed_passthrough_routes")}
+                            accessToken={accessToken || ""}
+                            placeholder="Select pass through routes (optional)"
+                            disabled={!premiumUser || !isProxyAdminRole(userRole || "")}
+                          />
+                        
+                      
                     
                   
 
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx b/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx
index 7a592785a44..e98226b86bc 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx
@@ -182,16 +182,18 @@ export function ToolTestPanel({
 
     Object.entries(values).forEach(([key, value]) => {
       const prop = schemaToUse.properties?.[key];
-      if (prop && value !== null && value !== undefined && value !== "") {
+      // Strip leading/trailing whitespace from string inputs before submitting
+      const normalizedValue = typeof value === "string" ? value.trim() : value;
+      if (prop && normalizedValue !== null && normalizedValue !== undefined && normalizedValue !== "") {
         switch (prop.type) {
           case "boolean":
-            convertedValues[key] = value === "true" || value === true;
+            convertedValues[key] = normalizedValue === "true" || normalizedValue === true;
             break;
           case "number":
           case "integer": {
-            const numericValue = Number(value);
+            const numericValue = Number(normalizedValue);
             convertedValues[key] = Number.isNaN(numericValue)
-              ? value
+              ? normalizedValue
               : prop.type === "integer"
                 ? Math.trunc(numericValue)
                 : numericValue;
@@ -200,28 +202,28 @@ export function ToolTestPanel({
           case "object":
           case "array": {
             try {
-              const parsed = typeof value === "string" ? JSON.parse(value) : value;
+              const parsed = typeof normalizedValue === "string" ? JSON.parse(normalizedValue) : normalizedValue;
               const isValidObject =
                 prop.type === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
               const isValidArray = prop.type === "array" && Array.isArray(parsed);
               if ((prop.type === "object" && isValidObject) || (prop.type === "array" && isValidArray)) {
                 convertedValues[key] = parsed;
               } else {
-                convertedValues[key] = value;
+                convertedValues[key] = normalizedValue;
               }
             } catch (err) {
-              convertedValues[key] = value;
+              convertedValues[key] = normalizedValue;
             }
             break;
           }
           case "string":
-            convertedValues[key] = String(value);
+            convertedValues[key] = String(normalizedValue);
             break;
           default:
-            convertedValues[key] = value;
+            convertedValues[key] = normalizedValue;
         }
-      } else if (value !== null && value !== undefined && value !== "") {
-        convertedValues[key] = value;
+      } else if (normalizedValue !== null && normalizedValue !== undefined && normalizedValue !== "") {
+        convertedValues[key] = normalizedValue;
       }
     });
 
diff --git a/ui/litellm-dashboard/src/components/model_dashboard/types.ts b/ui/litellm-dashboard/src/components/model_dashboard/types.ts
index b1447a0634b..77a03d2c039 100644
--- a/ui/litellm-dashboard/src/components/model_dashboard/types.ts
+++ b/ui/litellm-dashboard/src/components/model_dashboard/types.ts
@@ -6,6 +6,7 @@ export interface ModelInfo {
   team_id: string;
   db_model: boolean;
   access_groups: string[] | null;
+  blocked?: boolean;
 }
 
 export interface LiteLLMParams {
diff --git a/ui/litellm-dashboard/src/components/molecules/models/columns.test.tsx b/ui/litellm-dashboard/src/components/molecules/models/columns.test.tsx
index a3dbbb2783f..c08dca1b8ce 100644
--- a/ui/litellm-dashboard/src/components/molecules/models/columns.test.tsx
+++ b/ui/litellm-dashboard/src/components/molecules/models/columns.test.tsx
@@ -944,4 +944,108 @@ describe("columns", () => {
     expect(screen.getByText("Out: $0.03")).toBeInTheDocument();
     expect(screen.queryByText(/In:/)).not.toBeInTheDocument();
   });
+
+  describe("pause/resume toggle", () => {
+    const renderWithToggle = (
+      overrides: Partial["model_info"]> = {},
+      togglePauseHandler?: ReturnType,
+      userRole: string = "Admin",
+    ) => {
+      const handler = togglePauseHandler ?? vi.fn();
+      const cols = columns(
+        userRole,
+        defaultProps.userID,
+        defaultProps.premiumUser,
+        defaultProps.setSelectedModelId,
+        defaultProps.setSelectedTeamId,
+        defaultProps.getDisplayModelName,
+        defaultProps.handleEditClick,
+        defaultProps.handleRefreshClick,
+        defaultProps.expandedRows,
+        defaultProps.setExpandedRows,
+        vi.fn(),
+        handler,
+      );
+      const model = createMockModel({
+        model_info: { ...createMockModel().model_info, ...overrides },
+      });
+      render();
+      return { handler };
+    };
+
+    it("renders the toggle ON for a db_model that is not blocked", () => {
+      renderWithToggle({ db_model: true, blocked: false });
+      const toggle = screen.getByRole("switch", { name: /pause model/i });
+      expect(toggle).toBeEnabled();
+      expect(toggle).toHaveAttribute("aria-checked", "true");
+    });
+
+    it("renders the toggle OFF for a db_model that is blocked", () => {
+      renderWithToggle({ db_model: true, blocked: true });
+      const toggle = screen.getByRole("switch", { name: /resume model/i });
+      expect(toggle).toBeEnabled();
+      expect(toggle).toHaveAttribute("aria-checked", "false");
+    });
+
+    it("calls the handler with blocked=true when an admin flips an active toggle off", async () => {
+      const handler = vi.fn();
+      renderWithToggle({ db_model: true, blocked: false }, handler);
+      await userEvent.click(screen.getByRole("switch", { name: /pause model/i }));
+      expect(handler).toHaveBeenCalledWith("test-model-id", true);
+    });
+
+    it("calls the handler with blocked=false when an admin flips a paused toggle on", async () => {
+      const handler = vi.fn();
+      renderWithToggle({ db_model: true, blocked: true }, handler);
+      await userEvent.click(screen.getByRole("switch", { name: /resume model/i }));
+      expect(handler).toHaveBeenCalledWith("test-model-id", false);
+    });
+
+    it("disables the toggle for non-admin users", () => {
+      const handler = vi.fn();
+      renderWithToggle({ db_model: true, blocked: false }, handler, "User");
+      const toggle = screen.getByRole("switch", { name: /pause model/i });
+      expect(toggle).toBeDisabled();
+    });
+
+    it("disables the toggle for config models", () => {
+      const handler = vi.fn();
+      renderWithToggle({ db_model: false, blocked: false }, handler, "Admin");
+      const toggle = screen.getByRole("switch", { name: /pause model/i });
+      expect(toggle).toBeDisabled();
+    });
+
+    it("disables the toggle while a PATCH for the same row is in-flight", () => {
+      // Regression for Greptile P1 on PR #28151 — antd's `loading` prop is
+      // visual only and does not prevent click events, so the row needs to
+      // be explicitly disabled while its PATCH is pending to avoid
+      // racing/conflicting PATCH calls on double-click.
+      const handler = vi.fn();
+      const model = createMockModel({
+        model_info: {
+          ...createMockModel().model_info,
+          db_model: true,
+          blocked: false,
+        },
+      });
+      const cols = columns(
+        "Admin",
+        defaultProps.userID,
+        defaultProps.premiumUser,
+        defaultProps.setSelectedModelId,
+        defaultProps.setSelectedTeamId,
+        defaultProps.getDisplayModelName,
+        defaultProps.handleEditClick,
+        defaultProps.handleRefreshClick,
+        defaultProps.expandedRows,
+        defaultProps.setExpandedRows,
+        vi.fn(),
+        handler,
+        model.model_info.id, // pausingModelId matches this row
+      );
+      render();
+      const toggle = screen.getByRole("switch", { name: /pause model/i });
+      expect(toggle).toBeDisabled();
+    });
+  });
 });
diff --git a/ui/litellm-dashboard/src/components/molecules/models/columns.tsx b/ui/litellm-dashboard/src/components/molecules/models/columns.tsx
index a303e1b1a44..4563e9c80dd 100644
--- a/ui/litellm-dashboard/src/components/molecules/models/columns.tsx
+++ b/ui/litellm-dashboard/src/components/molecules/models/columns.tsx
@@ -2,7 +2,7 @@ import { EditOutlined, InfoCircleOutlined, SyncOutlined } from "@ant-design/icon
 import { TrashIcon } from "@heroicons/react/outline";
 import { ColumnDef } from "@tanstack/react-table";
 import { Badge, Button, Icon } from "@tremor/react";
-import { Divider, Flex, Popover, Space, Tooltip, Typography } from "antd";
+import { Divider, Flex, Popover, Space, Switch, Tooltip, Typography } from "antd";
 import { ModelData } from "../../model_dashboard/types";
 import { ProviderLogo } from "./ProviderLogo";
 
@@ -53,6 +53,8 @@ export const columns = (
   expandedRows: Set,
   setExpandedRows: (expandedRows: Set) => void,
   onDeleteClick?: (modelId: string) => void,
+  onTogglePauseClick?: (modelId: string, blocked: boolean) => void | Promise,
+  pausingModelId?: string | null,
 ): ColumnDef[] => [
     {
       header: () => Model ID,
@@ -398,15 +400,48 @@ export const columns = (
     {
       id: "actions",
       header: () => Actions,
-      size: 60,
-      minSize: 40,
+      size: 100,
+      minSize: 80,
       enableResizing: false,
       cell: ({ row }) => {
         const model = row.original;
         const canEditModel = userRole === "Admin" || model.model_info?.created_by === userID;
         const isConfigModel = !model.model_info?.db_model;
+        const isAdmin = userRole === "Admin";
+        const isBlocked = model.model_info?.blocked === true;
+        const isPauseToggleable = !isConfigModel && isAdmin && Boolean(onTogglePauseClick);
+        const pauseTooltip = isConfigModel
+          ? "Config models cannot be paused from the dashboard. Pause is DB-backed."
+          : !isAdmin
+            ? "Only proxy admins can pause or resume a model."
+            : isBlocked
+              ? "Resume model — restore normal routing."
+              : "Pause model — stop routing requests until resumed.";
+        // antd's `loading` prop on Switch is purely cosmetic — it does not block
+        // clicks. Pair `loading` with `disabled` derived from the same condition
+        // so a double-click during a pending PATCH cannot send a second,
+        // conflicting `blocked` value.
+        const isPausing = pausingModelId === model.model_info?.id;
         return (
           
+ + { + e.stopPropagation(); + }} + onChange={(nextChecked) => { + const modelId = model.model_info?.id; + if (isPauseToggleable && onTogglePauseClick && modelId) { + void onTogglePauseClick(modelId, !nextChecked); + } + }} + /> + {isConfigModel ? ( diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index 8f3553a8395..ce9d15e1e19 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -502,6 +502,14 @@ const TeamInfoView: React.FC = ({ (n) => !(values.guardrails || []).includes(n), ); + // Non-proxy-admins can't set allowed_passthrough_routes; preserve the + // stored value so an unrelated save can't wipe it. + const passthroughRoutesMetadata = is_proxy_admin + ? { allowed_passthrough_routes: values.allowed_passthrough_routes || [] } + : info.metadata?.allowed_passthrough_routes + ? { allowed_passthrough_routes: info.metadata.allowed_passthrough_routes } + : {}; + const updateData: any = { team_id: teamId, team_alias: values.team_alias, @@ -515,6 +523,7 @@ const TeamInfoView: React.FC = ({ budget_duration: values.budget_duration, metadata: { ...parsedMetadata, + ...passthroughRoutesMetadata, guardrails: (values.guardrails || []).filter((n: string) => !globalGuardrailNames.has(n)), opted_out_global_guardrails: optedOutGlobalGuardrails, ...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}), @@ -961,7 +970,7 @@ const TeamInfoView: React.FC = ({ : "", metadata: info.metadata ? JSON.stringify( - (({ logging, secret_manager_settings, soft_budget_alerting_emails, model_tpm_limit, model_rpm_limit, ...rest }) => rest)(info.metadata), + (({ logging, secret_manager_settings, soft_budget_alerting_emails, model_tpm_limit, model_rpm_limit, allowed_passthrough_routes, ...rest }) => rest)(info.metadata), null, 2, ) @@ -986,6 +995,7 @@ const TeamInfoView: React.FC = ({ }, access_group_ids: info.access_group_ids || [], default_team_member_models: info.default_team_member_models || [], + allowed_passthrough_routes: info.metadata?.allowed_passthrough_routes || [], }} layout="vertical" > @@ -1338,12 +1348,24 @@ const TeamInfoView: React.FC = ({ - form.setFieldValue("allowed_passthrough_routes", values)} - value={form.getFieldValue("allowed_passthrough_routes")} - accessToken={accessToken || ""} - placeholder="Select pass through routes" - /> + + form.setFieldValue("allowed_passthrough_routes", values)} + value={form.getFieldValue("allowed_passthrough_routes")} + accessToken={accessToken || ""} + placeholder="Select pass through routes" + disabled={!premiumUser || !is_proxy_admin} + /> + diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 03d917cd923..6c5fd03f0a0 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -208,6 +208,8 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p const deferredData = useDeferredValue(filteredData); const isStale = deferredData !== filteredData; const isButtonLoading = logsQuery.isFetching || isStale; + const isRefiltering = logsQuery.isPlaceholderData; + const isLogsLoading = logsQuery.isLoading || isRefiltering; if (!accessToken || !token || !userRole || !userID) { return ( @@ -277,7 +279,7 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p currentPage={currentPage} onCurrentPageChange={setCurrentPage} pageSize={pageSize} - isLoading={logsQuery.isLoading} + isLoading={isLogsLoading} isButtonLoading={isButtonLoading} onRefetch={() => logsQuery.refetch()} filteredLogs={filteredLogs} @@ -286,7 +288,7 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p columns={columns} data={deferredData} onRowClick={handleRowClick} - isLoading={logsQuery.isLoading} + isLoading={isLogsLoading} />