chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import logging
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
from litellm.types.utils import Choices, Message, ModelResponse, Usage
|
||||
|
||||
from agents.extensions.models.litellm_model import LitellmModel
|
||||
from agents.model_settings import ModelSettings
|
||||
from agents.models.interface import ModelTracing
|
||||
|
||||
|
||||
@pytest.mark.allow_call_model_methods
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_body_is_forwarded(monkeypatch):
|
||||
"""
|
||||
Forward `extra_body` via LiteLLM's dedicated kwarg.
|
||||
|
||||
This ensures that provider-specific request fields stay nested under `extra_body`
|
||||
so LiteLLM can merge them into the upstream request body itself.
|
||||
"""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_acompletion(model, messages=None, **kwargs):
|
||||
captured.update(kwargs)
|
||||
msg = Message(role="assistant", content="ok")
|
||||
choice = Choices(index=0, message=msg)
|
||||
return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
settings = ModelSettings(
|
||||
temperature=0.1, extra_body={"cached_content": "some_cache", "foo": 123}
|
||||
)
|
||||
model = LitellmModel(model="test-model")
|
||||
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input=[],
|
||||
model_settings=settings,
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
)
|
||||
|
||||
assert captured["extra_body"] == {"cached_content": "some_cache", "foo": 123}
|
||||
assert "cached_content" not in captured
|
||||
assert "foo" not in captured
|
||||
|
||||
|
||||
@pytest.mark.allow_call_model_methods
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_body_reasoning_effort_is_promoted(monkeypatch):
|
||||
"""
|
||||
Ensure reasoning_effort from extra_body is promoted to the top-level parameter.
|
||||
"""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_acompletion(model, messages=None, **kwargs):
|
||||
captured.update(kwargs)
|
||||
msg = Message(role="assistant", content="ok")
|
||||
choice = Choices(index=0, message=msg)
|
||||
return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
# GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764.
|
||||
settings = ModelSettings(
|
||||
extra_body={"reasoning_effort": "none", "cached_content": "some_cache"}
|
||||
)
|
||||
model = LitellmModel(model="test-model")
|
||||
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input=[],
|
||||
model_settings=settings,
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
)
|
||||
|
||||
assert captured["reasoning_effort"] == "none"
|
||||
assert captured["extra_body"] == {"cached_content": "some_cache"}
|
||||
assert settings.extra_body == {"reasoning_effort": "none", "cached_content": "some_cache"}
|
||||
|
||||
|
||||
@pytest.mark.allow_call_model_methods
|
||||
@pytest.mark.asyncio
|
||||
async def test_reasoning_effort_prefers_model_settings(monkeypatch):
|
||||
"""
|
||||
Verify explicit ModelSettings.reasoning takes precedence over extra_body entries.
|
||||
"""
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_acompletion(model, messages=None, **kwargs):
|
||||
captured.update(kwargs)
|
||||
msg = Message(role="assistant", content="ok")
|
||||
choice = Choices(index=0, message=msg)
|
||||
return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
settings = ModelSettings(
|
||||
reasoning=Reasoning(effort="low"),
|
||||
extra_body={"reasoning_effort": "high"},
|
||||
)
|
||||
model = LitellmModel(model="test-model")
|
||||
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input=[],
|
||||
model_settings=settings,
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
)
|
||||
|
||||
# reasoning_effort is string when no summary is provided (backward compatible)
|
||||
assert captured["reasoning_effort"] == "low"
|
||||
assert "extra_body" not in captured
|
||||
assert settings.extra_body == {"reasoning_effort": "high"}
|
||||
|
||||
|
||||
@pytest.mark.allow_call_model_methods
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_body_reasoning_effort_overrides_extra_args(monkeypatch):
|
||||
"""
|
||||
Ensure extra_body reasoning_effort wins over extra_args when both are provided.
|
||||
"""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_acompletion(model, messages=None, **kwargs):
|
||||
captured.update(kwargs)
|
||||
msg = Message(role="assistant", content="ok")
|
||||
choice = Choices(index=0, message=msg)
|
||||
return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
# GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764.
|
||||
settings = ModelSettings(
|
||||
extra_body={"reasoning_effort": "none"},
|
||||
extra_args={"reasoning_effort": "low", "custom_param": "custom"},
|
||||
)
|
||||
model = LitellmModel(model="test-model")
|
||||
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input=[],
|
||||
model_settings=settings,
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
)
|
||||
|
||||
assert captured["reasoning_effort"] == "none"
|
||||
assert captured["custom_param"] == "custom"
|
||||
assert "extra_body" not in captured
|
||||
assert settings.extra_args == {"reasoning_effort": "low", "custom_param": "custom"}
|
||||
|
||||
|
||||
@pytest.mark.allow_call_model_methods
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_body_metadata_stays_nested(monkeypatch):
|
||||
"""
|
||||
Keep extra_body metadata nested even when top-level metadata is also set.
|
||||
|
||||
LiteLLM resolves top-level metadata and extra_body separately. Flattening the nested
|
||||
metadata dict loses the caller's intended request shape for OpenAI-compatible proxies.
|
||||
"""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_acompletion(model, messages=None, **kwargs):
|
||||
captured.update(kwargs)
|
||||
msg = Message(role="assistant", content="ok")
|
||||
choice = Choices(index=0, message=msg)
|
||||
return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
settings = ModelSettings(
|
||||
metadata={"sdk": "agents"},
|
||||
extra_body={
|
||||
"metadata": {"trace_user_id": "user-123", "generation_id": "gen-456"},
|
||||
"cached_content": "some_cache",
|
||||
},
|
||||
)
|
||||
model = LitellmModel(model="test-model")
|
||||
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input=[],
|
||||
model_settings=settings,
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
)
|
||||
|
||||
assert captured["metadata"] == {"sdk": "agents"}
|
||||
assert captured["extra_body"] == {
|
||||
"metadata": {"trace_user_id": "user-123", "generation_id": "gen-456"},
|
||||
"cached_content": "some_cache",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.allow_call_model_methods
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"openai/gpt-5-mini",
|
||||
"anthropic/claude-sonnet-4-5",
|
||||
"gemini/gemini-2.5-pro",
|
||||
],
|
||||
)
|
||||
async def test_reasoning_summary_uses_scalar_effort_and_warns(
|
||||
monkeypatch, caplog: pytest.LogCaptureFixture, model_name: str
|
||||
):
|
||||
"""
|
||||
Ensure reasoning.summary does not change the LiteLLM chat-completions argument shape.
|
||||
|
||||
LitellmModel should continue to pass a scalar reasoning_effort value and warn that summary
|
||||
is ignored on this path, regardless of the provider encoded in the model string.
|
||||
"""
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_acompletion(model, messages=None, **kwargs):
|
||||
captured.update(kwargs)
|
||||
msg = Message(role="assistant", content="ok")
|
||||
choice = Choices(index=0, message=msg)
|
||||
return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
settings = ModelSettings(
|
||||
reasoning=Reasoning(effort="medium", summary="auto"),
|
||||
)
|
||||
model = LitellmModel(model=model_name)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="openai.agents"):
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input=[],
|
||||
model_settings=settings,
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
)
|
||||
|
||||
assert captured["reasoning_effort"] == "medium"
|
||||
warning_messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if "does not forward Reasoning.summary" in record.message
|
||||
]
|
||||
assert len(warning_messages) == 1
|
||||
Reference in New Issue
Block a user