fix: strip eager_input_streaming for non-Claude providers next to input_examples

This commit is contained in:
mateo-berri 2026-09-18 15:28:45 -07:00
parent 3d3a46fc1f
commit 1adbfbfbb1
2 changed files with 76 additions and 40 deletions

View file

@ -1146,37 +1146,35 @@ def responses_api_bridge_check(
return model_info, model
def _should_allow_input_examples(custom_llm_provider: str | None, model: str) -> bool:
_ANTHROPIC_ONLY_TOOL_KEYS: Final = frozenset({"input_examples", "eager_input_streaming"})
def _is_claude_tool_target(custom_llm_provider: str | None, model: str) -> bool:
if custom_llm_provider == "anthropic":
return True
if custom_llm_provider == "azure_ai" or custom_llm_provider == "bedrock" or custom_llm_provider == "vertex_ai":
return "claude" in model.lower()
model_lower: Final = model.lower()
if custom_llm_provider == "bedrock":
return "claude" in model_lower or ("arn:" in model_lower and ":bedrock:" in model_lower)
if custom_llm_provider == "azure_ai" or custom_llm_provider == "vertex_ai":
return "claude" in model_lower
return False
def _drop_input_examples_from_tool(tool: dict) -> dict:
tool_copy: Final = tool.copy()
tool_copy.pop("input_examples", None)
function = tool_copy.get("function")
if isinstance(function, dict):
function = function.copy()
function.pop("input_examples", None)
tool_copy["function"] = function
return tool_copy
def _without_anthropic_only_tool_keys(tool: dict) -> dict:
kept: Final = {key: value for key, value in tool.items() if key not in _ANTHROPIC_ONLY_TOOL_KEYS}
function: Final = tool.get("function")
if not isinstance(function, dict):
return kept
return {
**kept,
"function": {key: value for key, value in function.items() if key not in _ANTHROPIC_ONLY_TOOL_KEYS},
}
def _drop_input_examples_from_tools(
tools: list[dict] | None,
) -> list[dict] | None:
def _drop_anthropic_only_tool_keys(tools: list[dict] | None) -> list[dict] | None:
if tools is None:
return None
cleaned_tools: Final[list[dict]] = []
for tool in tools:
if isinstance(tool, dict):
cleaned_tools.append(_drop_input_examples_from_tool(tool))
else:
cleaned_tools.append(tool)
return cleaned_tools
return [_without_anthropic_only_tool_keys(tool) if isinstance(tool, dict) else tool for tool in tools]
class _ProxyAuthHeadersProvider(Protocol):
@ -5360,8 +5358,8 @@ def completion(
api_base=api_base,
)
if not _should_allow_input_examples(custom_llm_provider=custom_llm_provider, model=model):
tools = _drop_input_examples_from_tools(tools=tools)
if not _is_claude_tool_target(custom_llm_provider=custom_llm_provider, model=model):
tools = _drop_anthropic_only_tool_keys(tools=tools)
if provider_specific_header is not None:
headers.update(

View file

@ -349,28 +349,66 @@ def test_bedrock_latency_optimized_inference():
assert json_data["performanceConfig"]["latency"] == "optimized"
def test_strip_input_examples_for_non_anthropic_providers():
@pytest.mark.parametrize(
("custom_llm_provider", "model", "expected"),
[
("anthropic", "claude-sonnet-5", True),
("bedrock", "us.anthropic.claude-sonnet-5-20260501-v1:0", True),
("bedrock", "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123", True),
("bedrock", "us.amazon.nova-2-lite-v1:0", False),
("vertex_ai", "claude-sonnet-5", True),
("vertex_ai", "gemini-3.8-flash", False),
("azure_ai", "claude-sonnet-4-6", True),
("azure_ai", "gpt-5.6", False),
("openai", "gpt-5.6", False),
("gemini", "gemini-3.8-flash", False),
],
)
def test_is_claude_tool_target(custom_llm_provider: str, model: str, expected: bool):
assert litellm_main._is_claude_tool_target(custom_llm_provider=custom_llm_provider, model=model) is expected
@pytest.mark.parametrize("key", ["input_examples", "eager_input_streaming"])
def test_drop_anthropic_only_tool_keys_strips_tool_and_function_levels(key: str):
tools = [
{
"type": "function",
"name": "example_tool",
"input_examples": [{"foo": "bar"}],
"function": {
"name": "example_tool",
"input_examples": [{"foo": "bar"}],
},
}
{"type": "function", "name": "example_tool", key: True, "function": {"name": "example_tool", key: True}},
"opaque_tool",
]
assert not litellm_main._should_allow_input_examples(
custom_llm_provider="openai", model="gpt-4o-mini"
cleaned = litellm_main._drop_anthropic_only_tool_keys(tools=tools)
assert cleaned == [
{"type": "function", "name": "example_tool", "function": {"name": "example_tool"}},
"opaque_tool",
]
assert tools[0][key] is True
assert tools[0]["function"][key] is True
def test_completion_strips_eager_input_streaming_before_openai(respx_mock: respx.MockRouter, openai_api_response):
api_base: Final = "http://localhost:12346/v1"
mock_route: Final = respx_mock.post(url__regex=rf"{api_base}/chat/completions.*").mock(
return_value=httpx.Response(status_code=200, json=openai_api_response)
)
cleaned = litellm_main._drop_input_examples_from_tools(tools=tools)
litellm.completion(
model="openai/gpt-5.6",
messages=[{"role": "user", "content": "Write the file"}],
tools=[
{
"type": "function",
"function": {"name": "write_file", "parameters": {"type": "object", "properties": {}}},
"eager_input_streaming": True,
}
],
api_base=api_base,
api_key="fake_openai_api_key",
)
assert isinstance(cleaned, list)
assert "input_examples" not in cleaned[0]
assert "input_examples" not in cleaned[0]["function"]
assert mock_route.called
sent_tool: Final = json.loads(respx_mock.calls[0].request.content)["tools"][0]
assert "eager_input_streaming" not in sent_tool
assert sent_tool["function"]["name"] == "write_file"
def test_custom_provider_with_extra_headers():