diff --git a/litellm/llms/azure/fine_tuning/handler.py b/litellm/llms/azure/fine_tuning/handler.py index 2415c7d78ed..7e225a84454 100644 --- a/litellm/llms/azure/fine_tuning/handler.py +++ b/litellm/llms/azure/fine_tuning/handler.py @@ -42,6 +42,26 @@ class AzureOpenAIFineTuningAPI(OpenAIFineTuningAPI, BaseAzureLLM): ) return _litellm_fine_tuning_job_from_response(response, is_azure=True) + async def acancel_fine_tuning_job( + self, + fine_tuning_job_id: str, + openai_client: Union[AsyncOpenAI, AsyncAzureOpenAI], + ) -> LiteLLMFineTuningJob: + response = await openai_client.fine_tuning.jobs.cancel( + fine_tuning_job_id=fine_tuning_job_id + ) + return _litellm_fine_tuning_job_from_response(response, is_azure=True) + + async def aretrieve_fine_tuning_job( + self, + fine_tuning_job_id: str, + openai_client: Union[AsyncOpenAI, AsyncAzureOpenAI], + ) -> LiteLLMFineTuningJob: + response = await openai_client.fine_tuning.jobs.retrieve( + fine_tuning_job_id=fine_tuning_job_id + ) + return _litellm_fine_tuning_job_from_response(response, is_azure=True) + def create_fine_tuning_job( self, _is_async: bool, @@ -93,6 +113,98 @@ class AzureOpenAIFineTuningAPI(OpenAIFineTuningAPI, BaseAzureLLM): ) return _litellm_fine_tuning_job_from_response(response, is_azure=True) + def cancel_fine_tuning_job( + self, + _is_async: bool, + fine_tuning_job_id: str, + 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]]: + 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.acancel_fine_tuning_job( + fine_tuning_job_id=fine_tuning_job_id, + openai_client=openai_client, + ) + + response = cast(OpenAI, openai_client).fine_tuning.jobs.cancel( + fine_tuning_job_id=fine_tuning_job_id + ) + return _litellm_fine_tuning_job_from_response(response, is_azure=True) + + def retrieve_fine_tuning_job( + self, + _is_async: bool, + fine_tuning_job_id: str, + 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]]: + 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.aretrieve_fine_tuning_job( + fine_tuning_job_id=fine_tuning_job_id, + openai_client=openai_client, + ) + + response = cast(OpenAI, openai_client).fine_tuning.jobs.retrieve( + fine_tuning_job_id=fine_tuning_job_id + ) + 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 a9fbd88d2a8..6800fe81d65 100644 --- a/litellm/llms/openai/fine_tuning/handler.py +++ b/litellm/llms/openai/fine_tuning/handler.py @@ -8,6 +8,12 @@ from litellm.types.utils import LiteLLMFineTuningJob _AZURE_STATUS_MAP = { "pending": "queued", + "notRunning": "queued", + "running": "running", + "succeeded": "succeeded", + "failed": "failed", + "canceled": "cancelled", + "canceling": "cancelled", } diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 5a80b40d61f..b9c8030c877 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -315,11 +315,11 @@ class OpenAIFileObject(BaseModel): `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. """ - status: Optional[Literal["uploaded", "processed", "error"]] = None + status: Optional[Literal["uploaded", "processed", "error", "pending"]] = None """Deprecated. - The current status of the file, which can be either `uploaded`, `processed`, or - `error`. + The current status of the file, which can be either `uploaded`, `processed`, + `error`, or `pending` (Azure may return `pending` immediately after upload). """ expires_at: Optional[int] = None diff --git a/tests/batches_tests/test_fine_tuning_api.py b/tests/batches_tests/test_fine_tuning_api.py index 20867234e53..3ab15306fcb 100644 --- a/tests/batches_tests/test_fine_tuning_api.py +++ b/tests/batches_tests/test_fine_tuning_api.py @@ -616,11 +616,11 @@ async def test_mock_openai_retrieve_fine_tune_job(): @pytest.mark.asyncio async def test_mock_azure_create_fine_tune_job_with_azure_specific_params(): """Test that Azure-specific parameters are passed through extra_body""" - from openai import AsyncAzureOpenAI from openai.types.fine_tuning.fine_tuning_job import FineTuningJob from openai.types.fine_tuning.fine_tuning_job import Hyperparameters as OAIHyperparameters + from litellm.types.utils import LiteLLMFineTuningJob - mock_response = FineTuningJob( + mock_response = LiteLLMFineTuningJob( id="ft-azure-123", model="gpt-4.1-mini-2025-04-14", created_at=1677610602, @@ -634,8 +634,11 @@ async def test_mock_azure_create_fine_tune_job_with_azure_specific_params(): result_files=[], ) + async def mock_async_create(*args, **kwargs): + return mock_response + with patch("litellm.llms.azure.fine_tuning.handler.AzureOpenAIFineTuningAPI.create_fine_tuning_job") as mock_create: - mock_create.return_value = mock_response + mock_create.return_value = mock_async_create() response = await litellm.acreate_fine_tuning_job( model="gpt-4.1-mini-2025-04-14", 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 323eb5a9424..569743269a5 100644 --- a/tests/test_litellm/types/llms/test_types_llms_openai.py +++ b/tests/test_litellm/types/llms/test_types_llms_openai.py @@ -438,3 +438,18 @@ def test_normalize_fine_tuning_job_dict_openai_unchanged(): data = {"organization_id": None, "result_files": None, "status": "pending"} out = _normalize_fine_tuning_job_dict(data, is_azure=False) assert out is data + + +def test_openai_file_object_accepts_pending_status(): + from litellm.types.llms.openai import OpenAIFileObject + + file_obj = OpenAIFileObject( + id="file-123", + bytes=1024, + created_at=1677610602, + filename="train.jsonl", + object="file", + purpose="fine-tune", + status="pending", + ) + assert file_obj.status == "pending"