feat(translation): openai_compat serialize_request mirroring the v1 five-touch passthrough

Body assembly = {model, messages, **params} with the original
max-tokens key re-emitted, recursive cache_control strip on tool
parameters (v1 filter_value_from_dict parity incl. the recursion cap),
and fail-closed gates: o-series/gpt-5 families, top_k/thinking/
reasoning_effort, the user param's model-list gate, and
response_format on gpt-4/gpt-3.5-turbo-16k.
This commit is contained in:
mateo-berri
2026-06-12 01:32:44 +00:00
parent 7a2d74785e
commit dfca8f81d6
2 changed files with 190 additions and 0 deletions
@@ -13,6 +13,12 @@ from __future__ import annotations
from ...ir import ChatRequest
_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
drop_params interplay."""
def unsupported_model_family(model: str) -> str | None:
base = model.split("/")[-1]
@@ -35,4 +41,20 @@ def unsupported_params(request: ChatRequest) -> str | None:
"reasoning_effort on a plain-GPT OpenAI model; "
"v1's get_optional_params raises or drops it"
)
if request.user.is_some():
return (
"user param is gated on litellm.open_ai_chat_completion_models "
"membership in v1 (gpt_transformation.py:177-187); v1 handles it"
)
return 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:
return (
f"response_format on {request.model}: outside v1's supported set "
"(gpt_transformation.py:172-175); v1 raises or drops it"
)
return None
@@ -0,0 +1,168 @@
"""Serialize the IR into an OpenAI ``/v1/chat/completions`` request body.
v1's ``OpenAIGPTConfig.map_openai_params`` + ``transform_request``
(llms/openai/chat/gpt_transformation.py:429-456) is a near-passthrough with
exactly five touches: string ``image_url`` -> object (+ litellm ``format``
strip), pdf-URL ``file`` inlining (sync I/O, fails closed before this module:
the inbound schema has no file part), ``cache_control`` stripped recursively
from messages and tools, ``max_retries`` popped (an SDK kwarg, never in the
IR), and body assembly ``{model, messages, **optional_params}``. The shapes
this serializer admits are the ones the raw guard proved round-trip
losslessly; o-series and gpt-5 models fail closed until their param families
(``OpenAIOSeriesConfig``/``OpenAIGPT5Config``) are ported.
"""
from __future__ import annotations
from expression import Error, Ok, Option, Result
from expression.collections import Block
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from typing_extensions import assert_never
from ...deps import TranslationDeps
from ...errors import TranslationError
from ...ir import Body, ChatRequest, PlainJson, ResponseFormat, ToolChoice, ToolDef
from . import params as p
from .messages import serialize_messages
_SerializeResult = Result[Body, TranslationError]
def serialize_request(request: ChatRequest, deps: TranslationDeps) -> _SerializeResult:
reason = (
p.unsupported_model_family(request.model)
or p.unsupported_params(request)
or p.unsupported_response_format(request)
)
if reason is not None:
return Error(TranslationError.of_unsupported(reason))
messages = serialize_messages(request)
if isinstance(messages, TranslationError):
return Error(messages)
body: Body = {
"model": request.model,
"messages": messages,
**_present(
stream=True if request.stream else None,
**_max_tokens_fields(request),
temperature=request.params.temperature.default_value(None),
top_p=request.params.top_p.default_value(None),
stop=list(request.params.stop) if len(request.params.stop) > 0 else None,
tools=_tools_json(request.tools),
tool_choice=_tool_choice_json(request.tool_choice),
parallel_tool_calls=request.parallel_tool_calls.default_value(None),
response_format=_response_format_json(request.response_format),
),
}
return Ok(body)
def _present(**fields: PlainJson | None) -> dict[str, PlainJson]:
return {key: value for key, value in fields.items() if value is not None}
def _max_tokens_fields(request: ChatRequest) -> dict[str, PlainJson | None]:
"""Re-emit the caller's original key; the raw guard rejects requests
carrying both, so ``max_completion_tokens`` being set means it was the
one sent (``max_tokens`` then only holds the collapsed copy)."""
match request.params.max_completion_tokens:
case Option(tag="some", some=value):
return {"max_completion_tokens": value}
case _:
return {"max_tokens": request.params.max_tokens.default_value(None)}
def _tools_json(tools: Block[ToolDef]) -> PlainJson | None:
if len(tools) == 0:
return None
return [_tool_json(tool) for tool in tools]
def _tool_json(tool: ToolDef) -> PlainJson:
function: dict[str, PlainJson] = {"name": tool.name}
match tool.description:
case Option(tag="some", some=description):
function = {**function, "description": description}
case _:
pass
match tool.parameters:
case Option(tag="some", some=parameters):
function = {**function, "parameters": _strip_cache(parameters.value, 0)}
case _:
pass
match tool.strict:
case Option(tag="some", some=strict):
function = {**function, "strict": strict}
case _:
pass
return {"type": "function", "function": function}
def _strip_cache(value: PlainJson, depth: int) -> PlainJson:
"""Mirror v1's ``filter_value_from_dict(tool, "cache_control")``: the key
is removed at every nesting level, with the same recursion cap."""
if depth > DEFAULT_MAX_RECURSE_DEPTH:
return value
if isinstance(value, dict):
return {
key: _strip_cache(item, depth + 1)
for key, item in value.items()
if key != "cache_control"
}
if isinstance(value, list):
return [_strip_cache(item, depth + 1) for item in value]
return value
def _tool_choice_json(choice_opt: Option[ToolChoice]) -> PlainJson | None:
match choice_opt:
case Option(tag="some", some=choice):
pass
case _:
return None
match choice.tag:
case "auto":
return "auto"
case "required":
return "required"
case "none":
return "none"
case "specific":
return {"type": "function", "function": {"name": choice.specific}}
assert_never(choice.tag)
def _response_format_json(
response_format_opt: Option[ResponseFormat],
) -> PlainJson | None:
match response_format_opt:
case Option(tag="some", some=response_format):
pass
case _:
return None
match response_format.tag:
case "text":
return {"type": "text"}
case "json_object":
return {"type": "json_object"}
case "json_schema":
spec = response_format.json_schema
inner: dict[str, PlainJson] = {}
match spec.name:
case Option(tag="some", some=name):
inner = {**inner, "name": name}
case _:
pass
match spec.description:
case Option(tag="some", some=description):
inner = {**inner, "description": description}
case _:
pass
inner = {**inner, "schema": spec.schema.value}
match spec.strict:
case Option(tag="some", some=strict):
inner = {**inner, "strict": strict}
case _:
pass
return {"type": "json_schema", "json_schema": inner}
assert_never(response_format.tag)