litellm/tests/unit/fine_tuning/test_main.py
yucheng-berri 6162184b1c
fix(managed_files): send provider file ids when creating a fine-tuning job from managed files (#46552)
* fix(managed_files): send provider file ids when creating a fine-tuning job from managed files

Fine-tuning job creation with a managed training file sent the gateway's managed id and the provider-prefixed deployment model to OpenAI, which rejected the job. The pre-call hook now builds the file id mapping for training_file and validation_file, the deployment hook swaps both, the SDK strips the provider prefix from the model, and the create response returns the caller's own file ids

* fix(managed_files): route managed fine-tuning creates only to deployments holding every file

Fine-tuning creates now narrow routing the same way managed batches do, so a
deployment added after the upload is never picked. Adds a two-worker chaos
integration test and runs tests/unit/fine_tuning in the endpoints shard.

* fix(managed_files): keep fine-tuning creates on the model group when no deployment holds the files

A managed file used on a model group it was never uploaded for kept failing
with the provider's 400 before the routing filter. The filter turned that into
a 429 no-deployments error, so fine-tuning now falls back to the whole group in
that case while batch input files keep routing only to deployments that hold
them.

* test(fine_tuning): cover the provider prefix strip for OpenAI and Azure creates through the proxy

* test(fine_tuning): start each multi-deployment case from an empty upstream log

* test(fine_tuning): boot both proxies of the late deployment cell on one scratch database

---------

Co-authored-by: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com>
2026-10-19 14:40:15 -07:00

54 lines
1.8 KiB
Python

import json
from typing import Final
import httpx
import pytest
import respx
import litellm
API_BASE: Final = "https://fine-tuning.test"
BASE_MODEL: Final = "gpt-4.1-nano-2025-04-14"
PROVIDER_JOB: Final = {
"id": "ftjob-abc123",
"object": "fine_tuning.job",
"created_at": 1700000000,
"error": None,
"fine_tuned_model": None,
"finished_at": None,
"hyperparameters": {"n_epochs": 3},
"model": BASE_MODEL,
"organization_id": "org-test",
"result_files": [],
"seed": 42,
"status": "validating_files",
"trained_tokens": None,
"training_file": "file-abc123",
"validation_file": None,
}
@pytest.mark.asyncio
@respx.mock
@pytest.mark.parametrize(
("custom_llm_provider", "route"),
[("openai", f"{API_BASE}/v1/fine_tuning/jobs"), ("azure", f"{API_BASE}/openai/fine_tuning/jobs")],
)
@pytest.mark.parametrize("prefixed", [True, False], ids=["provider-prefixed", "bare"])
async def test_create_sends_the_model_without_the_provider_prefix(
monkeypatch: pytest.MonkeyPatch, custom_llm_provider: str, route: str, prefixed: bool
) -> None:
sent: Final = respx.post(url__startswith=route).mock(return_value=httpx.Response(200, json=PROVIDER_JOB))
async with httpx.AsyncClient() as session:
monkeypatch.setattr(litellm, "aclient_session", session)
await litellm.acreate_fine_tuning_job(
model=f"{custom_llm_provider}/{BASE_MODEL}" if prefixed else BASE_MODEL,
training_file="file-abc123",
custom_llm_provider=custom_llm_provider,
api_key="sk-test",
api_base=f"{API_BASE}/v1" if custom_llm_provider == "openai" else API_BASE,
**({"api_version": "2024-10-21"} if custom_llm_provider == "azure" else {}),
)
assert json.loads(sent.calls.last.request.content)["model"] == BASE_MODEL