|
4 | 4 | import sys |
5 | 5 | import types as pytypes |
6 | 6 | from collections.abc import AsyncIterator |
7 | | -from typing import Any |
| 7 | +from typing import Any, Literal, cast |
8 | 8 |
|
9 | 9 | import pytest |
10 | 10 | from openai.types.chat import ( |
|
25 | 25 | ) |
26 | 26 | from pydantic import BaseModel |
27 | 27 |
|
28 | | -from agents import ModelSettings, ModelTracing, __version__ |
| 28 | +from agents import ( |
| 29 | + Agent, |
| 30 | + Handoff, |
| 31 | + ModelSettings, |
| 32 | + ModelTracing, |
| 33 | + Tool, |
| 34 | + TResponseInputItem, |
| 35 | + __version__, |
| 36 | +) |
29 | 37 | from agents.exceptions import UserError |
30 | 38 | from agents.models.chatcmpl_helpers import HEADERS_OVERRIDE |
| 39 | +from agents.models.fake_id import FAKE_RESPONSES_ID |
31 | 40 |
|
32 | 41 |
|
33 | 42 | class FakeAnyLLMProvider: |
@@ -583,6 +592,140 @@ async def test_any_llm_prompt_requests_fail_fast(monkeypatch) -> None: |
583 | 592 | ) |
584 | 593 |
|
585 | 594 |
|
| 595 | +def test_any_llm_responses_input_sanitizer_strips_none_fields_from_reasoning_items() -> None: |
| 596 | + pytest.importorskip( |
| 597 | + "any_llm", |
| 598 | + reason="`any-llm-sdk` is only available when the optional dependency is installed.", |
| 599 | + ) |
| 600 | + from agents.extensions.models.any_llm_model import AnyLLMModel |
| 601 | + |
| 602 | + model = AnyLLMModel(model="openai/gpt-5.4-mini") |
| 603 | + raw_input = [ |
| 604 | + { |
| 605 | + "id": "rid1", |
| 606 | + "summary": [{"text": "why", "type": "summary_text"}], |
| 607 | + "type": "reasoning", |
| 608 | + "content": [{"type": "reasoning_text", "text": "thinking"}], |
| 609 | + "status": None, |
| 610 | + "encrypted_content": None, |
| 611 | + } |
| 612 | + ] |
| 613 | + |
| 614 | + cleaned = model._sanitize_any_llm_responses_input(raw_input) |
| 615 | + |
| 616 | + assert cleaned == [ |
| 617 | + { |
| 618 | + "id": "rid1", |
| 619 | + "summary": [{"text": "why", "type": "summary_text"}], |
| 620 | + "type": "reasoning", |
| 621 | + "content": [{"type": "reasoning_text", "text": "thinking"}], |
| 622 | + } |
| 623 | + ] |
| 624 | + |
| 625 | + ResponsesParams = importlib.import_module("any_llm.types.responses").ResponsesParams |
| 626 | + params = ResponsesParams(model="dummy", input=cleaned) |
| 627 | + assert isinstance(params.input, list) |
| 628 | + |
| 629 | + |
| 630 | +@pytest.mark.allow_call_model_methods |
| 631 | +@pytest.mark.asyncio |
| 632 | +async def test_any_llm_responses_path_sanitizes_replayed_items_before_validation() -> None: |
| 633 | + pytest.importorskip( |
| 634 | + "any_llm", |
| 635 | + reason="`any-llm-sdk` is only available when the optional dependency is installed.", |
| 636 | + ) |
| 637 | + from agents.extensions.models.any_llm_model import AnyLLMModel |
| 638 | + |
| 639 | + class ValidatingProvider: |
| 640 | + SUPPORTS_RESPONSES = True |
| 641 | + |
| 642 | + def __init__(self) -> None: |
| 643 | + self.private_responses_calls: list[dict[str, Any]] = [] |
| 644 | + |
| 645 | + async def aresponses(self, **kwargs: Any) -> Any: |
| 646 | + raise AssertionError("public aresponses path should not be used in this test") |
| 647 | + |
| 648 | + async def _aresponses(self, params: Any, **kwargs: Any) -> Response: |
| 649 | + self.private_responses_calls.append({"params": params, "kwargs": kwargs}) |
| 650 | + return _response("Hello from sanitized replay") |
| 651 | + |
| 652 | + class TestAnyLLMModel(AnyLLMModel): |
| 653 | + def __init__(self, provider: ValidatingProvider) -> None: |
| 654 | + super().__init__(model="openai/gpt-5.4-mini", api="responses") |
| 655 | + self._provider = provider |
| 656 | + |
| 657 | + def _get_provider(self) -> Any: |
| 658 | + return self._provider |
| 659 | + |
| 660 | + provider = ValidatingProvider() |
| 661 | + model = TestAnyLLMModel(provider) |
| 662 | + tools: list[Tool] = [] |
| 663 | + handoffs: list[Handoff[Any, Agent[Any]]] = [] |
| 664 | + stream_flag: Literal[False] = False |
| 665 | + |
| 666 | + replay_input = cast( |
| 667 | + list[TResponseInputItem], |
| 668 | + [ |
| 669 | + {"role": "user", "content": "What's the weather in Tokyo?"}, |
| 670 | + { |
| 671 | + "id": FAKE_RESPONSES_ID, |
| 672 | + "summary": [ |
| 673 | + {"text": "I should call the weather tool first.", "type": "summary_text"} |
| 674 | + ], |
| 675 | + "type": "reasoning", |
| 676 | + "content": [{"type": "reasoning_text", "text": "thinking"}], |
| 677 | + "status": None, |
| 678 | + "provider_data": {"model": "anthropic/fake-responses-model"}, |
| 679 | + }, |
| 680 | + { |
| 681 | + "id": FAKE_RESPONSES_ID, |
| 682 | + "arguments": '{"city": "Tokyo"}', |
| 683 | + "call_id": "call_weather_123", |
| 684 | + "name": "get_weather", |
| 685 | + "type": "function_call", |
| 686 | + "status": None, |
| 687 | + "provider_data": {"model": "anthropic/fake-responses-model"}, |
| 688 | + }, |
| 689 | + { |
| 690 | + "type": "function_call_output", |
| 691 | + "call_id": "call_weather_123", |
| 692 | + "output": "The weather in Tokyo is sunny and 22°C.", |
| 693 | + }, |
| 694 | + ], |
| 695 | + ) |
| 696 | + |
| 697 | + response = await model._fetch_responses_response( |
| 698 | + system_instructions=None, |
| 699 | + input=replay_input, |
| 700 | + model_settings=ModelSettings(), |
| 701 | + tools=tools, |
| 702 | + output_schema=None, |
| 703 | + handoffs=handoffs, |
| 704 | + previous_response_id=None, |
| 705 | + conversation_id=None, |
| 706 | + stream=stream_flag, |
| 707 | + prompt=None, |
| 708 | + ) |
| 709 | + |
| 710 | + assert response.id == "resp_123" |
| 711 | + assert len(provider.private_responses_calls) == 1 |
| 712 | + params = provider.private_responses_calls[0]["params"] |
| 713 | + assert params.input == [ |
| 714 | + {"role": "user", "content": "What's the weather in Tokyo?"}, |
| 715 | + { |
| 716 | + "arguments": '{"city": "Tokyo"}', |
| 717 | + "call_id": "call_weather_123", |
| 718 | + "name": "get_weather", |
| 719 | + "type": "function_call", |
| 720 | + }, |
| 721 | + { |
| 722 | + "type": "function_call_output", |
| 723 | + "call_id": "call_weather_123", |
| 724 | + "output": "The weather in Tokyo is sunny and 22°C.", |
| 725 | + }, |
| 726 | + ] |
| 727 | + |
| 728 | + |
586 | 729 | def test_any_llm_provider_passes_api_override() -> None: |
587 | 730 | pytest.importorskip( |
588 | 731 | "any_llm", |
|
0 commit comments