Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/fine_tuning_endpoints/endpoints.py: 25%
206 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1#########################################################################
3# /v1/fine_tuning Endpoints
5# Equivalent of https://platform.openai.com/docs/api-reference/fine-tuning
6##########################################################################
8import asyncio
9from typing import Final, cast
11from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
13import litellm
14from litellm._logging import verbose_proxy_logger
15from litellm.proxy._types import *
16from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
17from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
18from litellm.proxy.openai_files_endpoints.common_utils import (
19 _is_base64_encoded_unified_file_id,
20 validate_managed_id_requirement,
21)
22from litellm.proxy.utils import handle_exception_on_proxy
23from litellm.types.utils import LiteLLMFineTuningJob
25router: Final = APIRouter()
27from litellm.types.llms.openai import LiteLLMFineTuningJobCreate
29fine_tuning_config = None
32def set_fine_tuning_config(config):
33 if config is None: 33 ↛ 37line 33 didn't jump to line 37 because the condition on line 33 was always true
34 return
36 global fine_tuning_config
37 if not isinstance(config, list):
38 raise ValueError("invalid fine_tuning config, expected a list is not a list")
40 for element in config:
41 if isinstance(element, dict):
42 for key, value in element.items():
43 if isinstance(value, str) and value.startswith("os.environ/"):
44 element[key] = litellm.get_secret(value)
46 fine_tuning_config = config
49# Function to search for specific custom_llm_provider and return its configuration
50def get_fine_tuning_provider_config(
51 custom_llm_provider: str,
52):
53 global fine_tuning_config
54 if fine_tuning_config is None:
55 raise ValueError("fine_tuning_config is not set, set it on your config.yaml file.")
56 for setting in fine_tuning_config:
57 if setting.get("custom_llm_provider") == custom_llm_provider:
58 return setting
59 return None
62@router.post(
63 "/v1/fine_tuning/jobs",
64 dependencies=[Depends(user_api_key_auth)],
65 tags=["fine-tuning"],
66 summary="✨ (Enterprise) Create Fine-Tuning Job",
67)
68@router.post(
69 "/fine_tuning/jobs",
70 dependencies=[Depends(user_api_key_auth)],
71 tags=["fine-tuning"],
72 summary="✨ (Enterprise) Create Fine-Tuning Job",
73)
74async def create_fine_tuning_job(
75 request: Request,
76 fastapi_response: Response,
77 fine_tuning_request: LiteLLMFineTuningJobCreate,
78 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
79):
80 """
81 Creates a fine-tuning job which begins the process of creating a new model from a given dataset.
82 This is the equivalent of POST https://api.openai.com/v1/fine_tuning/jobs
84 Supports Identical Params as: https://platform.openai.com/docs/api-reference/fine-tuning/create
86 Example Curl:
87 ```
88 curl http://localhost:4000/v1/fine_tuning/jobs \
89 -H "Content-Type: application/json" \
90 -H "Authorization: Bearer sk-1234" \
91 -d '{
92 "model": "gpt-3.5-turbo",
93 "training_file": "file-abc123",
94 "hyperparameters": {
95 "n_epochs": 4
96 }
97 }'
98 ```
99 """
100 from litellm.proxy.proxy_server import (
101 general_settings,
102 llm_router,
103 premium_user,
104 proxy_config,
105 proxy_logging_obj,
106 version,
107 )
109 data = fine_tuning_request.model_dump(exclude_none=True)
110 try:
111 if premium_user is not True: 111 ↛ 115line 111 didn't jump to line 115 because the condition on line 111 was always true
112 raise ValueError(f"Only premium users can use this endpoint + {CommonProxyErrors.not_premium_user.value}")
113 # Convert Pydantic model to dict
115 verbose_proxy_logger.debug(
116 "Request received by LiteLLM:\n%s",
117 json.dumps(data, indent=4),
118 )
120 # Include original request and headers in the data
121 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
122 (
123 data,
124 litellm_logging_obj,
125 ) = await base_llm_response_processor.common_processing_pre_call_logic(
126 request=request,
127 general_settings=general_settings,
128 user_api_key_dict=user_api_key_dict,
129 version=version,
130 proxy_logging_obj=proxy_logging_obj,
131 proxy_config=proxy_config,
132 route_type="acreate_fine_tuning_job",
133 )
135 ## CHECK IF MANAGED FILE ID
136 unified_file_id: str | Literal[False] = False
137 training_file: Final = fine_tuning_request.training_file
138 await validate_managed_id_requirement(
139 resource_id=training_file,
140 resource_kind="file",
141 user_api_key_dict=user_api_key_dict,
142 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
143 )
144 await validate_managed_id_requirement(
145 resource_id=fine_tuning_request.validation_file,
146 resource_kind="file",
147 user_api_key_dict=user_api_key_dict,
148 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
149 )
150 response: LiteLLMFineTuningJob | None = None
151 if training_file:
152 unified_file_id = _is_base64_encoded_unified_file_id(training_file)
153 ## IF SO, Route based on that
154 if unified_file_id:
155 """ """
156 if llm_router is None:
157 raise HTTPException(
158 status_code=500,
159 detail={"error": "LLM Router not initialized. Ensure models added to proxy."},
160 )
162 response = cast(LiteLLMFineTuningJob, await llm_router.acreate_fine_tuning_job(**data))
163 response.training_file = unified_file_id
164 response._hidden_params["unified_file_id"] = unified_file_id
165 ## ELSE, Route based on custom_llm_provider
166 elif fine_tuning_request.custom_llm_provider:
167 # get configs for custom_llm_provider
168 llm_provider_config: Final = get_fine_tuning_provider_config(
169 custom_llm_provider=fine_tuning_request.custom_llm_provider,
170 )
171 # add llm_provider_config to data
172 if llm_provider_config is not None:
173 data.update(llm_provider_config)
175 response = await litellm.acreate_fine_tuning_job(**data)
177 if response is None:
178 raise ValueError("Invalid request, No litellm managed file id or custom_llm_provider provided.")
180 ### CALL HOOKS ### - modify outgoing data
181 _response: Final = await proxy_logging_obj.post_call_success_hook(
182 data=data,
183 user_api_key_dict=user_api_key_dict,
184 response=response,
185 )
186 if _response is not None and isinstance(_response, LiteLLMFineTuningJob):
187 response = _response
189 ### ALERTING ###
190 asyncio.create_task(
191 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
192 )
194 ### RESPONSE HEADERS ###
195 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
196 model_id: Final = hidden_params.get("model_id", None) or ""
197 cache_key: Final = hidden_params.get("cache_key", None) or ""
198 api_base: Final = hidden_params.get("api_base", None) or ""
200 fastapi_response.headers.update(
201 ProxyBaseLLMRequestProcessing.get_custom_headers(
202 user_api_key_dict=user_api_key_dict,
203 model_id=model_id,
204 cache_key=cache_key,
205 api_base=api_base,
206 version=version,
207 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
208 )
209 )
211 return response
212 except Exception as e:
213 await proxy_logging_obj.post_call_failure_hook(
214 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
215 )
216 verbose_proxy_logger.exception(
217 "litellm.proxy.proxy_server.create_fine_tuning_job(): Exception occurred - %s", e
218 )
219 raise handle_exception_on_proxy(e)
222@router.get(
223 "/v1/fine_tuning/jobs/{fine_tuning_job_id:path}",
224 dependencies=[Depends(user_api_key_auth)],
225 tags=["fine-tuning"],
226 summary="✨ (Enterprise) Retrieve Fine-Tuning Job",
227)
228@router.get(
229 "/fine_tuning/jobs/{fine_tuning_job_id:path}",
230 dependencies=[Depends(user_api_key_auth)],
231 tags=["fine-tuning"],
232 summary="✨ (Enterprise) Retrieve Fine-Tuning Job",
233)
234async def retrieve_fine_tuning_job(
235 request: Request,
236 fastapi_response: Response,
237 fine_tuning_job_id: str,
238 custom_llm_provider: Literal["openai", "azure"] | None = None,
239 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
240):
241 """
242 Retrieves a fine-tuning job.
243 This is the equivalent of GET https://api.openai.com/v1/fine_tuning/jobs/{fine_tuning_job_id}
245 Supported Query Params:
246 - `custom_llm_provider`: Name of the LiteLLM provider
247 - `fine_tuning_job_id`: The ID of the fine-tuning job to retrieve.
248 """
249 from litellm.proxy.proxy_server import (
250 general_settings,
251 llm_router,
252 premium_user,
253 proxy_config,
254 proxy_logging_obj,
255 version,
256 )
258 data: dict = {"fine_tuning_job_id": fine_tuning_job_id}
259 try:
260 if premium_user is not True: 260 ↛ 262line 260 didn't jump to line 262 because the condition on line 260 was always true
261 raise ValueError(f"Only premium users can use this endpoint + {CommonProxyErrors.not_premium_user.value}")
262 await validate_managed_id_requirement(
263 resource_id=fine_tuning_job_id,
264 resource_kind="fine-tuning job",
265 user_api_key_dict=user_api_key_dict,
266 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
267 )
268 # Include original request and headers in the data
269 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
270 (
271 data,
272 litellm_logging_obj,
273 ) = await base_llm_response_processor.common_processing_pre_call_logic(
274 request=request,
275 general_settings=general_settings,
276 user_api_key_dict=user_api_key_dict,
277 version=version,
278 proxy_logging_obj=proxy_logging_obj,
279 proxy_config=proxy_config,
280 route_type=CallTypes.aretrieve_fine_tuning_job.value,
281 )
283 try:
284 request_body = await request.json()
285 except Exception:
286 request_body = {}
288 custom_llm_provider = request_body.get("custom_llm_provider", None) or custom_llm_provider
290 ## CHECK IF MANAGED FILE ID
291 unified_finetuning_job_id: str | Literal[False] = False
292 response: LiteLLMFineTuningJob | None = None
293 if fine_tuning_job_id:
294 unified_finetuning_job_id = _is_base64_encoded_unified_file_id(fine_tuning_job_id)
295 if unified_finetuning_job_id:
296 if llm_router is None:
297 raise HTTPException(
298 status_code=500,
299 detail={"error": "LLM Router not initialized. Ensure models added to proxy."},
300 )
301 response = cast(
302 LiteLLMFineTuningJob,
303 await llm_router.aretrieve_fine_tuning_job(
304 **data,
305 ),
306 )
307 response._hidden_params["unified_finetuning_job_id"] = unified_finetuning_job_id
308 elif custom_llm_provider:
309 # get configs for custom_llm_provider
310 llm_provider_config: Final = get_fine_tuning_provider_config(custom_llm_provider=custom_llm_provider)
312 if llm_provider_config is not None:
313 data.update(llm_provider_config)
315 response = await litellm.aretrieve_fine_tuning_job(
316 **data,
317 )
319 if response is None:
320 raise HTTPException(
321 status_code=400,
322 detail="Invalid request, No litellm managed file id or custom_llm_provider provided.",
323 )
325 ### CALL HOOKS ### - modify outgoing data
326 _response: Final = await proxy_logging_obj.post_call_success_hook(
327 data=data,
328 user_api_key_dict=user_api_key_dict,
329 response=response,
330 )
331 if _response is not None and isinstance(_response, LiteLLMFineTuningJob):
332 response = _response
334 ### ALERTING ###
335 asyncio.create_task(
336 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
337 )
339 ### RESPONSE HEADERS ###
340 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
341 model_id: Final = hidden_params.get("model_id", None) or ""
342 cache_key: Final = hidden_params.get("cache_key", None) or ""
343 api_base: Final = hidden_params.get("api_base", None) or ""
345 fastapi_response.headers.update(
346 ProxyBaseLLMRequestProcessing.get_custom_headers(
347 user_api_key_dict=user_api_key_dict,
348 model_id=model_id,
349 cache_key=cache_key,
350 api_base=api_base,
351 version=version,
352 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
353 )
354 )
356 return response
358 except Exception as e:
359 await proxy_logging_obj.post_call_failure_hook(
360 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
361 )
362 verbose_proxy_logger.exception(
363 "litellm.proxy.proxy_server.retrieve_fine_tuning_job(): Exception occurred - %s", e
364 )
365 raise handle_exception_on_proxy(e)
368@router.get(
369 "/v1/fine_tuning/jobs",
370 dependencies=[Depends(user_api_key_auth)],
371 tags=["fine-tuning"],
372 summary="✨ (Enterprise) List Fine-Tuning Jobs",
373)
374@router.get(
375 "/fine_tuning/jobs",
376 dependencies=[Depends(user_api_key_auth)],
377 tags=["fine-tuning"],
378 summary="✨ (Enterprise) List Fine-Tuning Jobs",
379)
380async def list_fine_tuning_jobs(
381 request: Request,
382 fastapi_response: Response,
383 custom_llm_provider: Literal["openai", "azure"] | None = None,
384 target_model_names: str | None = Query(
385 default=None,
386 description="Comma separated list of model names to filter by. Example: 'gpt-4o,gpt-4o-mini'",
387 ),
388 after: str | None = None,
389 limit: int | None = None,
390 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
391):
392 """
393 Lists fine-tuning jobs for the organization.
394 This is the equivalent of GET https://api.openai.com/v1/fine_tuning/jobs
396 Supported Query Params:
397 - `custom_llm_provider`: Name of the LiteLLM provider
398 - `after`: Identifier for the last job from the previous pagination request.
399 - `limit`: Number of fine-tuning jobs to retrieve (default is 20).
400 """
401 from litellm.proxy.proxy_server import (
402 general_settings,
403 llm_router,
404 premium_user,
405 proxy_config,
406 proxy_logging_obj,
407 version,
408 )
410 data: dict = {}
411 try:
412 if premium_user is not True: 412 ↛ 415line 412 didn't jump to line 415 because the condition on line 412 was always true
413 raise ValueError(f"Only premium users can use this endpoint + {CommonProxyErrors.not_premium_user.value}")
414 # Include original request and headers in the data
415 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
416 (
417 data,
418 litellm_logging_obj,
419 ) = await base_llm_response_processor.common_processing_pre_call_logic(
420 request=request,
421 general_settings=general_settings,
422 user_api_key_dict=user_api_key_dict,
423 version=version,
424 proxy_logging_obj=proxy_logging_obj,
425 proxy_config=proxy_config,
426 route_type=CallTypes.alist_fine_tuning_jobs.value,
427 )
429 response: Any | None = None
430 if target_model_names and isinstance(target_model_names, str):
431 target_model_names_list: Final = target_model_names.split(",")
432 if len(target_model_names_list) != 1:
433 raise HTTPException(
434 status_code=400,
435 detail="target_model_names on list fine-tuning jobs must be a list of one model name. Example: ['gpt-4o']",
436 )
437 ## Use router to list fine-tuning jobs for that model
438 if llm_router is None:
439 raise HTTPException(
440 status_code=500,
441 detail="LLM Router not initialized. Ensure models added to proxy.",
442 )
443 data["model"] = target_model_names_list[0]
444 response = await llm_router.alist_fine_tuning_jobs(
445 **data,
446 after=after,
447 limit=limit,
448 )
449 return response
450 elif custom_llm_provider:
451 # get configs for custom_llm_provider
452 llm_provider_config: Final = get_fine_tuning_provider_config(custom_llm_provider=custom_llm_provider)
454 if llm_provider_config is not None:
455 data.update(llm_provider_config)
457 response = await litellm.alist_fine_tuning_jobs(
458 **data,
459 after=after,
460 limit=limit,
461 )
462 if response is None:
463 raise HTTPException(
464 status_code=400,
465 detail="Invalid request, No litellm managed file id or custom_llm_provider provided.",
466 )
468 ### RESPONSE HEADERS ###
469 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
470 model_id: Final = hidden_params.get("model_id", None) or ""
471 cache_key: Final = hidden_params.get("cache_key", None) or ""
472 api_base: Final = hidden_params.get("api_base", None) or ""
474 fastapi_response.headers.update(
475 ProxyBaseLLMRequestProcessing.get_custom_headers(
476 user_api_key_dict=user_api_key_dict,
477 model_id=model_id,
478 cache_key=cache_key,
479 api_base=api_base,
480 version=version,
481 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
482 )
483 )
485 return response
487 except Exception as e:
488 await proxy_logging_obj.post_call_failure_hook(
489 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
490 )
491 verbose_proxy_logger.exception("litellm.proxy.proxy_server.list_fine_tuning_jobs(): Exception occurred - %s", e)
492 raise handle_exception_on_proxy(e)
495@router.post(
496 "/v1/fine_tuning/jobs/{fine_tuning_job_id:path}/cancel",
497 dependencies=[Depends(user_api_key_auth)],
498 tags=["fine-tuning"],
499 summary="✨ (Enterprise) Cancel Fine-Tuning Jobs",
500)
501@router.post(
502 "/fine_tuning/jobs/{fine_tuning_job_id:path}/cancel",
503 dependencies=[Depends(user_api_key_auth)],
504 tags=["fine-tuning"],
505 summary="✨ (Enterprise) Cancel Fine-Tuning Jobs",
506)
507async def cancel_fine_tuning_job(
508 request: Request,
509 fastapi_response: Response,
510 fine_tuning_job_id: str,
511 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
512):
513 """
514 Cancel a fine-tuning job.
516 This is the equivalent of POST https://api.openai.com/v1/fine_tuning/jobs/{fine_tuning_job_id}/cancel
518 Supported Query Params:
519 - `custom_llm_provider`: Name of the LiteLLM provider
520 - `fine_tuning_job_id`: The ID of the fine-tuning job to cancel.
521 """
522 from litellm.proxy.proxy_server import (
523 general_settings,
524 llm_router,
525 premium_user,
526 proxy_config,
527 proxy_logging_obj,
528 version,
529 )
531 data: dict = {"fine_tuning_job_id": fine_tuning_job_id}
532 try:
533 if premium_user is not True: 533 ↛ 535line 533 didn't jump to line 535 because the condition on line 533 was always true
534 raise ValueError(f"Only premium users can use this endpoint + {CommonProxyErrors.not_premium_user.value}")
535 await validate_managed_id_requirement(
536 resource_id=fine_tuning_job_id,
537 resource_kind="fine-tuning job",
538 user_api_key_dict=user_api_key_dict,
539 managed_files_obj=proxy_logging_obj.get_proxy_hook("managed_files"),
540 )
541 # Include original request and headers in the data
542 base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
543 (
544 data,
545 litellm_logging_obj,
546 ) = await base_llm_response_processor.common_processing_pre_call_logic(
547 request=request,
548 general_settings=general_settings,
549 user_api_key_dict=user_api_key_dict,
550 version=version,
551 proxy_logging_obj=proxy_logging_obj,
552 proxy_config=proxy_config,
553 route_type=CallTypes.acancel_fine_tuning_job.value,
554 )
556 try:
557 request_body = await request.json()
558 except Exception:
559 request_body = {}
561 custom_llm_provider: Final = request_body.get("custom_llm_provider", None)
563 ## CHECK IF MANAGED FILE ID
564 unified_finetuning_job_id: str | Literal[False] = False
565 response: LiteLLMFineTuningJob | None = None
566 if fine_tuning_job_id:
567 unified_finetuning_job_id = _is_base64_encoded_unified_file_id(fine_tuning_job_id)
568 if unified_finetuning_job_id:
569 if llm_router is None:
570 raise HTTPException(
571 status_code=500,
572 detail={"error": "LLM Router not initialized. Ensure models added to proxy."},
573 )
574 response = cast(
575 LiteLLMFineTuningJob,
576 await llm_router.acancel_fine_tuning_job(
577 **data,
578 ),
579 )
580 response._hidden_params["unified_finetuning_job_id"] = unified_finetuning_job_id
581 else:
582 # get configs for custom_llm_provider
583 llm_provider_config: Final = get_fine_tuning_provider_config(custom_llm_provider=custom_llm_provider)
585 if llm_provider_config is not None:
586 data.update(llm_provider_config)
588 response = await litellm.acancel_fine_tuning_job(
589 **data,
590 )
592 if response is None:
593 raise HTTPException(
594 status_code=400,
595 detail="Invalid request, No litellm managed file id or custom_llm_provider provided.",
596 )
598 ### CALL HOOKS ### - modify outgoing data
599 _response: Final = await proxy_logging_obj.post_call_success_hook(
600 data=data,
601 user_api_key_dict=user_api_key_dict,
602 response=response,
603 )
604 if _response is not None and isinstance(_response, LiteLLMFineTuningJob):
605 response = _response
607 ### ALERTING ###
608 asyncio.create_task(
609 proxy_logging_obj.update_request_status(litellm_call_id=data.get("litellm_call_id", ""), status="success")
610 )
612 ### RESPONSE HEADERS ###
613 hidden_params: Final = getattr(response, "_hidden_params", {}) or {}
614 model_id: Final = hidden_params.get("model_id", None) or ""
615 cache_key: Final = hidden_params.get("cache_key", None) or ""
616 api_base: Final = hidden_params.get("api_base", None) or ""
618 fastapi_response.headers.update(
619 ProxyBaseLLMRequestProcessing.get_custom_headers(
620 user_api_key_dict=user_api_key_dict,
621 model_id=model_id,
622 cache_key=cache_key,
623 api_base=api_base,
624 version=version,
625 model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
626 )
627 )
629 return response
631 except Exception as e:
632 await proxy_logging_obj.post_call_failure_hook(
633 user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
634 )
635 verbose_proxy_logger.exception(
636 "litellm.proxy.proxy_server.cancel_fine_tuning_job(): Exception occurred - %s", e
637 )
638 raise handle_exception_on_proxy(e)