diff --git a/litellm/main.py b/litellm/main.py index 6ae2df517d..64049c31d1 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -74,6 +74,7 @@ from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.realtime_api.main import _realtime_health_check from litellm.secret_managers.main import get_secret_str from litellm.types.router import GenericLiteLLMParams +from litellm.types.utils import RawRequestTypedDict from litellm.utils import ( CustomStreamWrapper, ProviderConfigManager, @@ -2986,9 +2987,11 @@ def completion( # type: ignore # noqa: PLR0915 ) return response response = model_response - elif custom_llm_provider == "snowflake" or model in litellm.snowflake_models: + elif custom_llm_provider == "snowflake" or model in litellm.snowflake_models: try: - client = HTTPHandler(timeout=timeout) if stream is False else None # Keep this here, otherwise, the httpx.client closes and streaming is impossible + client = ( + HTTPHandler(timeout=timeout) if stream is False else None + ) # Keep this here, otherwise, the httpx.client closes and streaming is impossible response = base_llm_http_handler.completion( model=model, messages=messages, @@ -3001,12 +3004,11 @@ def completion( # type: ignore # noqa: PLR0915 optional_params=optional_params, litellm_params=litellm_params, timeout=timeout, # type: ignore - client= client, + client=client, custom_llm_provider=custom_llm_provider, encoding=encoding, stream=stream, - ) - + ) except Exception as e: ## LOGGING - log the original exception returned @@ -3017,7 +3019,7 @@ def completion( # type: ignore # noqa: PLR0915 additional_args={"headers": headers}, ) raise e - + elif custom_llm_provider == "custom": url = litellm.api_base or api_base or "" if url is None or url == "": @@ -5449,6 +5451,17 @@ async def ahealth_check( "x-ms-region": str, } """ + # Map modes to their corresponding health check calls + litellm_logging_obj = Logging( + model="", + messages=[], + stream=False, + call_type="acompletion", + litellm_call_id="1234", + start_time=datetime.datetime.now(), + function_id="1234", + log_raw_request_response=True, + ) try: model: Optional[str] = model_params.get("model", None) if model is None: @@ -5471,9 +5484,12 @@ async def ahealth_check( custom_llm_provider=custom_llm_provider, model_params=model_params, ) - # Map modes to their corresponding health check calls + model_params["litellm_logging_obj"] = litellm_logging_obj + mode_handlers = { - "chat": lambda: litellm.acompletion(**model_params), + "chat": lambda: litellm.acompletion( + **model_params, + ), "completion": lambda: litellm.atext_completion( **_filter_model_params(model_params), prompt=prompt or "test", @@ -5530,13 +5546,16 @@ async def ahealth_check( "error": f"error:{str(e)}. Missing `mode`. Set the `mode` for the model - https://docs.litellm.ai/docs/proxy/health#embedding-models \nstacktrace: {stack_trace}" } - error_to_return = ( - str(e) - + "\nHave you set 'mode' - https://docs.litellm.ai/docs/proxy/health#embedding-models" - + "\nstack trace: " - + stack_trace + error_to_return = str(e) + "\nstack trace: " + stack_trace + + raw_request_typed_dict = litellm_logging_obj.model_call_details.get( + "raw_request_typed_dict" ) - return {"error": error_to_return} + + return { + "error": error_to_return, + "raw_request_typed_dict": raw_request_typed_dict, + } ####### HELPER FUNCTIONS ################ diff --git a/litellm/proxy/health_check.py b/litellm/proxy/health_check.py index f452dded62..f9455387cc 100644 --- a/litellm/proxy/health_check.py +++ b/litellm/proxy/health_check.py @@ -36,6 +36,7 @@ def _clean_endpoint_data(endpoint_data: dict, details: Optional[bool] = True): """ Clean the endpoint data for display to users. """ + endpoint_data.pop("litellm_logging_obj", None) return ( {k: v for k, v in endpoint_data.items() if k not in ILLEGAL_DISPLAY_PARAMS} if details is not False diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index f52ab15c4e..34e7d34bbf 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -3,13 +3,14 @@ import copy import os import traceback from datetime import datetime, timedelta -from typing import Literal, Optional, Union +from typing import Dict, Literal, Optional, Union import fastapi from fastapi import APIRouter, Depends, HTTPException, Request, Response, status import litellm from litellm._logging import verbose_proxy_logger +from litellm.constants import HEALTH_CHECK_TIMEOUT_SECONDS from litellm.proxy._types import ( AlertType, CallInfo, @@ -19,7 +20,12 @@ from litellm.proxy._types import ( WebhookEvent, ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.health_check import perform_health_check +from litellm.proxy.health_check import ( + _clean_endpoint_data, + _update_litellm_params_for_health_check, + perform_health_check, + run_with_timeout, +) #### Health ENDPOINTS #### @@ -600,3 +606,93 @@ async def health_liveliness_options(): "Access-Control-Allow-Headers": "*", } return Response(headers=response_headers, status_code=200) + + +@router.post( + "/health/test_connection", + tags=["health"], + dependencies=[Depends(user_api_key_auth)], +) +async def test_model_connection( + request: Request, + mode: Optional[ + Literal[ + "chat", + "completion", + "embedding", + "audio_speech", + "audio_transcription", + "image_generation", + "batch", + "rerank", + "realtime", + ] + ] = fastapi.Body("chat", description="The mode to test the model with"), + litellm_params: Dict = fastapi.Body( + None, + description="Parameters for litellm.completion, litellm.embedding for the health check", + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Test a direct connection to a specific model. + + This endpoint allows you to verify if your proxy can successfully connect to a specific model. + It's useful for troubleshooting model connectivity issues without going through the full proxy routing. + + Example: + ```bash + curl -X POST 'http://localhost:4000/health/test_connection' \\ + -H 'Authorization: Bearer sk-1234' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "litellm_params": { + "model": "gpt-4", + "custom_llm_provider": "azure_ai", + "litellm_credential_name": null, + "api_key": "6xxxxxxx", + "api_base": "https://litellm8397336933.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21", + }, + "mode": "chat" + }' + ``` + + Returns: + dict: A dictionary containing the health check result with either success information or error details. + """ + try: + # Include health_check_params if provided + litellm_params = _update_litellm_params_for_health_check( + model_info={}, + litellm_params=litellm_params, + ) + mode = mode or litellm_params.pop("mode", None) + result = await run_with_timeout( + litellm.ahealth_check( + model_params=litellm_params, + mode=mode, + prompt="test from litellm", + input=["test from litellm"], + ), + HEALTH_CHECK_TIMEOUT_SECONDS, + ) + + # Clean the result for display + cleaned_result = _clean_endpoint_data( + {**litellm_params, **result}, details=True + ) + + return { + "status": "error" if "error" in result else "success", + "result": cleaned_result, + } + + except Exception as e: + verbose_proxy_logger.error( + f"litellm.proxy.health_endpoints.test_model_connection(): Exception occurred - {str(e)}" + ) + verbose_proxy_logger.debug(traceback.format_exc()) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={"error": f"Failed to test connection: {str(e)}"}, + ) diff --git a/ui/litellm-dashboard/src/components/add_model/add_model_modes.tsx b/ui/litellm-dashboard/src/components/add_model/add_model_modes.tsx new file mode 100644 index 0000000000..2b544fd190 --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/add_model_modes.tsx @@ -0,0 +1,11 @@ +// Define the available test modes +export const TEST_MODES = [ + { value: "chat", label: "Chat - /chat/completions" }, + { value: "completion", label: "Completion - /completions" }, + { value: "embedding", label: "Embedding - /embeddings" }, + { value: "audio_speech", label: "Audio Speech - /audio/speech" }, + { value: "audio_transcription", label: "Audio Transcription - /audio/transcriptions" }, + { value: "image_generation", label: "Image Generation - /images/generations" }, + { value: "rerank", label: "Rerank - /rerank" }, + { value: "realtime", label: "Realtime - /realtime"} +]; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx index c699c161e3..e2115e8467 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect } from "antd"; +import React, { useState } from "react"; +import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd"; import type { FormInstance } from "antd"; import type { UploadProps } from "antd/es/upload"; import LiteLLMModelNameField from "./litellm_model_name"; @@ -9,6 +9,11 @@ import AdvancedSettings from "./advanced_settings"; import { Providers, providerLogoMap, getPlaceholder } from "../provider_info_helpers"; import type { Team } from "../key_team_helpers/key_list"; import { CredentialItem } from "../networking"; +import ConnectionErrorDisplay from "./model_connection_test"; +import { TEST_MODES } from "./add_model_modes"; +import { Row, Col } from "antd"; +import { Text, TextInput } from "@tremor/react"; + interface AddModelTabProps { form: FormInstance; handleOk: () => void; @@ -22,6 +27,7 @@ interface AddModelTabProps { setShowAdvancedSettings: (show: boolean) => void; teams: Team[] | null; credentials: CredentialItem[]; + accessToken: string; } const { Title, Link } = Typography; @@ -39,158 +45,234 @@ const AddModelTab: React.FC = ({ setShowAdvancedSettings, teams, credentials, + accessToken, }) => { + // State for test mode and connection testing + const [testMode, setTestMode] = useState("chat"); + const [isResultModalVisible, setIsResultModalVisible] = useState(false); + const [isTestingConnection, setIsTestingConnection] = useState(false); + // Using a unique ID to force the ConnectionErrorDisplay to remount and run a fresh test + const [connectionTestId, setConnectionTestId] = useState(""); + + // Test connection when button is clicked + const handleTestConnection = async () => { + setIsTestingConnection(true); + // Generate a new test ID (using timestamp for uniqueness) + // This forces React to create a new instance of ConnectionErrorDisplay + setConnectionTestId(`test-${Date.now()}`); + // Show the modal with the fresh test + setIsResultModalVisible(true); + }; + return ( <> Add new model -
+ <> + {/* Provider Selection */} + + { + setSelectedProvider(value); + setProviderModelsFn(value); + form.setFieldsValue({ + model: [], + model_name: undefined + }); + }} > - <> - {/* Provider Selection */} - ( + - { - setSelectedProvider(value); - setProviderModelsFn(value); - form.setFieldsValue({ - model: [], - model_name: undefined - }); - }} - > - {Object.entries(Providers).map(([providerEnum, providerDisplayName]) => ( - -
- {`${providerEnum} { - // Create a div with provider initial as fallback - const target = e.target as HTMLImageElement; - const parent = target.parentElement; - if (parent) { - const fallbackDiv = document.createElement('div'); - fallbackDiv.className = 'w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs'; - fallbackDiv.textContent = providerDisplayName.charAt(0); - parent.replaceChild(fallbackDiv, target); - } - }} - /> - {providerDisplayName} -
-
- ))} -
-
- + {`${providerEnum} { + // Create a div with provider initial as fallback + const target = e.target as HTMLImageElement; + const parent = target.parentElement; + if (parent) { + const fallbackDiv = document.createElement('div'); + fallbackDiv.className = 'w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs'; + fallbackDiv.textContent = providerDisplayName.charAt(0); + parent.replaceChild(fallbackDiv, target); + } + }} + /> + {providerDisplayName} + + + ))} +
+
+ + + {/* Conditionally Render "Public Model Name" */} + + + {/* Select Mode */} + + setTestMode(value)} + options={TEST_MODES} + /> + + + + + + Optional - LiteLLM endpoint to use when health checking this model Learn more + + + + + {/* Credentials */} +
+ + Either select existing credentials OR enter new provider credentials below + +
+ + + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + options={[ + { value: null, label: 'None' }, + ...credentials.map((credential) => ({ + value: credential.credential_name, + label: credential.credential_name + })) + ]} + allowClear + /> + + +
+
+ OR +
+
+ + + prevValues.litellm_credential_name !== currentValues.litellm_credential_name || + prevValues.provider !== currentValues.provider + } + > + {({ getFieldValue }) => { + const credentialName = getFieldValue('litellm_credential_name'); + console.log("🔑 Credential Name Changed:", credentialName); + // Only show provider specific fields if no credentials selected + if (!credentialName) { + return ( + - - {/* Conditionally Render "Public Model Name" */} - - - {/* Credentials */} -
- - Either select existing credentials OR enter new provider credentials below - + ); + } + return ( +
+ Using existing credentials - no additional provider fields needed
+ ); + }} + + - - - (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) - } - options={[ - { value: null, label: 'None' }, - ...credentials.map((credential) => ({ - value: credential.credential_name, - label: credential.credential_name - })) - ]} - allowClear - /> - - -
-
- OR -
-
- - - prevValues.litellm_credential_name !== currentValues.litellm_credential_name || - prevValues.provider !== currentValues.provider - } - > - {({ getFieldValue }) => { - const credentialName = getFieldValue('litellm_credential_name'); - console.log("🔑 Credential Name Changed:", credentialName); - // Only show provider specific fields if no credentials selected - if (!credentialName) { - return ( - - ); - } - return ( -
- Using existing credentials - no additional provider fields needed -
- ); - }} -
- - - -
- - - Need Help? - - - -
- - - - +
+ + + Need Help? + + +
+ + +
+
+ + + + {/* Test Connection Results Modal */} + { + setIsResultModalVisible(false); + setIsTestingConnection(false); + }} + footer={[ + + ]} + width={700} + > + {/* Only render the ConnectionErrorDisplay when modal is visible and we have a test ID */} + {isResultModalVisible && ( + { + setIsResultModalVisible(false); + setIsTestingConnection(false); + }} + onTestComplete={() => setIsTestingConnection(false)} + /> + )} + ); }; diff --git a/ui/litellm-dashboard/src/components/add_model/conditional_public_model_name.tsx b/ui/litellm-dashboard/src/components/add_model/conditional_public_model_name.tsx index 8e0f6a288b..75ca5ae328 100644 --- a/ui/litellm-dashboard/src/components/add_model/conditional_public_model_name.tsx +++ b/ui/litellm-dashboard/src/components/add_model/conditional_public_model_name.tsx @@ -98,14 +98,6 @@ const ConditionalPublicModelName: React.FC = () => { size="small" /> - - - - - Model name your users will pass in. - - - ); }; diff --git a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx index 0fa08e48af..d54198854c 100644 --- a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx +++ b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx @@ -1,13 +1,13 @@ import { message } from "antd"; import { provider_map, Providers } from "../provider_info_helpers"; -import { modelCreateCall, Model } from "../networking"; +import { modelCreateCall, Model, testConnectionRequest } from "../networking"; +import React, { useState } from 'react'; +import ConnectionErrorDisplay from './model_connection_test'; - -export const handleAddModelSubmit = async ( +export const prepareModelAddRequest = async ( formValues: Record, accessToken: string, form: any, - callback?: ()=>void ) => { try { console.log("handling submit for formValues:", formValues); @@ -72,7 +72,6 @@ export const handleAddModelSubmit = async ( } else if (key == "model") { continue; } - // Check if key is "base_model" else if (key === "base_model") { // Add key-value pair to model_info dictionary @@ -81,6 +80,13 @@ export const handleAddModelSubmit = async ( else if (key === "team_id") { modelInfoObj["team_id"] = value; } + else if (key == "mode") { + console.log("placing mode in modelInfo") + modelInfoObj["mode"] = value; + + // remove "mode" from litellmParams + delete litellmParamsObj["mode"]; + } else if (key === "custom_model_name") { litellmParamsObj["model"] = value; } else if (key == "litellm_extra_params") { @@ -135,20 +141,43 @@ export const handleAddModelSubmit = async ( litellmParamsObj[key] = value; } } - - const new_model: Model = { - model_name: modelName, - litellm_params: litellmParamsObj, - model_info: modelInfoObj, - }; - - const response: any = await modelCreateCall(accessToken, new_model); - console.log(`response for model create call: ${response["data"]}`); - } - callback && callback() - form.resetFields(); + return { litellmParamsObj, modelInfoObj, modelName }; + } } catch (error) { message.error("Failed to create model: " + error, 10); } - }; \ No newline at end of file + }; + +export const handleAddModelSubmit = async ( + values: any, + accessToken: string, + form: any, + callback?: () => void, + ) => { + try { + const result = await prepareModelAddRequest(values, accessToken, form); + + if (!result) { + return; // Exit if preparation failed + } + + const { litellmParamsObj, modelInfoObj, modelName } = result; + + const new_model: Model = { + model_name: modelName, + litellm_params: litellmParamsObj, + model_info: modelInfoObj, + }; + + const response: any = await modelCreateCall(accessToken, new_model); + console.log(`response for model create call: ${response["data"]}`); + + callback && callback(); + form.resetFields(); + } catch (error) { + message.error("Failed to add model: " + error, 10); + } + }; + + diff --git a/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx b/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx new file mode 100644 index 0000000000..6c96fe318a --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx @@ -0,0 +1,258 @@ +import React from 'react'; +import { Typography, Space, Button, Divider, message } from 'antd'; +import { WarningOutlined, InfoCircleOutlined, CopyOutlined } from '@ant-design/icons'; +import { testConnectionRequest } from "../networking"; +import { prepareModelAddRequest } from "./handle_add_model_submit"; + +const { Text } = Typography; + +interface ModelConnectionTestProps { + formValues: Record; + accessToken: string; + testMode: string; + modelName?: string; + onClose?: () => void; + onTestComplete?: () => void; +} + +const ModelConnectionTest: React.FC = ({ + formValues, + accessToken, + testMode, + modelName = "this model", + onClose, + onTestComplete +}) => { + const [error, setError] = React.useState(null); + const [rawRequest, setRawRequest] = React.useState(null); + const [rawResponse, setRawResponse] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(true); + const [isSuccess, setIsSuccess] = React.useState(false); + const [showDetails, setShowDetails] = React.useState(false); + + const testModelConnection = async () => { + setIsLoading(true); + setShowDetails(false); + setError(null); + setRawRequest(null); + setRawResponse(null); + setIsSuccess(false); + + // Add a small delay to ensure form values are fully populated + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + console.log("Testing connection with form values:", formValues); + const result = await prepareModelAddRequest(formValues, accessToken, null); + + if (!result) { + console.log("No result from prepareModelAddRequest"); + setError("Failed to prepare model data. Please check your form inputs."); + setIsSuccess(false); + setIsLoading(false); + return; + } + + console.log("Result from prepareModelAddRequest:", result); + + const { litellmParamsObj, modelInfoObj, modelName: returnedModelName } = result; + + const response = await testConnectionRequest(accessToken, litellmParamsObj, modelInfoObj?.mode); + if (response.status === "success") { + message.success("Connection test successful!"); + setError(null); + setIsSuccess(true); + } else { + const errorMessage = response.result?.error || response.message || "Unknown error"; + setError(errorMessage); + setRawRequest(litellmParamsObj); + setRawResponse(response.result?.raw_request_typed_dict); + setIsSuccess(false); + } + } catch (error) { + console.error("Test connection error:", error); + setError(error instanceof Error ? error.message : String(error)); + setIsSuccess(false); + } finally { + setIsLoading(false); + if (onTestComplete) onTestComplete(); + } + }; + + React.useEffect(() => { + // Run the test once when component mounts + // Add a small timeout to ensure form values are ready + const timer = setTimeout(() => { + testModelConnection(); + }, 200); + + return () => clearTimeout(timer); + }, []); // Empty dependency array means this runs once on mount + + const getCleanErrorMessage = (errorMsg: string) => { + if (!errorMsg) return "Unknown error"; + + const mainError = errorMsg.split('stack trace:')[0].trim(); + + const cleanedError = mainError.replace(/^litellm\.(.*?)Error: /, ''); + + return cleanedError; + }; + + const errorMessage = typeof error === 'string' + ? getCleanErrorMessage(error) + : error?.message ? getCleanErrorMessage(error.message) : "Unknown error"; + + const formatCurlCommand = (apiBase: string, requestBody: Record, requestHeaders: Record) => { + const formattedBody = JSON.stringify(requestBody, null, 2) + .split('\n') + .map(line => ` ${line}`) + .join('\n'); + + const headerString = Object.entries(requestHeaders) + .map(([key, value]) => `-H '${key}: ${value}'`) + .join(' \\\n '); + + return `curl -X POST \\ + ${apiBase} \\ + ${headerString ? `${headerString} \\\n ` : ''}-H 'Content-Type: application/json' \\ + -d '{ +${formattedBody} + }'`; + }; + + const curlCommand = rawResponse ? formatCurlCommand( + rawResponse.raw_request_api_base, + rawResponse.raw_request_body, + rawResponse.raw_request_headers || {} + ) : ''; + + return ( +
+ {isLoading ? ( +
+
+ {/* Simple CSS spinner */} +
+
+ Testing connection to {modelName}... + +
+ ) : isSuccess ? ( +
+
+ +
+ + Connection to {modelName} successful! + +
+ ) : ( + <> +
+
+ + Connection to {modelName} failed +
+ +
+ Error: + {errorMessage} + + {error && ( +
+ +
+ )} +
+ + {showDetails && ( +
+ Troubleshooting Details +
+                  {typeof error === 'string' ? error : JSON.stringify(error, null, 2)}
+                
+
+ )} + +
+ API Request +
+                {curlCommand || "No request data available"}
+              
+ +
+
+ + )} + +
+ +
+
+ ); +}; + +export default ModelConnectionTest; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/model_dashboard.tsx b/ui/litellm-dashboard/src/components/model_dashboard.tsx index 0a9c26ac4a..77e422ce43 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard.tsx @@ -1140,6 +1140,7 @@ const ModelDashboard: React.FC = ({ setShowAdvancedSettings={setShowAdvancedSettings} teams={teams} credentials={credentialsList} + accessToken={accessToken} /> diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index c30cf727b0..70e84dc916 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2313,6 +2313,63 @@ export const keyInfoCall = async (accessToken: String, keys: String[]) => { }; +export const testConnectionRequest = async ( + accessToken: string, + litellm_params: Record, + mode: string, +) => { + try { + console.log("Sending model connection test request:", JSON.stringify(litellm_params)); + + // Construct the URL based on environment + const url = proxyBaseUrl ? `${proxyBaseUrl}/health/test_connection` : `/health/test_connection`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + [globalLitellmHeaderName]: `Bearer ${accessToken}` + }, + body: JSON.stringify( + { + litellm_params: litellm_params, + mode: mode, + } + ) + }); + + // Check for non-JSON responses first + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + const text = await response.text(); + console.error("Received non-JSON response:", text); + throw new Error(`Received non-JSON response (${response.status}: ${response.statusText}). Check network tab for details.`); + } + + const data = await response.json(); + + if (!response.ok || data.status === "error") { + // Return the error response instead of throwing an error + // This allows the caller to handle the error format properly + if (data.status === "error") { + return data; // Return the full error response + } else { + return { + status: "error", + message: data.error?.message || `Connection test failed: ${response.status} ${response.statusText}` + }; + } + } + + return data; + } catch (error) { + console.error("Model connection test error:", error); + // For network errors or other exceptions, still throw + throw error; + } +}; + +// ... existing code ... export const keyInfoV1Call = async (accessToken: string, key: string) => { try { let url = proxyBaseUrl ? `${proxyBaseUrl}/key/info` : `/key/info`;