From be4152c8d56c395a841b9f1559d3263035ad74ee Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 23 Apr 2025 21:56:56 -0700 Subject: [PATCH] UI - fix edit azure public model name + fix editing model name post create * test(test_router.py): add unit test confirming fallbacks with tag based routing works as expected * test: update testing * test: update test to not use gemini-pro google removed it * fix(conditional_public_model_name.tsx): edit azure public model name Fixes https://github.com/BerriAI/litellm/issues/10093 * fix(model_info_view.tsx): migrate to patch model updates Enables changing model name easily --- tests/litellm/test_router.py | 40 +++++++++++++++++ .../conditional_public_model_name.tsx | 38 ++++++++++------ .../add_model/litellm_model_name.tsx | 30 ++++++++++--- .../src/components/model_info_view.tsx | 6 ++- .../src/components/networking.tsx | 43 +++++++++++++++++++ 5 files changed, 136 insertions(+), 21 deletions(-) diff --git a/tests/litellm/test_router.py b/tests/litellm/test_router.py index 3a572d861d..95bdfccebb 100644 --- a/tests/litellm/test_router.py +++ b/tests/litellm/test_router.py @@ -78,3 +78,43 @@ def test_router_with_model_info_and_model_group(): model_group="gpt-3.5-turbo", user_facing_model_group_name="gpt-3.5-turbo", ) + + +@pytest.mark.asyncio +async def test_router_with_tags_and_fallbacks(): + """ + If fallback model missing tag, raise error + """ + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "gpt-3.5-turbo", + "litellm_params": { + "model": "gpt-3.5-turbo", + "mock_response": "Hello, world!", + "tags": ["test"], + }, + }, + { + "model_name": "anthropic-claude-3-5-sonnet", + "litellm_params": { + "model": "claude-3-5-sonnet-latest", + "mock_response": "Hello, world 2!", + }, + }, + ], + fallbacks=[ + {"gpt-3.5-turbo": ["anthropic-claude-3-5-sonnet"]}, + ], + enable_tag_filtering=True, + ) + + with pytest.raises(Exception): + response = await router.acompletion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Hello, world!"}], + mock_testing_fallbacks=True, + metadata={"tags": ["test"]}, + ) 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 75ca5ae328..0f79401753 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 @@ -14,6 +14,7 @@ const ConditionalPublicModelName: React.FC = () => { const customModelName = Form.useWatch('custom_model_name', form); const showPublicModelName = !selectedModels.includes('all-wildcard'); + // Force table to re-render when custom model name changes useEffect(() => { if (customModelName && selectedModels.includes('custom')) { @@ -35,20 +36,33 @@ const ConditionalPublicModelName: React.FC = () => { // Initial setup of model mappings when models are selected useEffect(() => { if (selectedModels.length > 0 && !selectedModels.includes('all-wildcard')) { - const mappings = selectedModels.map((model: string) => { - if (model === 'custom' && customModelName) { + // Check if we already have mappings that match the selected models + const currentMappings = form.getFieldValue('model_mappings') || []; + + // Only update if the mappings don't exist or don't match the selected models + const shouldUpdateMappings = currentMappings.length !== selectedModels.length || + !selectedModels.every(model => + currentMappings.some((mapping: { public_name: string; litellm_model: string }) => + mapping.public_name === model || + (model === 'custom' && mapping.public_name === customModelName))); + + if (shouldUpdateMappings) { + const mappings = selectedModels.map((model: string) => { + if (model === 'custom' && customModelName) { + return { + public_name: customModelName, + litellm_model: customModelName + }; + } return { - public_name: customModelName, - litellm_model: customModelName + public_name: model, + litellm_model: model }; - } - return { - public_name: model, - litellm_model: model - }; - }); - form.setFieldValue('model_mappings', mappings); - setTableKey(prev => prev + 1); // Force table re-render + }); + + form.setFieldValue('model_mappings', mappings); + setTableKey(prev => prev + 1); // Force table re-render + } } }, [selectedModels, customModelName, form]); diff --git a/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx b/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx index 06fad01187..f5c92f8df5 100644 --- a/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx +++ b/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx @@ -23,22 +23,34 @@ const LiteLLMModelNameField: React.FC = ({ // If "all-wildcard" is selected, clear the model_name field if (values.includes("all-wildcard")) { - form.setFieldsValue({ model_name: undefined, model_mappings: [] }); + form.setFieldsValue({ model: undefined, model_mappings: [] }); } else { - // Update model mappings immediately for each selected model - const mappings = values - .map(model => ({ + // Get current model value to check if we need to update + const currentModel = form.getFieldValue('model'); + + // Only update if the value has actually changed + if (JSON.stringify(currentModel) !== JSON.stringify(values)) { + + // Create mappings first + const mappings = values.map(model => ({ public_name: model, litellm_model: model })); - form.setFieldsValue({ model_mappings: mappings }); + + // Update both fields in one call to reduce re-renders + form.setFieldsValue({ + model: values, + model_mappings: mappings + }); + + } } }; // Handle custom model name changes const handleCustomModelNameChange = (e: React.ChangeEvent) => { const customName = e.target.value; - + // Immediately update the model mappings const currentMappings = form.getFieldValue('model_mappings') || []; const updatedMappings = currentMappings.map((mapping: any) => { @@ -69,7 +81,11 @@ const LiteLLMModelNameField: React.FC = ({ {(selectedProvider === Providers.Azure) || (selectedProvider === Providers.OpenAI_Compatible) || (selectedProvider === Providers.Ollama) ? ( - + <> + + ) : providerModels.length > 0 ? ( , // Assuming formValues is an object + modelId: string +) => { + try { + console.log("Form Values in modelUpateCall:", formValues); // Log the form values before making the API call + + const url = proxyBaseUrl ? `${proxyBaseUrl}/model/${modelId}/update` : `/model/${modelId}/update`; + const response = await fetch(url, { + method: "PATCH", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...formValues, // Include formValues in the request body + }), + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + console.error("Error update from the server:", errorData); + throw new Error("Network response was not ok"); + } + const data = await response.json(); + console.log("Update model Response:", data); + return data; + // Handle success - you might want to update some state or UI based on the created key + } catch (error) { + console.error("Failed to update model:", error); + throw error; + } +}; + export const modelUpdateCall = async ( accessToken: string, formValues: Record // Assuming formValues is an object