fix(router): arm the provider-scoped fallback pin only on resource-operating handlers

This commit is contained in:
mateo-berri 2026-08-29 01:46:33 -07:00
parent 2968c246b9
commit 8cf090b368
2 changed files with 101 additions and 6 deletions

View file

@ -253,6 +253,17 @@ def get_fallback_model_group(fallbacks: list[Any], model_group: str) -> tuple[li
PROVIDER_SCOPED_RESOURCE_KEYS: Final = ("input_file_id", "training_file", "batch_id", "file_id", "fine_tuning_job_id")
PROVIDER_SCOPED_RESOURCE_FUNCTION_NAMES: Final = frozenset(
{
"_acreate_batch",
"_acancel_batch",
"acreate_fine_tuning_job",
"acancel_fine_tuning_job",
"aretrieve_fine_tuning_job",
"afile_content",
"afile_delete",
}
)
PROVIDER_SCOPED_CREATION_FUNCTION_NAMES: Final = frozenset({"_acreate_file"})
@ -284,13 +295,23 @@ async def _is_fallback_target_authorized(
def references_provider_scoped_resource(kwargs: Mapping[str, object]) -> bool:
"""
True when the request names a file, batch, or fine-tuning job that only exists under
one provider's credentials.
True when a file, batch, or fine-tuning job operation names an id that only exists
under one provider's credentials.
Each of those ids lives in the account of the deployment that issued it. Handing it to
a different model group asks a provider about an id it never issued, which costs an
extra round trip that can only answer not-found.
extra round trip that can only answer not-found. Generic calls dispatched through
`Router._ageneric_api_call_with_fallbacks` carry the real handler in
`original_generic_function`, so both slots are checked. Gating on the handler name
keeps completion-style requests eligible for cross-group fallback even when a caller
passes a stray extra body field that happens to share one of these key names.
"""
handler_names: Final = tuple(
getattr(kwargs.get(function_key), "__name__", None)
for function_key in ("original_function", "original_generic_function")
)
if all(name not in PROVIDER_SCOPED_RESOURCE_FUNCTION_NAMES for name in handler_names):
return False
return any(kwargs.get(key) for key in PROVIDER_SCOPED_RESOURCE_KEYS)

View file

@ -180,6 +180,30 @@ async def _acreate_file(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
async def _acancel_batch(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
async def _acompletion(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
async def _ageneric_api_call_with_fallbacks_helper(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
async def acreate_fine_tuning_job(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
async def aretrieve_fine_tuning_job(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
async def afile_content(*args: object, **kwargs: object) -> NoReturn:
raise AssertionError("only used for its __name__")
@pytest.mark.asyncio
async def test_run_async_fallback_keeps_uploaded_file_requests_in_their_model_group():
"""An input_file_id only exists under the credentials of the group it was uploaded
@ -217,6 +241,8 @@ async def test_run_async_fallback_keeps_fine_tuning_requests_in_their_model_grou
fallback_depth=0,
model="openai-group",
training_file="file-owned-by-openai",
original_function=_ageneric_api_call_with_fallbacks_helper,
original_generic_function=acreate_fine_tuning_job,
)
assert router.attempted_model_groups == []
@ -300,10 +326,33 @@ async def test_run_async_fallback_still_crosses_model_groups_without_an_uploaded
@pytest.mark.asyncio
@pytest.mark.parametrize("resource_key", ["batch_id", "file_id", "fine_tuning_job_id"])
async def test_run_async_fallback_keeps_provider_scoped_ids_in_their_model_group(resource_key: str):
@pytest.mark.parametrize(
("resource_key", "handler_kwargs"),
[
("batch_id", {"original_function": _acancel_batch}),
(
"file_id",
{
"original_function": _ageneric_api_call_with_fallbacks_helper,
"original_generic_function": afile_content,
},
),
(
"fine_tuning_job_id",
{
"original_function": _ageneric_api_call_with_fallbacks_helper,
"original_generic_function": aretrieve_fine_tuning_job,
},
),
],
)
async def test_run_async_fallback_keeps_provider_scoped_ids_in_their_model_group(
resource_key: str, handler_kwargs: dict
):
"""A batch, file, or fine-tuning job id only exists under the credentials of the group
that issued it, so a cross-group fallback asks a provider about an id it never saw."""
that issued it, so a cross-group fallback asks a provider about an id it never saw.
Generic API calls carry the real handler in original_generic_function, so the pin
must recognize it there too."""
router = AttemptRecordingRouter()
with pytest.raises(RuntimeError, match="openai connection error"):
@ -316,11 +365,35 @@ async def test_run_async_fallback_keeps_provider_scoped_ids_in_their_model_group
fallback_depth=0,
model="openai-group",
**{resource_key: "owned-by-openai"},
**handler_kwargs,
)
assert router.attempted_model_groups == []
@pytest.mark.asyncio
@pytest.mark.parametrize("resource_key", ["batch_id", "file_id", "fine_tuning_job_id"])
async def test_run_async_fallback_ignores_stray_resource_ids_on_completion_calls(resource_key: str):
"""A caller-supplied top-level field like file_id on a chat completion is application
data, never a provider resource reference, so it must not cost the request its
cross-group fallbacks."""
router = AttemptRecordingRouter()
await run_async_fallback(
litellm_router=router,
fallback_model_group=["azure-group"],
original_model_group="openai-group",
original_exception=RuntimeError("openai connection error"),
max_fallbacks=3,
fallback_depth=0,
model="openai-group",
original_function=_acompletion,
**{resource_key: "caller-app-data"},
)
assert router.attempted_model_groups == ["azure-group"]
@pytest.mark.asyncio
async def test_run_async_fallback_allows_same_model_group_retry_for_batch_cancel():
router = AttemptRecordingRouter()
@ -334,6 +407,7 @@ async def test_run_async_fallback_allows_same_model_group_retry_for_batch_cancel
fallback_depth=0,
model="openai-group",
batch_id="owned-by-openai",
original_function=_acancel_batch,
)
assert router.attempted_model_groups == ["openai-group"]