From 230dcec902d8f87256b2c981ec4bb5cbcc42db43 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 23 Mar 2026 12:26:14 +0530 Subject: [PATCH] address greptile review feedback (greploop iteration 1) - Move trainingType injection to AzureOpenAIFineTuningAPI handler - Guard normalization with is_azure flag to only apply to Azure responses - Override acreate_fine_tuning_job in Azure handler to use is_azure=True - Update test to directly test _ensure_training_type method - Add test for OpenAI unchanged behavior Made-with: Cursor --- litellm/fine_tuning/main.py | 9 -- litellm/llms/azure/fine_tuning/handler.py | 85 ++++++++++++++++++- litellm/llms/openai/fine_tuning/handler.py | 13 ++- tests/batches_tests/test_fine_tuning_api.py | 27 ++---- .../types/llms/test_types_llms_openai.py | 11 ++- 5 files changed, 110 insertions(+), 35 deletions(-) diff --git a/litellm/fine_tuning/main.py b/litellm/fine_tuning/main.py index a611488b179..08373cda782 100644 --- a/litellm/fine_tuning/main.py +++ b/litellm/fine_tuning/main.py @@ -245,15 +245,6 @@ def create_fine_tuning_job( ) # Azure OpenAI elif custom_llm_provider == "azure": - # Azure requires trainingType (e.g. 1 = supervised). Omitting it yields a misleading - # "The specified base model does not support fine-tuning" error from the service. - if kwargs.get("trainingType") is None: - _eb = ( - kwargs.get("extra_body") or optional_params.get("extra_body") or {} - ) - if not (isinstance(_eb, dict) and _eb.get("trainingType") is not None): - kwargs["trainingType"] = 1 - api_base = optional_params.api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") # type: ignore api_version = ( diff --git a/litellm/llms/azure/fine_tuning/handler.py b/litellm/llms/azure/fine_tuning/handler.py index 429b8349896..2415c7d78ed 100644 --- a/litellm/llms/azure/fine_tuning/handler.py +++ b/litellm/llms/azure/fine_tuning/handler.py @@ -1,10 +1,15 @@ -from typing import Optional, Union +from typing import Any, Coroutine, Dict, Optional, Union, cast import httpx from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI +from litellm._logging import verbose_logger from litellm.llms.azure.common_utils import BaseAzureLLM -from litellm.llms.openai.fine_tuning.handler import OpenAIFineTuningAPI +from litellm.llms.openai.fine_tuning.handler import ( + OpenAIFineTuningAPI, + _litellm_fine_tuning_job_from_response, +) +from litellm.types.utils import LiteLLMFineTuningJob class AzureOpenAIFineTuningAPI(OpenAIFineTuningAPI, BaseAzureLLM): @@ -12,6 +17,82 @@ class AzureOpenAIFineTuningAPI(OpenAIFineTuningAPI, BaseAzureLLM): AzureOpenAI methods to support fine tuning, inherits from OpenAIFineTuningAPI. """ + @staticmethod + def _ensure_training_type(create_fine_tuning_job_data: Dict[str, Any]) -> None: + """ + Azure requires trainingType in extra_body. Default to 1 (supervised) if omitted. + """ + extra_body = create_fine_tuning_job_data.get("extra_body") or {} + if not isinstance(extra_body, dict): + extra_body = {} + if extra_body.get("trainingType") is None: + extra_body["trainingType"] = 1 + create_fine_tuning_job_data["extra_body"] = extra_body + verbose_logger.debug( + "Azure fine-tuning: defaulting trainingType=1 (supervised)" + ) + + async def acreate_fine_tuning_job( + self, + create_fine_tuning_job_data: dict, + openai_client: Union[AsyncOpenAI, AsyncAzureOpenAI], + ) -> LiteLLMFineTuningJob: + response = await openai_client.fine_tuning.jobs.create( + **create_fine_tuning_job_data + ) + return _litellm_fine_tuning_job_from_response(response, is_azure=True) + + def create_fine_tuning_job( + self, + _is_async: bool, + create_fine_tuning_job_data: dict, + api_key: Optional[str], + api_base: Optional[str], + api_version: Optional[str], + timeout: Union[float, httpx.Timeout], + max_retries: Optional[int], + organization: Optional[str], + client: Optional[ + Union[OpenAI, AsyncOpenAI, AzureOpenAI, AsyncAzureOpenAI] + ] = None, + ) -> Union[LiteLLMFineTuningJob, Coroutine[Any, Any, LiteLLMFineTuningJob]]: + self._ensure_training_type(create_fine_tuning_job_data) + + openai_client: Optional[ + Union[OpenAI, AsyncOpenAI, AzureOpenAI, AsyncAzureOpenAI] + ] = self.get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + max_retries=max_retries, + organization=organization, + client=client, + _is_async=_is_async, + api_version=api_version, + ) + if openai_client is None: + raise ValueError( + "Azure OpenAI client is not initialized. Make sure api_key is passed or AZURE_API_KEY is set in the environment." + ) + + if _is_async is True: + if not isinstance(openai_client, (AsyncOpenAI, AsyncAzureOpenAI)): + raise ValueError( + "OpenAI client is not an instance of AsyncOpenAI. Make sure you passed an AsyncOpenAI client." + ) + return self.acreate_fine_tuning_job( + create_fine_tuning_job_data=create_fine_tuning_job_data, + openai_client=openai_client, + ) + + verbose_logger.debug( + "creating fine tuning job, args= %s", create_fine_tuning_job_data + ) + response = cast(OpenAI, openai_client).fine_tuning.jobs.create( + **create_fine_tuning_job_data + ) + return _litellm_fine_tuning_job_from_response(response, is_azure=True) + def get_openai_client( self, api_key: Optional[str], diff --git a/litellm/llms/openai/fine_tuning/handler.py b/litellm/llms/openai/fine_tuning/handler.py index 581ca1eea8c..2bb39aeaea4 100644 --- a/litellm/llms/openai/fine_tuning/handler.py +++ b/litellm/llms/openai/fine_tuning/handler.py @@ -7,7 +7,9 @@ from litellm._logging import verbose_logger from litellm.types.utils import LiteLLMFineTuningJob -def _normalize_fine_tuning_job_dict(data: Dict[str, Any]) -> Dict[str, Any]: +def _normalize_fine_tuning_job_dict( + data: Dict[str, Any], is_azure: bool = False +) -> Dict[str, Any]: """ Normalize Azure OpenAI FineTuningJob response to match OpenAI schema. @@ -16,6 +18,9 @@ def _normalize_fine_tuning_job_dict(data: Dict[str, Any]) -> Dict[str, Any]: - result_files: null → [] - status: "pending" → "queued" """ + if not is_azure: + return data + normalized = data.copy() if normalized.get("organization_id") is None: @@ -30,9 +35,11 @@ def _normalize_fine_tuning_job_dict(data: Dict[str, Any]) -> Dict[str, Any]: return normalized -def _litellm_fine_tuning_job_from_response(response: Any) -> LiteLLMFineTuningJob: +def _litellm_fine_tuning_job_from_response( + response: Any, is_azure: bool = False +) -> LiteLLMFineTuningJob: return LiteLLMFineTuningJob( - **_normalize_fine_tuning_job_dict(response.model_dump()) + **_normalize_fine_tuning_job_dict(response.model_dump(), is_azure=is_azure) ) diff --git a/tests/batches_tests/test_fine_tuning_api.py b/tests/batches_tests/test_fine_tuning_api.py index b7ff8c7fae7..20867234e53 100644 --- a/tests/batches_tests/test_fine_tuning_api.py +++ b/tests/batches_tests/test_fine_tuning_api.py @@ -182,30 +182,17 @@ async def test_azure_create_fine_tune_jobs_async(): def test_azure_trainingtype_defaults_to_one(): """ - Azure requires trainingType in extra_body. When omitted, LiteLLM defaults it to 1. + Azure requires trainingType in extra_body. When omitted, AzureOpenAIFineTuningAPI defaults it to 1. """ - from unittest.mock import MagicMock, patch - from litellm.fine_tuning.main import create_fine_tuning_job + from litellm.llms.azure.fine_tuning.handler import AzureOpenAIFineTuningAPI - with patch( - "litellm.fine_tuning.main.azure_fine_tuning_apis_instance.create_fine_tuning_job" - ) as mock_create: - mock_create.return_value = MagicMock( - id="ftjob-test", status="queued", model="gpt-4o-mini" - ) + handler = AzureOpenAIFineTuningAPI() + create_data = {"model": "gpt-4o-mini", "training_file": "file-test"} - create_fine_tuning_job( - model="gpt-4o-mini", - training_file="file-test", - custom_llm_provider="azure", - api_base="https://test.openai.azure.com", - api_key="test-key", - ) + handler._ensure_training_type(create_data) - call_kwargs = mock_create.call_args[1] - create_data = call_kwargs["create_fine_tuning_job_data"] - assert "extra_body" in create_data - assert create_data["extra_body"].get("trainingType") == 1 + assert "extra_body" in create_data + assert create_data["extra_body"]["trainingType"] == 1 @pytest.mark.asyncio() diff --git a/tests/test_litellm/types/llms/test_types_llms_openai.py b/tests/test_litellm/types/llms/test_types_llms_openai.py index 384d0dd73d7..323eb5a9424 100644 --- a/tests/test_litellm/types/llms/test_types_llms_openai.py +++ b/tests/test_litellm/types/llms/test_types_llms_openai.py @@ -424,8 +424,17 @@ def test_normalize_fine_tuning_job_dict_maps_azure_pending(): from litellm.llms.openai.fine_tuning.handler import _normalize_fine_tuning_job_dict out = _normalize_fine_tuning_job_dict( - {"organization_id": None, "result_files": None, "status": "pending"} + {"organization_id": None, "result_files": None, "status": "pending"}, + is_azure=True, ) assert out["organization_id"] == "" assert out["result_files"] == [] assert out["status"] == "queued" + + +def test_normalize_fine_tuning_job_dict_openai_unchanged(): + from litellm.llms.openai.fine_tuning.handler import _normalize_fine_tuning_job_dict + + data = {"organization_id": None, "result_files": None, "status": "pending"} + out = _normalize_fine_tuning_job_dict(data, is_azure=False) + assert out is data