fix(translation): close the together_ai gpt-4-name parity break (verifier-wave1a F2)

v1's TogetherAIConfig.get_supported_openai_params crashes with a bare
ValueError (list.remove) for any model literally named gpt-4 or
gpt-3.5-turbo-16k without the model-map supports_function_calling flag,
and it runs inside get_optional_params on EVERY request -- so v2 serving a
plain-text request on those names was a flag-on parity break. The together
gate now falls back typed on the name+non-fc combination before any param
check, with the reason naming the v1 ValueError mechanism (this also fixes
the wrong-exception-class reason the old rf-name fallback carried for
these rows). Four differential rows assert the v1 ValueError in-process
(exact class, not an UnsupportedParamsError subclass) across the orderings
the verifier measured: plain text on both names, temperature-only, and
response_format; the report generator emits them as
'FALLBACK (v1 raises ValueError)'.
This commit is contained in:
mateo-berri
2026-06-12 10:40:11 +00:00
parent fa9e015a19
commit 64aefbf9df
4 changed files with 82 additions and 8 deletions
@@ -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",
@@ -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"
@@ -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}:")
):
@@ -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]