diff --git a/litellm/translation/providers/compat_sdk/params.py b/litellm/translation/providers/compat_sdk/params.py index b3556ed306..93aba6acdd 100644 --- a/litellm/translation/providers/compat_sdk/params.py +++ b/litellm/translation/providers/compat_sdk/params.py @@ -27,7 +27,10 @@ from typing import Literal from ...deps import TranslationDeps from ...ir import ChatRequest -from ..openai_compat.params import unsupported_response_format +from ..openai_compat.params import ( + RESPONSE_FORMAT_UNSUPPORTED_MODELS, + unsupported_response_format, +) CompatSdkProvider = Literal[ "together_ai", @@ -147,11 +150,20 @@ def together_ai_unsupported(request: ChatRequest, deps: TranslationDeps) -> str base list unless ``supports_function_calling(model, "together_ai")`` is True (together_ai/chat.py); parallel_tool_calls stays supported either way (v1 truth, not an oversight here).""" - allowed = ( - _BASE_LIST - if supports_together_tools(request.model, deps) - else _BASE_LIST - _FUNCTION_CALLING_KEYS - ) + supports_tools = supports_together_tools(request.model, deps) + if request.model in RESPONSE_FORMAT_UNSUPPORTED_MODELS and not supports_tools: + # The base list already dropped response_format for this model name, + # so the non-fc fork's list.remove("response_format") crashes inside + # get_supported_openai_params -- which _check_valid_arg runs on EVERY + # together_ai request, plain text included (verifier-wave1a F2). + return ( + f"model {request.model!r} on together_ai without the model-map " + "supports_function_calling flag: v1's TogetherAIConfig." + "get_supported_openai_params raises ValueError (list.remove on " + "a base list that already dropped response_format for this " + "model name) on every request; v1 raises its own error" + ) + allowed = _BASE_LIST if supports_tools else _BASE_LIST - _FUNCTION_CALLING_KEYS return unsupported_against( request, provider="together_ai", diff --git a/litellm/translation/providers/openai_compat/params.py b/litellm/translation/providers/openai_compat/params.py index 5d3648d9ab..b62d47e50b 100644 --- a/litellm/translation/providers/openai_compat/params.py +++ b/litellm/translation/providers/openai_compat/params.py @@ -13,7 +13,7 @@ from __future__ import annotations from ...ir import ChatRequest -_RESPONSE_FORMAT_UNSUPPORTED_MODELS = ("gpt-4", "gpt-3.5-turbo-16k") +RESPONSE_FORMAT_UNSUPPORTED_MODELS = ("gpt-4", "gpt-3.5-turbo-16k") """v1's get_supported_openai_params excludes response_format for exactly these two model names (gpt_transformation.py:172-175) and then DROPS or RAISES on it in get_optional_params; fail closed instead of re-deriving the @@ -52,7 +52,7 @@ def unsupported_params(request: ChatRequest) -> str | None: def unsupported_response_format(request: ChatRequest) -> str | None: if request.response_format.is_none(): return None - if request.model in _RESPONSE_FORMAT_UNSUPPORTED_MODELS: + if request.model in RESPONSE_FORMAT_UNSUPPORTED_MODELS: return ( f"response_format on {request.model}: outside v1's supported set " "(gpt_transformation.py:172-175); v1 raises or drops it" diff --git a/tests/test_litellm/translation/generate_differential_report.py b/tests/test_litellm/translation/generate_differential_report.py index 14931166a8..1f802fda3b 100644 --- a/tests/test_litellm/translation/generate_differential_report.py +++ b/tests/test_litellm/translation/generate_differential_report.py @@ -297,6 +297,20 @@ def _compat_sdk_rows(lines: list) -> int: failures += 0 if ok else 1 label = "FALLBACK (v1 raises UnsupportedParamsError)" if ok else "DIVERGENT" lines.append(f"- {label}: {name} ({reason})") + for name in sorted( + k for k in req.V1_RAISES_VALUE_ERROR if k.startswith(f"{provider}:") + ): + p, case, reason = req.V1_RAISES_VALUE_ERROR[name] + result = req._v2(p, case) + try: + corpus.run_v1_request_transform(p, case) + raised = False + except ValueError as err: + raised = type(err) is ValueError + ok = result.is_error() and reason in result.error.summary and raised + failures += 0 if ok else 1 + label = "FALLBACK (v1 raises ValueError)" if ok else "DIVERGENT" + lines.append(f"- {label}: {name} ({reason})") for name in sorted( k for k in req.EXPECTED_FALLBACKS if k.startswith(f"{provider}:") ): diff --git a/tests/test_litellm/translation/test_differential_compat_sdk_request.py b/tests/test_litellm/translation/test_differential_compat_sdk_request.py index 7be595ee18..e7a51534bf 100644 --- a/tests/test_litellm/translation/test_differential_compat_sdk_request.py +++ b/tests/test_litellm/translation/test_differential_compat_sdk_request.py @@ -159,6 +159,40 @@ V1_RAISES.update( } ) +# Rows where v1 CRASHES with a bare ValueError BEFORE any param gate: +# together's non-fc fork runs list.remove("response_format") on a base list +# that already dropped the key for these model names, inside the +# get_supported_openai_params call _check_valid_arg makes on EVERY request +# (verifier-wave1a F2). v2 must fall back typed on every shape, plain text +# included, so v1 raises its own error instead of v2 serving what v1 crashes +# on. +V1_RAISES_VALUE_ERROR = { + "together_ai:plain_text_on_gpt4_name": ( + "together_ai", + {"model": "gpt-4", "messages": _USER}, + "ValueError", + ), + "together_ai:plain_text_on_gpt35_16k_name": ( + "together_ai", + {"model": "gpt-3.5-turbo-16k", "messages": _USER}, + "ValueError", + ), + "together_ai:temperature_only_on_gpt4_name": ( + "together_ai", + {"model": "gpt-4", "temperature": 0.2, "messages": _USER}, + "ValueError", + ), + "together_ai:response_format_on_gpt4_name": ( + "together_ai", + { + "model": "gpt-4", + "response_format": {"type": "json_object"}, + "messages": _USER, + }, + "ValueError", + ), +} + # Typed fallbacks where v1 SERVES the request (v1 is not invoked: the seam # routes these to v1 untouched). Shared raw-guard/parse shapes per provider # plus the provider-specific ones. @@ -296,6 +330,20 @@ def test_v1_raise_rows_fall_back_typed(name: str) -> None: run_v1_request_transform(provider, case) +@pytest.mark.parametrize("name", sorted(V1_RAISES_VALUE_ERROR)) +def test_v1_value_error_rows_fall_back_typed(name: str) -> None: + """The together gpt-4-name corner: v1 crashes with a bare ValueError on + EVERY request for these names (not UnsupportedParamsError), asserted + in-process; v2 serving any of them would be a parity break.""" + provider, case, reason_fragment = V1_RAISES_VALUE_ERROR[name] + result = _v2(provider, case) + assert result.is_error(), f"{name} unexpectedly translated: {result.ok!r}" + assert reason_fragment in result.error.summary, result.error.summary + with pytest.raises(ValueError) as excinfo: + run_v1_request_transform(provider, case) + assert type(excinfo.value) is ValueError, excinfo.value + + @pytest.mark.parametrize("name", sorted(EXPECTED_FALLBACKS)) def test_unsupported_shape_is_a_typed_fallback(name: str) -> None: provider, case, reason_fragment = EXPECTED_FALLBACKS[name]