jwadow commited on
Commit
3fa4c6a
·
1 Parent(s): bda15b0

test(models): add comprehensive Pydantic model validation tests

Browse files
tests/README.md CHANGED
@@ -80,7 +80,8 @@ tests/
80
  │ ├── test_converters_openai.py # OpenAI Chat API → Kiro converter tests
81
  │ ├── test_debug_logger.py # DebugLogger tests (off/errors/all modes)
82
  │ ├── test_main_cli.py # CLI argument parsing tests (--host, --port)
83
- │ ├── test_models_anthropic.py # Anthropic Pydantic models tests (image content blocks, Issue #30)
 
84
  │ ├── test_parsers.py # AwsEventStreamParser tests
85
  │ ├── test_routes_anthropic.py # Anthropic API endpoint tests (/v1/messages)
86
  │ ├── test_routes_openai.py # OpenAI API endpoint tests (/v1/chat/completions)
 
80
  │ ├── test_converters_openai.py # OpenAI Chat API → Kiro converter tests
81
  │ ├── test_debug_logger.py # DebugLogger tests (off/errors/all modes)
82
  │ ├── test_main_cli.py # CLI argument parsing tests (--host, --port)
83
+ │ ├── test_models_anthropic.py # Anthropic Pydantic models tests (all content blocks, tools, streaming)
84
+ │ ├── test_models_openai.py # OpenAI Pydantic models tests (messages, tools, responses, streaming)
85
  │ ├── test_parsers.py # AwsEventStreamParser tests
86
  │ ├── test_routes_anthropic.py # Anthropic API endpoint tests (/v1/messages)
87
  │ ├── test_routes_openai.py # OpenAI API endpoint tests (/v1/chat/completions)
tests/unit/test_models_anthropic.py CHANGED
@@ -3,27 +3,59 @@
3
  """
4
  Unit tests for Anthropic Pydantic models.
5
 
6
- Tests for image-related models added in Issue #30 fix:
7
- - Base64ImageSource
8
- - URLImageSource
9
- - ImageContentBlock
10
- - ContentBlock union with ImageContentBlock
11
- - AnthropicMessage with image content
 
12
  """
13
 
14
  import pytest
15
  from pydantic import ValidationError
16
 
17
  from kiro.models_anthropic import (
 
 
 
 
 
 
18
  Base64ImageSource,
19
  URLImageSource,
20
  ImageContentBlock,
21
  ContentBlock,
22
- TextContentBlock,
23
- ToolUseContentBlock,
24
- ToolResultContentBlock,
25
  AnthropicMessage,
 
 
 
 
 
 
 
 
26
  AnthropicMessagesRequest,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  )
28
 
29
 
@@ -587,3 +619,886 @@ class TestAnthropicMessagesRequestWithImages:
587
  assert request.messages[2].content == "Can you describe it in more detail?"
588
 
589
  print("Multi-turn conversation with images validated successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
  Unit tests for Anthropic Pydantic models.
5
 
6
+ Comprehensive tests for all Anthropic API models:
7
+ - Content blocks (text, image, tool_use, tool_result, thinking)
8
+ - Image sources (base64, URL)
9
+ - Messages and requests
10
+ - Tools and tool choice
11
+ - Responses and streaming events
12
+ - Error models
13
  """
14
 
15
  import pytest
16
  from pydantic import ValidationError
17
 
18
  from kiro.models_anthropic import (
19
+ # Content blocks
20
+ TextContentBlock,
21
+ ThinkingContentBlock,
22
+ ToolUseContentBlock,
23
+ ToolResultContentBlock,
24
+ # Image models
25
  Base64ImageSource,
26
  URLImageSource,
27
  ImageContentBlock,
28
  ContentBlock,
29
+ # Message models
 
 
30
  AnthropicMessage,
31
+ # Tool models
32
+ AnthropicTool,
33
+ ToolChoiceAuto,
34
+ ToolChoiceAny,
35
+ ToolChoiceTool,
36
+ ToolChoice,
37
+ # Request models
38
+ SystemContentBlock,
39
  AnthropicMessagesRequest,
40
+ # Response models
41
+ AnthropicUsage,
42
+ AnthropicMessagesResponse,
43
+ # Streaming models
44
+ MessageStartEvent,
45
+ ContentBlockStartEvent,
46
+ TextDelta,
47
+ ThinkingDelta,
48
+ InputJsonDelta,
49
+ ContentBlockDeltaEvent,
50
+ ContentBlockStopEvent,
51
+ MessageDeltaUsage,
52
+ MessageDeltaEvent,
53
+ MessageStopEvent,
54
+ PingEvent,
55
+ ErrorEvent,
56
+ # Error models
57
+ AnthropicErrorDetail,
58
+ AnthropicErrorResponse,
59
  )
60
 
61
 
 
619
  assert request.messages[2].content == "Can you describe it in more detail?"
620
 
621
  print("Multi-turn conversation with images validated successfully!")
622
+
623
+
624
+ # ==================================================================================================
625
+ # Tests for TextContentBlock
626
+ # ==================================================================================================
627
+
628
+ class TestTextContentBlock:
629
+ """Tests for TextContentBlock Pydantic model."""
630
+
631
+ def test_valid_text_block(self):
632
+ """
633
+ What it does: Verifies creation of valid TextContentBlock.
634
+ Purpose: Ensure model accepts valid text content.
635
+ """
636
+ print("Setup: Creating TextContentBlock with valid text...")
637
+ block = TextContentBlock(text="Hello, world!")
638
+
639
+ print(f"Result: {block}")
640
+ print(f"Comparing type: Expected 'text', Got '{block.type}'")
641
+ assert block.type == "text"
642
+
643
+ print(f"Comparing text: Expected 'Hello, world!', Got '{block.text}'")
644
+ assert block.text == "Hello, world!"
645
+
646
+ def test_type_defaults_to_text(self):
647
+ """
648
+ What it does: Verifies that type defaults to "text".
649
+ Purpose: Ensure default value is set correctly.
650
+ """
651
+ print("Setup: Creating TextContentBlock without explicit type...")
652
+ block = TextContentBlock(text="Test")
653
+
654
+ print(f"Comparing type: Expected 'text', Got '{block.type}'")
655
+ assert block.type == "text"
656
+
657
+ def test_requires_text(self):
658
+ """
659
+ What it does: Verifies that text is required.
660
+ Purpose: Ensure validation fails without text.
661
+ """
662
+ print("Setup: Attempting to create TextContentBlock without text...")
663
+
664
+ print("Action: Creating model (should raise ValidationError)...")
665
+ with pytest.raises(ValidationError) as exc_info:
666
+ TextContentBlock()
667
+
668
+ print(f"ValidationError raised: {exc_info.value}")
669
+ assert "text" in str(exc_info.value)
670
+
671
+ def test_accepts_empty_string(self):
672
+ """
673
+ What it does: Verifies that empty string is accepted.
674
+ Purpose: Ensure empty text is valid.
675
+ """
676
+ print("Setup: Creating TextContentBlock with empty string...")
677
+ block = TextContentBlock(text="")
678
+
679
+ print(f"Comparing text: Expected '', Got '{block.text}'")
680
+ assert block.text == ""
681
+
682
+ def test_accepts_multiline_text(self):
683
+ """
684
+ What it does: Verifies that multiline text is accepted.
685
+ Purpose: Ensure newlines are preserved.
686
+ """
687
+ print("Setup: Creating TextContentBlock with multiline text...")
688
+ multiline = "Line 1\nLine 2\nLine 3"
689
+ block = TextContentBlock(text=multiline)
690
+
691
+ print(f"Comparing text: Expected multiline, Got '{block.text}'")
692
+ assert block.text == multiline
693
+ assert "\n" in block.text
694
+
695
+
696
+ # ==================================================================================================
697
+ # Tests for ThinkingContentBlock
698
+ # ==================================================================================================
699
+
700
+ class TestThinkingContentBlock:
701
+ """Tests for ThinkingContentBlock Pydantic model."""
702
+
703
+ def test_valid_thinking_block(self):
704
+ """
705
+ What it does: Verifies creation of valid ThinkingContentBlock.
706
+ Purpose: Ensure model accepts valid thinking content.
707
+ """
708
+ print("Setup: Creating ThinkingContentBlock with valid thinking...")
709
+ block = ThinkingContentBlock(
710
+ thinking="Let me analyze this step by step...",
711
+ signature="abc123"
712
+ )
713
+
714
+ print(f"Result: {block}")
715
+ print(f"Comparing type: Expected 'thinking', Got '{block.type}'")
716
+ assert block.type == "thinking"
717
+
718
+ print(f"Comparing thinking: Got '{block.thinking[:30]}...'")
719
+ assert block.thinking == "Let me analyze this step by step..."
720
+
721
+ print(f"Comparing signature: Expected 'abc123', Got '{block.signature}'")
722
+ assert block.signature == "abc123"
723
+
724
+ def test_type_defaults_to_thinking(self):
725
+ """
726
+ What it does: Verifies that type defaults to "thinking".
727
+ Purpose: Ensure default value is set correctly.
728
+ """
729
+ print("Setup: Creating ThinkingContentBlock without explicit type...")
730
+ block = ThinkingContentBlock(thinking="Test thinking")
731
+
732
+ print(f"Comparing type: Expected 'thinking', Got '{block.type}'")
733
+ assert block.type == "thinking"
734
+
735
+ def test_signature_defaults_to_empty(self):
736
+ """
737
+ What it does: Verifies that signature defaults to empty string.
738
+ Purpose: Ensure default value is set correctly.
739
+ """
740
+ print("Setup: Creating ThinkingContentBlock without signature...")
741
+ block = ThinkingContentBlock(thinking="Test")
742
+
743
+ print(f"Comparing signature: Expected '', Got '{block.signature}'")
744
+ assert block.signature == ""
745
+
746
+ def test_requires_thinking(self):
747
+ """
748
+ What it does: Verifies that thinking is required.
749
+ Purpose: Ensure validation fails without thinking.
750
+ """
751
+ print("Setup: Attempting to create ThinkingContentBlock without thinking...")
752
+
753
+ print("Action: Creating model (should raise ValidationError)...")
754
+ with pytest.raises(ValidationError) as exc_info:
755
+ ThinkingContentBlock()
756
+
757
+ print(f"ValidationError raised: {exc_info.value}")
758
+ assert "thinking" in str(exc_info.value)
759
+
760
+
761
+ # ==================================================================================================
762
+ # Tests for ToolUseContentBlock
763
+ # ==================================================================================================
764
+
765
+ class TestToolUseContentBlock:
766
+ """Tests for ToolUseContentBlock Pydantic model."""
767
+
768
+ def test_valid_tool_use_block(self):
769
+ """
770
+ What it does: Verifies creation of valid ToolUseContentBlock.
771
+ Purpose: Ensure model accepts valid tool use data.
772
+ """
773
+ print("Setup: Creating ToolUseContentBlock with valid data...")
774
+ block = ToolUseContentBlock(
775
+ id="call_123",
776
+ name="get_weather",
777
+ input={"location": "Moscow", "units": "celsius"}
778
+ )
779
+
780
+ print(f"Result: {block}")
781
+ print(f"Comparing type: Expected 'tool_use', Got '{block.type}'")
782
+ assert block.type == "tool_use"
783
+
784
+ print(f"Comparing id: Expected 'call_123', Got '{block.id}'")
785
+ assert block.id == "call_123"
786
+
787
+ print(f"Comparing name: Expected 'get_weather', Got '{block.name}'")
788
+ assert block.name == "get_weather"
789
+
790
+ print(f"Comparing input: Got {block.input}")
791
+ assert block.input == {"location": "Moscow", "units": "celsius"}
792
+
793
+ def test_type_defaults_to_tool_use(self):
794
+ """
795
+ What it does: Verifies that type defaults to "tool_use".
796
+ Purpose: Ensure default value is set correctly.
797
+ """
798
+ print("Setup: Creating ToolUseContentBlock without explicit type...")
799
+ block = ToolUseContentBlock(id="call_1", name="test", input={})
800
+
801
+ print(f"Comparing type: Expected 'tool_use', Got '{block.type}'")
802
+ assert block.type == "tool_use"
803
+
804
+ def test_requires_id(self):
805
+ """
806
+ What it does: Verifies that id is required.
807
+ Purpose: Ensure validation fails without id.
808
+ """
809
+ print("Setup: Attempting to create ToolUseContentBlock without id...")
810
+
811
+ print("Action: Creating model (should raise ValidationError)...")
812
+ with pytest.raises(ValidationError) as exc_info:
813
+ ToolUseContentBlock(name="test", input={})
814
+
815
+ print(f"ValidationError raised: {exc_info.value}")
816
+ assert "id" in str(exc_info.value)
817
+
818
+ def test_requires_name(self):
819
+ """
820
+ What it does: Verifies that name is required.
821
+ Purpose: Ensure validation fails without name.
822
+ """
823
+ print("Setup: Attempting to create ToolUseContentBlock without name...")
824
+
825
+ print("Action: Creating model (should raise ValidationError)...")
826
+ with pytest.raises(ValidationError) as exc_info:
827
+ ToolUseContentBlock(id="call_1", input={})
828
+
829
+ print(f"ValidationError raised: {exc_info.value}")
830
+ assert "name" in str(exc_info.value)
831
+
832
+ def test_requires_input(self):
833
+ """
834
+ What it does: Verifies that input is required.
835
+ Purpose: Ensure validation fails without input.
836
+ """
837
+ print("Setup: Attempting to create ToolUseContentBlock without input...")
838
+
839
+ print("Action: Creating model (should raise ValidationError)...")
840
+ with pytest.raises(ValidationError) as exc_info:
841
+ ToolUseContentBlock(id="call_1", name="test")
842
+
843
+ print(f"ValidationError raised: {exc_info.value}")
844
+ assert "input" in str(exc_info.value)
845
+
846
+ def test_accepts_empty_input(self):
847
+ """
848
+ What it does: Verifies that empty input dict is accepted.
849
+ Purpose: Ensure tools without parameters work.
850
+ """
851
+ print("Setup: Creating ToolUseContentBlock with empty input...")
852
+ block = ToolUseContentBlock(id="call_1", name="no_params_tool", input={})
853
+
854
+ print(f"Comparing input: Expected {{}}, Got {block.input}")
855
+ assert block.input == {}
856
+
857
+ def test_accepts_complex_input(self):
858
+ """
859
+ What it does: Verifies that complex nested input is accepted.
860
+ Purpose: Ensure nested structures work.
861
+ """
862
+ print("Setup: Creating ToolUseContentBlock with complex input...")
863
+ complex_input = {
864
+ "query": "test",
865
+ "options": {"limit": 10, "offset": 0},
866
+ "filters": ["active", "recent"]
867
+ }
868
+ block = ToolUseContentBlock(id="call_1", name="search", input=complex_input)
869
+
870
+ print(f"Comparing input: Got {block.input}")
871
+ assert block.input == complex_input
872
+
873
+
874
+ # ==================================================================================================
875
+ # Tests for ToolResultContentBlock
876
+ # ==================================================================================================
877
+
878
+ class TestToolResultContentBlock:
879
+ """Tests for ToolResultContentBlock Pydantic model."""
880
+
881
+ def test_valid_tool_result_block(self):
882
+ """
883
+ What it does: Verifies creation of valid ToolResultContentBlock.
884
+ Purpose: Ensure model accepts valid tool result data.
885
+ """
886
+ print("Setup: Creating ToolResultContentBlock with valid data...")
887
+ block = ToolResultContentBlock(
888
+ tool_use_id="call_123",
889
+ content="Weather in Moscow: Sunny, 25°C"
890
+ )
891
+
892
+ print(f"Result: {block}")
893
+ print(f"Comparing type: Expected 'tool_result', Got '{block.type}'")
894
+ assert block.type == "tool_result"
895
+
896
+ print(f"Comparing tool_use_id: Expected 'call_123', Got '{block.tool_use_id}'")
897
+ assert block.tool_use_id == "call_123"
898
+
899
+ print(f"Comparing content: Got '{block.content}'")
900
+ assert block.content == "Weather in Moscow: Sunny, 25°C"
901
+
902
+ def test_type_defaults_to_tool_result(self):
903
+ """
904
+ What it does: Verifies that type defaults to "tool_result".
905
+ Purpose: Ensure default value is set correctly.
906
+ """
907
+ print("Setup: Creating ToolResultContentBlock without explicit type...")
908
+ block = ToolResultContentBlock(tool_use_id="call_1")
909
+
910
+ print(f"Comparing type: Expected 'tool_result', Got '{block.type}'")
911
+ assert block.type == "tool_result"
912
+
913
+ def test_requires_tool_use_id(self):
914
+ """
915
+ What it does: Verifies that tool_use_id is required.
916
+ Purpose: Ensure validation fails without tool_use_id.
917
+ """
918
+ print("Setup: Attempting to create ToolResultContentBlock without tool_use_id...")
919
+
920
+ print("Action: Creating model (should raise ValidationError)...")
921
+ with pytest.raises(ValidationError) as exc_info:
922
+ ToolResultContentBlock(content="Result")
923
+
924
+ print(f"ValidationError raised: {exc_info.value}")
925
+ assert "tool_use_id" in str(exc_info.value)
926
+
927
+ def test_content_is_optional(self):
928
+ """
929
+ What it does: Verifies that content is optional.
930
+ Purpose: Ensure tool results without content work.
931
+ """
932
+ print("Setup: Creating ToolResultContentBlock without content...")
933
+ block = ToolResultContentBlock(tool_use_id="call_1")
934
+
935
+ print(f"Comparing content: Expected None, Got {block.content}")
936
+ assert block.content is None
937
+
938
+ def test_accepts_list_content(self):
939
+ """
940
+ What it does: Verifies that list content is accepted.
941
+ Purpose: Ensure content can be list of TextContentBlock.
942
+ """
943
+ print("Setup: Creating ToolResultContentBlock with list content...")
944
+ block = ToolResultContentBlock(
945
+ tool_use_id="call_1",
946
+ content=[TextContentBlock(text="Part 1"), TextContentBlock(text="Part 2")]
947
+ )
948
+
949
+ print(f"Comparing content type: Expected list, Got {type(block.content)}")
950
+ assert isinstance(block.content, list)
951
+ assert len(block.content) == 2
952
+
953
+ def test_is_error_field(self):
954
+ """
955
+ What it does: Verifies that is_error field works.
956
+ Purpose: Ensure error results can be marked.
957
+ """
958
+ print("Setup: Creating ToolResultContentBlock with is_error=True...")
959
+ block = ToolResultContentBlock(
960
+ tool_use_id="call_1",
961
+ content="Error: File not found",
962
+ is_error=True
963
+ )
964
+
965
+ print(f"Comparing is_error: Expected True, Got {block.is_error}")
966
+ assert block.is_error is True
967
+
968
+ def test_is_error_defaults_to_none(self):
969
+ """
970
+ What it does: Verifies that is_error defaults to None.
971
+ Purpose: Ensure default value is correct.
972
+ """
973
+ print("Setup: Creating ToolResultContentBlock without is_error...")
974
+ block = ToolResultContentBlock(tool_use_id="call_1", content="Success")
975
+
976
+ print(f"Comparing is_error: Expected None, Got {block.is_error}")
977
+ assert block.is_error is None
978
+
979
+
980
+ # ==================================================================================================
981
+ # Tests for AnthropicTool
982
+ # ==================================================================================================
983
+
984
+ class TestAnthropicTool:
985
+ """Tests for AnthropicTool Pydantic model."""
986
+
987
+ def test_valid_tool(self):
988
+ """
989
+ What it does: Verifies creation of valid AnthropicTool.
990
+ Purpose: Ensure model accepts valid tool definition.
991
+ """
992
+ print("Setup: Creating AnthropicTool with valid data...")
993
+ tool = AnthropicTool(
994
+ name="get_weather",
995
+ description="Get weather for a location",
996
+ input_schema={
997
+ "type": "object",
998
+ "properties": {
999
+ "location": {"type": "string", "description": "City name"}
1000
+ },
1001
+ "required": ["location"]
1002
+ }
1003
+ )
1004
+
1005
+ print(f"Result: {tool}")
1006
+ print(f"Comparing name: Expected 'get_weather', Got '{tool.name}'")
1007
+ assert tool.name == "get_weather"
1008
+
1009
+ print(f"Comparing description: Got '{tool.description}'")
1010
+ assert tool.description == "Get weather for a location"
1011
+
1012
+ print(f"Comparing input_schema: Got {tool.input_schema}")
1013
+ assert "properties" in tool.input_schema
1014
+
1015
+ def test_requires_name(self):
1016
+ """
1017
+ What it does: Verifies that name is required.
1018
+ Purpose: Ensure validation fails without name.
1019
+ """
1020
+ print("Setup: Attempting to create AnthropicTool without name...")
1021
+
1022
+ print("Action: Creating model (should raise ValidationError)...")
1023
+ with pytest.raises(ValidationError) as exc_info:
1024
+ AnthropicTool(input_schema={})
1025
+
1026
+ print(f"ValidationError raised: {exc_info.value}")
1027
+ assert "name" in str(exc_info.value)
1028
+
1029
+ def test_requires_input_schema(self):
1030
+ """
1031
+ What it does: Verifies that input_schema is required.
1032
+ Purpose: Ensure validation fails without input_schema.
1033
+ """
1034
+ print("Setup: Attempting to create AnthropicTool without input_schema...")
1035
+
1036
+ print("Action: Creating model (should raise ValidationError)...")
1037
+ with pytest.raises(ValidationError) as exc_info:
1038
+ AnthropicTool(name="test")
1039
+
1040
+ print(f"ValidationError raised: {exc_info.value}")
1041
+ assert "input_schema" in str(exc_info.value)
1042
+
1043
+ def test_description_is_optional(self):
1044
+ """
1045
+ What it does: Verifies that description is optional.
1046
+ Purpose: Ensure tools without description work.
1047
+ """
1048
+ print("Setup: Creating AnthropicTool without description...")
1049
+ tool = AnthropicTool(name="simple_tool", input_schema={})
1050
+
1051
+ print(f"Comparing description: Expected None, Got {tool.description}")
1052
+ assert tool.description is None
1053
+
1054
+
1055
+ # ==================================================================================================
1056
+ # Tests for ToolChoice models
1057
+ # ==================================================================================================
1058
+
1059
+ class TestToolChoiceModels:
1060
+ """Tests for ToolChoice Pydantic models."""
1061
+
1062
+ def test_tool_choice_auto(self):
1063
+ """
1064
+ What it does: Verifies creation of ToolChoiceAuto.
1065
+ Purpose: Ensure auto tool choice works.
1066
+ """
1067
+ print("Setup: Creating ToolChoiceAuto...")
1068
+ choice = ToolChoiceAuto()
1069
+
1070
+ print(f"Result: {choice}")
1071
+ print(f"Comparing type: Expected 'auto', Got '{choice.type}'")
1072
+ assert choice.type == "auto"
1073
+
1074
+ def test_tool_choice_any(self):
1075
+ """
1076
+ What it does: Verifies creation of ToolChoiceAny.
1077
+ Purpose: Ensure any tool choice works.
1078
+ """
1079
+ print("Setup: Creating ToolChoiceAny...")
1080
+ choice = ToolChoiceAny()
1081
+
1082
+ print(f"Result: {choice}")
1083
+ print(f"Comparing type: Expected 'any', Got '{choice.type}'")
1084
+ assert choice.type == "any"
1085
+
1086
+ def test_tool_choice_tool(self):
1087
+ """
1088
+ What it does: Verifies creation of ToolChoiceTool.
1089
+ Purpose: Ensure specific tool choice works.
1090
+ """
1091
+ print("Setup: Creating ToolChoiceTool...")
1092
+ choice = ToolChoiceTool(name="get_weather")
1093
+
1094
+ print(f"Result: {choice}")
1095
+ print(f"Comparing type: Expected 'tool', Got '{choice.type}'")
1096
+ assert choice.type == "tool"
1097
+
1098
+ print(f"Comparing name: Expected 'get_weather', Got '{choice.name}'")
1099
+ assert choice.name == "get_weather"
1100
+
1101
+ def test_tool_choice_tool_requires_name(self):
1102
+ """
1103
+ What it does: Verifies that ToolChoiceTool requires name.
1104
+ Purpose: Ensure validation fails without name.
1105
+ """
1106
+ print("Setup: Attempting to create ToolChoiceTool without name...")
1107
+
1108
+ print("Action: Creating model (should raise ValidationError)...")
1109
+ with pytest.raises(ValidationError) as exc_info:
1110
+ ToolChoiceTool()
1111
+
1112
+ print(f"ValidationError raised: {exc_info.value}")
1113
+ assert "name" in str(exc_info.value)
1114
+
1115
+
1116
+ # ==================================================================================================
1117
+ # Tests for SystemContentBlock
1118
+ # ==================================================================================================
1119
+
1120
+ class TestSystemContentBlock:
1121
+ """Tests for SystemContentBlock Pydantic model."""
1122
+
1123
+ def test_valid_system_block(self):
1124
+ """
1125
+ What it does: Verifies creation of valid SystemContentBlock.
1126
+ Purpose: Ensure model accepts valid system content.
1127
+ """
1128
+ print("Setup: Creating SystemContentBlock with valid data...")
1129
+ block = SystemContentBlock(text="You are a helpful assistant.")
1130
+
1131
+ print(f"Result: {block}")
1132
+ print(f"Comparing type: Expected 'text', Got '{block.type}'")
1133
+ assert block.type == "text"
1134
+
1135
+ print(f"Comparing text: Got '{block.text}'")
1136
+ assert block.text == "You are a helpful assistant."
1137
+
1138
+ def test_with_cache_control(self):
1139
+ """
1140
+ What it does: Verifies SystemContentBlock with cache_control.
1141
+ Purpose: Ensure prompt caching format works.
1142
+ """
1143
+ print("Setup: Creating SystemContentBlock with cache_control...")
1144
+ block = SystemContentBlock(
1145
+ text="You are helpful.",
1146
+ cache_control={"type": "ephemeral"}
1147
+ )
1148
+
1149
+ print(f"Result: {block}")
1150
+ print(f"Comparing cache_control: Got {block.cache_control}")
1151
+ assert block.cache_control == {"type": "ephemeral"}
1152
+
1153
+ def test_cache_control_is_optional(self):
1154
+ """
1155
+ What it does: Verifies that cache_control is optional.
1156
+ Purpose: Ensure blocks without cache_control work.
1157
+ """
1158
+ print("Setup: Creating SystemContentBlock without cache_control...")
1159
+ block = SystemContentBlock(text="Test")
1160
+
1161
+ print(f"Comparing cache_control: Expected None, Got {block.cache_control}")
1162
+ assert block.cache_control is None
1163
+
1164
+ def test_requires_text(self):
1165
+ """
1166
+ What it does: Verifies that text is required.
1167
+ Purpose: Ensure validation fails without text.
1168
+ """
1169
+ print("Setup: Attempting to create SystemContentBlock without text...")
1170
+
1171
+ print("Action: Creating model (should raise ValidationError)...")
1172
+ with pytest.raises(ValidationError) as exc_info:
1173
+ SystemContentBlock()
1174
+
1175
+ print(f"ValidationError raised: {exc_info.value}")
1176
+ assert "text" in str(exc_info.value)
1177
+
1178
+
1179
+ # ==================================================================================================
1180
+ # Tests for AnthropicUsage
1181
+ # ==================================================================================================
1182
+
1183
+ class TestAnthropicUsage:
1184
+ """Tests for AnthropicUsage Pydantic model."""
1185
+
1186
+ def test_valid_usage(self):
1187
+ """
1188
+ What it does: Verifies creation of valid AnthropicUsage.
1189
+ Purpose: Ensure model accepts valid usage data.
1190
+ """
1191
+ print("Setup: Creating AnthropicUsage with valid data...")
1192
+ usage = AnthropicUsage(input_tokens=100, output_tokens=50)
1193
+
1194
+ print(f"Result: {usage}")
1195
+ print(f"Comparing input_tokens: Expected 100, Got {usage.input_tokens}")
1196
+ assert usage.input_tokens == 100
1197
+
1198
+ print(f"Comparing output_tokens: Expected 50, Got {usage.output_tokens}")
1199
+ assert usage.output_tokens == 50
1200
+
1201
+ def test_requires_input_tokens(self):
1202
+ """
1203
+ What it does: Verifies that input_tokens is required.
1204
+ Purpose: Ensure validation fails without input_tokens.
1205
+ """
1206
+ print("Setup: Attempting to create AnthropicUsage without input_tokens...")
1207
+
1208
+ print("Action: Creating model (should raise ValidationError)...")
1209
+ with pytest.raises(ValidationError) as exc_info:
1210
+ AnthropicUsage(output_tokens=50)
1211
+
1212
+ print(f"ValidationError raised: {exc_info.value}")
1213
+ assert "input_tokens" in str(exc_info.value)
1214
+
1215
+ def test_requires_output_tokens(self):
1216
+ """
1217
+ What it does: Verifies that output_tokens is required.
1218
+ Purpose: Ensure validation fails without output_tokens.
1219
+ """
1220
+ print("Setup: Attempting to create AnthropicUsage without output_tokens...")
1221
+
1222
+ print("Action: Creating model (should raise ValidationError)...")
1223
+ with pytest.raises(ValidationError) as exc_info:
1224
+ AnthropicUsage(input_tokens=100)
1225
+
1226
+ print(f"ValidationError raised: {exc_info.value}")
1227
+ assert "output_tokens" in str(exc_info.value)
1228
+
1229
+
1230
+ # ==================================================================================================
1231
+ # Tests for AnthropicMessagesResponse
1232
+ # ==================================================================================================
1233
+
1234
+ class TestAnthropicMessagesResponse:
1235
+ """Tests for AnthropicMessagesResponse Pydantic model."""
1236
+
1237
+ def test_valid_response(self):
1238
+ """
1239
+ What it does: Verifies creation of valid AnthropicMessagesResponse.
1240
+ Purpose: Ensure model accepts valid response data.
1241
+ """
1242
+ print("Setup: Creating AnthropicMessagesResponse with valid data...")
1243
+ response = AnthropicMessagesResponse(
1244
+ id="msg_123",
1245
+ model="claude-sonnet-4-5",
1246
+ content=[TextContentBlock(text="Hello!")],
1247
+ usage=AnthropicUsage(input_tokens=10, output_tokens=5)
1248
+ )
1249
+
1250
+ print(f"Result: {response}")
1251
+ print(f"Comparing id: Expected 'msg_123', Got '{response.id}'")
1252
+ assert response.id == "msg_123"
1253
+
1254
+ print(f"Comparing type: Expected 'message', Got '{response.type}'")
1255
+ assert response.type == "message"
1256
+
1257
+ print(f"Comparing role: Expected 'assistant', Got '{response.role}'")
1258
+ assert response.role == "assistant"
1259
+
1260
+ print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{response.model}'")
1261
+ assert response.model == "claude-sonnet-4-5"
1262
+
1263
+ def test_stop_reason_values(self):
1264
+ """
1265
+ What it does: Verifies that stop_reason accepts valid values.
1266
+ Purpose: Ensure all stop reasons work.
1267
+ """
1268
+ print("Setup: Testing various stop_reason values...")
1269
+ stop_reasons = ["end_turn", "max_tokens", "stop_sequence", "tool_use"]
1270
+
1271
+ for reason in stop_reasons:
1272
+ print(f"Testing stop_reason: {reason}")
1273
+ response = AnthropicMessagesResponse(
1274
+ id="msg_1",
1275
+ model="claude-sonnet-4-5",
1276
+ content=[TextContentBlock(text="Test")],
1277
+ usage=AnthropicUsage(input_tokens=1, output_tokens=1),
1278
+ stop_reason=reason
1279
+ )
1280
+ assert response.stop_reason == reason
1281
+
1282
+ print("All stop_reason values accepted successfully")
1283
+
1284
+ def test_stop_reason_is_optional(self):
1285
+ """
1286
+ What it does: Verifies that stop_reason is optional.
1287
+ Purpose: Ensure responses without stop_reason work.
1288
+ """
1289
+ print("Setup: Creating response without stop_reason...")
1290
+ response = AnthropicMessagesResponse(
1291
+ id="msg_1",
1292
+ model="claude-sonnet-4-5",
1293
+ content=[TextContentBlock(text="Test")],
1294
+ usage=AnthropicUsage(input_tokens=1, output_tokens=1)
1295
+ )
1296
+
1297
+ print(f"Comparing stop_reason: Expected None, Got {response.stop_reason}")
1298
+ assert response.stop_reason is None
1299
+
1300
+
1301
+ # ==================================================================================================
1302
+ # Tests for Streaming Event Models
1303
+ # ==================================================================================================
1304
+
1305
+ class TestStreamingEvents:
1306
+ """Tests for streaming event Pydantic models."""
1307
+
1308
+ def test_message_start_event(self):
1309
+ """
1310
+ What it does: Verifies creation of MessageStartEvent.
1311
+ Purpose: Ensure message_start event works.
1312
+ """
1313
+ print("Setup: Creating MessageStartEvent...")
1314
+ event = MessageStartEvent(
1315
+ message={"id": "msg_1", "type": "message", "role": "assistant"}
1316
+ )
1317
+
1318
+ print(f"Result: {event}")
1319
+ print(f"Comparing type: Expected 'message_start', Got '{event.type}'")
1320
+ assert event.type == "message_start"
1321
+ assert event.message["id"] == "msg_1"
1322
+
1323
+ def test_content_block_start_event(self):
1324
+ """
1325
+ What it does: Verifies creation of ContentBlockStartEvent.
1326
+ Purpose: Ensure content_block_start event works.
1327
+ """
1328
+ print("Setup: Creating ContentBlockStartEvent...")
1329
+ event = ContentBlockStartEvent(
1330
+ index=0,
1331
+ content_block={"type": "text", "text": ""}
1332
+ )
1333
+
1334
+ print(f"Result: {event}")
1335
+ print(f"Comparing type: Expected 'content_block_start', Got '{event.type}'")
1336
+ assert event.type == "content_block_start"
1337
+ assert event.index == 0
1338
+
1339
+ def test_text_delta(self):
1340
+ """
1341
+ What it does: Verifies creation of TextDelta.
1342
+ Purpose: Ensure text_delta works.
1343
+ """
1344
+ print("Setup: Creating TextDelta...")
1345
+ delta = TextDelta(text="Hello")
1346
+
1347
+ print(f"Result: {delta}")
1348
+ print(f"Comparing type: Expected 'text_delta', Got '{delta.type}'")
1349
+ assert delta.type == "text_delta"
1350
+ assert delta.text == "Hello"
1351
+
1352
+ def test_thinking_delta(self):
1353
+ """
1354
+ What it does: Verifies creation of ThinkingDelta.
1355
+ Purpose: Ensure thinking_delta works.
1356
+ """
1357
+ print("Setup: Creating ThinkingDelta...")
1358
+ delta = ThinkingDelta(thinking="Let me think...")
1359
+
1360
+ print(f"Result: {delta}")
1361
+ print(f"Comparing type: Expected 'thinking_delta', Got '{delta.type}'")
1362
+ assert delta.type == "thinking_delta"
1363
+ assert delta.thinking == "Let me think..."
1364
+
1365
+ def test_input_json_delta(self):
1366
+ """
1367
+ What it does: Verifies creation of InputJsonDelta.
1368
+ Purpose: Ensure input_json_delta works.
1369
+ """
1370
+ print("Setup: Creating InputJsonDelta...")
1371
+ delta = InputJsonDelta(partial_json='{"loc')
1372
+
1373
+ print(f"Result: {delta}")
1374
+ print(f"Comparing type: Expected 'input_json_delta', Got '{delta.type}'")
1375
+ assert delta.type == "input_json_delta"
1376
+ assert delta.partial_json == '{"loc'
1377
+
1378
+ def test_content_block_delta_event(self):
1379
+ """
1380
+ What it does: Verifies creation of ContentBlockDeltaEvent.
1381
+ Purpose: Ensure content_block_delta event works.
1382
+ """
1383
+ print("Setup: Creating ContentBlockDeltaEvent...")
1384
+ event = ContentBlockDeltaEvent(
1385
+ index=0,
1386
+ delta=TextDelta(text="Hello")
1387
+ )
1388
+
1389
+ print(f"Result: {event}")
1390
+ print(f"Comparing type: Expected 'content_block_delta', Got '{event.type}'")
1391
+ assert event.type == "content_block_delta"
1392
+ assert event.index == 0
1393
+
1394
+ def test_content_block_stop_event(self):
1395
+ """
1396
+ What it does: Verifies creation of ContentBlockStopEvent.
1397
+ Purpose: Ensure content_block_stop event works.
1398
+ """
1399
+ print("Setup: Creating ContentBlockStopEvent...")
1400
+ event = ContentBlockStopEvent(index=0)
1401
+
1402
+ print(f"Result: {event}")
1403
+ print(f"Comparing type: Expected 'content_block_stop', Got '{event.type}'")
1404
+ assert event.type == "content_block_stop"
1405
+ assert event.index == 0
1406
+
1407
+ def test_message_delta_event(self):
1408
+ """
1409
+ What it does: Verifies creation of MessageDeltaEvent.
1410
+ Purpose: Ensure message_delta event works.
1411
+ """
1412
+ print("Setup: Creating MessageDeltaEvent...")
1413
+ event = MessageDeltaEvent(
1414
+ delta={"stop_reason": "end_turn"},
1415
+ usage=MessageDeltaUsage(output_tokens=10)
1416
+ )
1417
+
1418
+ print(f"Result: {event}")
1419
+ print(f"Comparing type: Expected 'message_delta', Got '{event.type}'")
1420
+ assert event.type == "message_delta"
1421
+ assert event.delta["stop_reason"] == "end_turn"
1422
+
1423
+ def test_message_stop_event(self):
1424
+ """
1425
+ What it does: Verifies creation of MessageStopEvent.
1426
+ Purpose: Ensure message_stop event works.
1427
+ """
1428
+ print("Setup: Creating MessageStopEvent...")
1429
+ event = MessageStopEvent()
1430
+
1431
+ print(f"Result: {event}")
1432
+ print(f"Comparing type: Expected 'message_stop', Got '{event.type}'")
1433
+ assert event.type == "message_stop"
1434
+
1435
+ def test_ping_event(self):
1436
+ """
1437
+ What it does: Verifies creation of PingEvent.
1438
+ Purpose: Ensure ping event works.
1439
+ """
1440
+ print("Setup: Creating PingEvent...")
1441
+ event = PingEvent()
1442
+
1443
+ print(f"Result: {event}")
1444
+ print(f"Comparing type: Expected 'ping', Got '{event.type}'")
1445
+ assert event.type == "ping"
1446
+
1447
+ def test_error_event(self):
1448
+ """
1449
+ What it does: Verifies creation of ErrorEvent.
1450
+ Purpose: Ensure error event works.
1451
+ """
1452
+ print("Setup: Creating ErrorEvent...")
1453
+ event = ErrorEvent(error={"type": "invalid_request", "message": "Bad request"})
1454
+
1455
+ print(f"Result: {event}")
1456
+ print(f"Comparing type: Expected 'error', Got '{event.type}'")
1457
+ assert event.type == "error"
1458
+ assert event.error["type"] == "invalid_request"
1459
+
1460
+
1461
+ # ==================================================================================================
1462
+ # Tests for Error Models
1463
+ # ==================================================================================================
1464
+
1465
+ class TestErrorModels:
1466
+ """Tests for error Pydantic models."""
1467
+
1468
+ def test_anthropic_error_detail(self):
1469
+ """
1470
+ What it does: Verifies creation of AnthropicErrorDetail.
1471
+ Purpose: Ensure error detail model works.
1472
+ """
1473
+ print("Setup: Creating AnthropicErrorDetail...")
1474
+ detail = AnthropicErrorDetail(
1475
+ type="invalid_request_error",
1476
+ message="Invalid API key"
1477
+ )
1478
+
1479
+ print(f"Result: {detail}")
1480
+ print(f"Comparing type: Expected 'invalid_request_error', Got '{detail.type}'")
1481
+ assert detail.type == "invalid_request_error"
1482
+
1483
+ print(f"Comparing message: Got '{detail.message}'")
1484
+ assert detail.message == "Invalid API key"
1485
+
1486
+ def test_anthropic_error_response(self):
1487
+ """
1488
+ What it does: Verifies creation of AnthropicErrorResponse.
1489
+ Purpose: Ensure error response model works.
1490
+ """
1491
+ print("Setup: Creating AnthropicErrorResponse...")
1492
+ response = AnthropicErrorResponse(
1493
+ error=AnthropicErrorDetail(
1494
+ type="authentication_error",
1495
+ message="Invalid API key provided"
1496
+ )
1497
+ )
1498
+
1499
+ print(f"Result: {response}")
1500
+ print(f"Comparing type: Expected 'error', Got '{response.type}'")
1501
+ assert response.type == "error"
1502
+
1503
+ print(f"Comparing error.type: Got '{response.error.type}'")
1504
+ assert response.error.type == "authentication_error"
tests/unit/test_models_openai.py ADDED
@@ -0,0 +1,1029 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ Unit tests for OpenAI Pydantic models.
5
+
6
+ Comprehensive tests for all OpenAI-compatible API models:
7
+ - Model listing (OpenAIModel, ModelList)
8
+ - Chat messages (ChatMessage)
9
+ - Tools (ToolFunction, Tool)
10
+ - Requests (ChatCompletionRequest)
11
+ - Responses (ChatCompletionChoice, ChatCompletionUsage, ChatCompletionResponse)
12
+ - Streaming (ChatCompletionChunk, ChatCompletionChunkChoice, ChatCompletionChunkDelta)
13
+ """
14
+
15
+ import pytest
16
+ from pydantic import ValidationError
17
+
18
+ from kiro.models_openai import (
19
+ # Model listing
20
+ OpenAIModel,
21
+ ModelList,
22
+ # Chat messages
23
+ ChatMessage,
24
+ # Tools
25
+ ToolFunction,
26
+ Tool,
27
+ # Requests
28
+ ChatCompletionRequest,
29
+ # Responses
30
+ ChatCompletionChoice,
31
+ ChatCompletionUsage,
32
+ ChatCompletionResponse,
33
+ # Streaming
34
+ ChatCompletionChunkDelta,
35
+ ChatCompletionChunkChoice,
36
+ ChatCompletionChunk,
37
+ )
38
+
39
+
40
+ # ==================================================================================================
41
+ # Tests for OpenAIModel
42
+ # ==================================================================================================
43
+
44
+ class TestOpenAIModel:
45
+ """Tests for OpenAIModel Pydantic model."""
46
+
47
+ def test_valid_model(self):
48
+ """
49
+ What it does: Verifies creation of valid OpenAIModel.
50
+ Purpose: Ensure model accepts valid data.
51
+ """
52
+ print("Setup: Creating OpenAIModel with valid data...")
53
+ model = OpenAIModel(
54
+ id="claude-sonnet-4-5",
55
+ description="Claude Sonnet 4.5 model"
56
+ )
57
+
58
+ print(f"Result: {model}")
59
+ print(f"Comparing id: Expected 'claude-sonnet-4-5', Got '{model.id}'")
60
+ assert model.id == "claude-sonnet-4-5"
61
+
62
+ print(f"Comparing object: Expected 'model', Got '{model.object}'")
63
+ assert model.object == "model"
64
+
65
+ print(f"Comparing owned_by: Expected 'anthropic', Got '{model.owned_by}'")
66
+ assert model.owned_by == "anthropic"
67
+
68
+ print(f"Comparing description: Got '{model.description}'")
69
+ assert model.description == "Claude Sonnet 4.5 model"
70
+
71
+ def test_requires_id(self):
72
+ """
73
+ What it does: Verifies that id is required.
74
+ Purpose: Ensure validation fails without id.
75
+ """
76
+ print("Setup: Attempting to create OpenAIModel without id...")
77
+
78
+ print("Action: Creating model (should raise ValidationError)...")
79
+ with pytest.raises(ValidationError) as exc_info:
80
+ OpenAIModel()
81
+
82
+ print(f"ValidationError raised: {exc_info.value}")
83
+ assert "id" in str(exc_info.value)
84
+
85
+ def test_object_defaults_to_model(self):
86
+ """
87
+ What it does: Verifies that object defaults to "model".
88
+ Purpose: Ensure default value is set correctly.
89
+ """
90
+ print("Setup: Creating OpenAIModel without explicit object...")
91
+ model = OpenAIModel(id="test-model")
92
+
93
+ print(f"Comparing object: Expected 'model', Got '{model.object}'")
94
+ assert model.object == "model"
95
+
96
+ def test_owned_by_defaults_to_anthropic(self):
97
+ """
98
+ What it does: Verifies that owned_by defaults to "anthropic".
99
+ Purpose: Ensure default value is set correctly.
100
+ """
101
+ print("Setup: Creating OpenAIModel without explicit owned_by...")
102
+ model = OpenAIModel(id="test-model")
103
+
104
+ print(f"Comparing owned_by: Expected 'anthropic', Got '{model.owned_by}'")
105
+ assert model.owned_by == "anthropic"
106
+
107
+ def test_created_is_auto_generated(self):
108
+ """
109
+ What it does: Verifies that created timestamp is auto-generated.
110
+ Purpose: Ensure timestamp is set automatically.
111
+ """
112
+ print("Setup: Creating OpenAIModel without explicit created...")
113
+ model = OpenAIModel(id="test-model")
114
+
115
+ print(f"Comparing created: Got {model.created}")
116
+ assert model.created > 0
117
+ assert isinstance(model.created, int)
118
+
119
+ def test_description_is_optional(self):
120
+ """
121
+ What it does: Verifies that description is optional.
122
+ Purpose: Ensure models without description work.
123
+ """
124
+ print("Setup: Creating OpenAIModel without description...")
125
+ model = OpenAIModel(id="test-model")
126
+
127
+ print(f"Comparing description: Expected None, Got {model.description}")
128
+ assert model.description is None
129
+
130
+
131
+ # ==================================================================================================
132
+ # Tests for ModelList
133
+ # ==================================================================================================
134
+
135
+ class TestModelList:
136
+ """Tests for ModelList Pydantic model."""
137
+
138
+ def test_valid_model_list(self):
139
+ """
140
+ What it does: Verifies creation of valid ModelList.
141
+ Purpose: Ensure model list accepts valid data.
142
+ """
143
+ print("Setup: Creating ModelList with valid data...")
144
+ model_list = ModelList(
145
+ data=[
146
+ OpenAIModel(id="claude-sonnet-4-5"),
147
+ OpenAIModel(id="claude-opus-4")
148
+ ]
149
+ )
150
+
151
+ print(f"Result: {model_list}")
152
+ print(f"Comparing object: Expected 'list', Got '{model_list.object}'")
153
+ assert model_list.object == "list"
154
+
155
+ print(f"Comparing data length: Expected 2, Got {len(model_list.data)}")
156
+ assert len(model_list.data) == 2
157
+
158
+ def test_object_defaults_to_list(self):
159
+ """
160
+ What it does: Verifies that object defaults to "list".
161
+ Purpose: Ensure default value is set correctly.
162
+ """
163
+ print("Setup: Creating ModelList without explicit object...")
164
+ model_list = ModelList(data=[])
165
+
166
+ print(f"Comparing object: Expected 'list', Got '{model_list.object}'")
167
+ assert model_list.object == "list"
168
+
169
+ def test_requires_data(self):
170
+ """
171
+ What it does: Verifies that data is required.
172
+ Purpose: Ensure validation fails without data.
173
+ """
174
+ print("Setup: Attempting to create ModelList without data...")
175
+
176
+ print("Action: Creating model (should raise ValidationError)...")
177
+ with pytest.raises(ValidationError) as exc_info:
178
+ ModelList()
179
+
180
+ print(f"ValidationError raised: {exc_info.value}")
181
+ assert "data" in str(exc_info.value)
182
+
183
+ def test_accepts_empty_list(self):
184
+ """
185
+ What it does: Verifies that empty list is accepted.
186
+ Purpose: Ensure empty model list works.
187
+ """
188
+ print("Setup: Creating ModelList with empty data...")
189
+ model_list = ModelList(data=[])
190
+
191
+ print(f"Comparing data: Expected [], Got {model_list.data}")
192
+ assert model_list.data == []
193
+
194
+
195
+ # ==================================================================================================
196
+ # Tests for ChatMessage
197
+ # ==================================================================================================
198
+
199
+ class TestChatMessage:
200
+ """Tests for ChatMessage Pydantic model."""
201
+
202
+ def test_valid_user_message(self):
203
+ """
204
+ What it does: Verifies creation of valid user message.
205
+ Purpose: Ensure model accepts valid user message.
206
+ """
207
+ print("Setup: Creating ChatMessage with user role...")
208
+ message = ChatMessage(role="user", content="Hello!")
209
+
210
+ print(f"Result: {message}")
211
+ print(f"Comparing role: Expected 'user', Got '{message.role}'")
212
+ assert message.role == "user"
213
+
214
+ print(f"Comparing content: Expected 'Hello!', Got '{message.content}'")
215
+ assert message.content == "Hello!"
216
+
217
+ def test_valid_assistant_message(self):
218
+ """
219
+ What it does: Verifies creation of valid assistant message.
220
+ Purpose: Ensure model accepts valid assistant message.
221
+ """
222
+ print("Setup: Creating ChatMessage with assistant role...")
223
+ message = ChatMessage(role="assistant", content="Hi there!")
224
+
225
+ print(f"Result: {message}")
226
+ print(f"Comparing role: Expected 'assistant', Got '{message.role}'")
227
+ assert message.role == "assistant"
228
+
229
+ def test_valid_system_message(self):
230
+ """
231
+ What it does: Verifies creation of valid system message.
232
+ Purpose: Ensure model accepts valid system message.
233
+ """
234
+ print("Setup: Creating ChatMessage with system role...")
235
+ message = ChatMessage(role="system", content="You are helpful.")
236
+
237
+ print(f"Result: {message}")
238
+ print(f"Comparing role: Expected 'system', Got '{message.role}'")
239
+ assert message.role == "system"
240
+
241
+ def test_valid_tool_message(self):
242
+ """
243
+ What it does: Verifies creation of valid tool message.
244
+ Purpose: Ensure model accepts valid tool message.
245
+ """
246
+ print("Setup: Creating ChatMessage with tool role...")
247
+ message = ChatMessage(
248
+ role="tool",
249
+ content="Tool result",
250
+ tool_call_id="call_123"
251
+ )
252
+
253
+ print(f"Result: {message}")
254
+ print(f"Comparing role: Expected 'tool', Got '{message.role}'")
255
+ assert message.role == "tool"
256
+
257
+ print(f"Comparing tool_call_id: Expected 'call_123', Got '{message.tool_call_id}'")
258
+ assert message.tool_call_id == "call_123"
259
+
260
+ def test_requires_role(self):
261
+ """
262
+ What it does: Verifies that role is required.
263
+ Purpose: Ensure validation fails without role.
264
+ """
265
+ print("Setup: Attempting to create ChatMessage without role...")
266
+
267
+ print("Action: Creating model (should raise ValidationError)...")
268
+ with pytest.raises(ValidationError) as exc_info:
269
+ ChatMessage(content="Hello")
270
+
271
+ print(f"ValidationError raised: {exc_info.value}")
272
+ assert "role" in str(exc_info.value)
273
+
274
+ def test_content_is_optional(self):
275
+ """
276
+ What it does: Verifies that content is optional.
277
+ Purpose: Ensure messages without content work (e.g., tool calls).
278
+ """
279
+ print("Setup: Creating ChatMessage without content...")
280
+ message = ChatMessage(role="assistant")
281
+
282
+ print(f"Comparing content: Expected None, Got {message.content}")
283
+ assert message.content is None
284
+
285
+ def test_accepts_list_content(self):
286
+ """
287
+ What it does: Verifies that list content is accepted.
288
+ Purpose: Ensure multimodal content works.
289
+ """
290
+ print("Setup: Creating ChatMessage with list content...")
291
+ message = ChatMessage(
292
+ role="user",
293
+ content=[
294
+ {"type": "text", "text": "What's in this image?"},
295
+ {"type": "image_url", "image_url": {"url": "https://example.com/img.jpg"}}
296
+ ]
297
+ )
298
+
299
+ print(f"Result: {message}")
300
+ print(f"Comparing content type: Expected list, Got {type(message.content)}")
301
+ assert isinstance(message.content, list)
302
+ assert len(message.content) == 2
303
+
304
+ def test_accepts_tool_calls(self):
305
+ """
306
+ What it does: Verifies that tool_calls is accepted.
307
+ Purpose: Ensure assistant messages with tool calls work.
308
+ """
309
+ print("Setup: Creating ChatMessage with tool_calls...")
310
+ message = ChatMessage(
311
+ role="assistant",
312
+ content="I'll call a tool",
313
+ tool_calls=[{
314
+ "id": "call_123",
315
+ "type": "function",
316
+ "function": {"name": "get_weather", "arguments": '{"location": "Moscow"}'}
317
+ }]
318
+ )
319
+
320
+ print(f"Result: {message}")
321
+ print(f"Comparing tool_calls: Got {message.tool_calls}")
322
+ assert message.tool_calls is not None
323
+ assert len(message.tool_calls) == 1
324
+
325
+ def test_name_is_optional(self):
326
+ """
327
+ What it does: Verifies that name is optional.
328
+ Purpose: Ensure messages without name work.
329
+ """
330
+ print("Setup: Creating ChatMessage without name...")
331
+ message = ChatMessage(role="user", content="Hello")
332
+
333
+ print(f"Comparing name: Expected None, Got {message.name}")
334
+ assert message.name is None
335
+
336
+ def test_accepts_name(self):
337
+ """
338
+ What it does: Verifies that name is accepted.
339
+ Purpose: Ensure named messages work.
340
+ """
341
+ print("Setup: Creating ChatMessage with name...")
342
+ message = ChatMessage(role="user", content="Hello", name="John")
343
+
344
+ print(f"Comparing name: Expected 'John', Got '{message.name}'")
345
+ assert message.name == "John"
346
+
347
+ def test_extra_fields_allowed(self):
348
+ """
349
+ What it does: Verifies that extra fields are allowed.
350
+ Purpose: Ensure model_config extra="allow" works.
351
+ """
352
+ print("Setup: Creating ChatMessage with extra field...")
353
+ message = ChatMessage(role="user", content="Hello", custom_field="value")
354
+
355
+ print(f"Comparing custom_field: Got '{message.custom_field}'")
356
+ assert message.custom_field == "value"
357
+
358
+
359
+ # ==================================================================================================
360
+ # Tests for ToolFunction
361
+ # ==================================================================================================
362
+
363
+ class TestToolFunction:
364
+ """Tests for ToolFunction Pydantic model."""
365
+
366
+ def test_valid_tool_function(self):
367
+ """
368
+ What it does: Verifies creation of valid ToolFunction.
369
+ Purpose: Ensure model accepts valid tool function.
370
+ """
371
+ print("Setup: Creating ToolFunction with valid data...")
372
+ func = ToolFunction(
373
+ name="get_weather",
374
+ description="Get weather for a location",
375
+ parameters={
376
+ "type": "object",
377
+ "properties": {"location": {"type": "string"}}
378
+ }
379
+ )
380
+
381
+ print(f"Result: {func}")
382
+ print(f"Comparing name: Expected 'get_weather', Got '{func.name}'")
383
+ assert func.name == "get_weather"
384
+
385
+ print(f"Comparing description: Got '{func.description}'")
386
+ assert func.description == "Get weather for a location"
387
+
388
+ print(f"Comparing parameters: Got {func.parameters}")
389
+ assert "properties" in func.parameters
390
+
391
+ def test_requires_name(self):
392
+ """
393
+ What it does: Verifies that name is required.
394
+ Purpose: Ensure validation fails without name.
395
+ """
396
+ print("Setup: Attempting to create ToolFunction without name...")
397
+
398
+ print("Action: Creating model (should raise ValidationError)...")
399
+ with pytest.raises(ValidationError) as exc_info:
400
+ ToolFunction(description="Test")
401
+
402
+ print(f"ValidationError raised: {exc_info.value}")
403
+ assert "name" in str(exc_info.value)
404
+
405
+ def test_description_is_optional(self):
406
+ """
407
+ What it does: Verifies that description is optional.
408
+ Purpose: Ensure functions without description work.
409
+ """
410
+ print("Setup: Creating ToolFunction without description...")
411
+ func = ToolFunction(name="test_func")
412
+
413
+ print(f"Comparing description: Expected None, Got {func.description}")
414
+ assert func.description is None
415
+
416
+ def test_parameters_is_optional(self):
417
+ """
418
+ What it does: Verifies that parameters is optional.
419
+ Purpose: Ensure functions without parameters work.
420
+ """
421
+ print("Setup: Creating ToolFunction without parameters...")
422
+ func = ToolFunction(name="no_params_func")
423
+
424
+ print(f"Comparing parameters: Expected None, Got {func.parameters}")
425
+ assert func.parameters is None
426
+
427
+
428
+ # ==================================================================================================
429
+ # Tests for Tool
430
+ # ==================================================================================================
431
+
432
+ class TestTool:
433
+ """Tests for Tool Pydantic model."""
434
+
435
+ def test_valid_tool(self):
436
+ """
437
+ What it does: Verifies creation of valid Tool.
438
+ Purpose: Ensure model accepts valid tool.
439
+ """
440
+ print("Setup: Creating Tool with valid data...")
441
+ tool = Tool(
442
+ type="function",
443
+ function=ToolFunction(
444
+ name="get_weather",
445
+ description="Get weather",
446
+ parameters={}
447
+ )
448
+ )
449
+
450
+ print(f"Result: {tool}")
451
+ print(f"Comparing type: Expected 'function', Got '{tool.type}'")
452
+ assert tool.type == "function"
453
+
454
+ print(f"Comparing function.name: Expected 'get_weather', Got '{tool.function.name}'")
455
+ assert tool.function.name == "get_weather"
456
+
457
+ def test_type_defaults_to_function(self):
458
+ """
459
+ What it does: Verifies that type defaults to "function".
460
+ Purpose: Ensure default value is set correctly.
461
+ """
462
+ print("Setup: Creating Tool without explicit type...")
463
+ tool = Tool(function=ToolFunction(name="test"))
464
+
465
+ print(f"Comparing type: Expected 'function', Got '{tool.type}'")
466
+ assert tool.type == "function"
467
+
468
+ def test_requires_function(self):
469
+ """
470
+ What it does: Verifies that function is required.
471
+ Purpose: Ensure validation fails without function.
472
+ """
473
+ print("Setup: Attempting to create Tool without function...")
474
+
475
+ print("Action: Creating model (should raise ValidationError)...")
476
+ with pytest.raises(ValidationError) as exc_info:
477
+ Tool(type="function")
478
+
479
+ print(f"ValidationError raised: {exc_info.value}")
480
+ assert "function" in str(exc_info.value)
481
+
482
+
483
+ # ==================================================================================================
484
+ # Tests for ChatCompletionRequest
485
+ # ==================================================================================================
486
+
487
+ class TestChatCompletionRequest:
488
+ """Tests for ChatCompletionRequest Pydantic model."""
489
+
490
+ def test_valid_request(self):
491
+ """
492
+ What it does: Verifies creation of valid ChatCompletionRequest.
493
+ Purpose: Ensure model accepts valid request.
494
+ """
495
+ print("Setup: Creating ChatCompletionRequest with valid data...")
496
+ request = ChatCompletionRequest(
497
+ model="claude-sonnet-4-5",
498
+ messages=[ChatMessage(role="user", content="Hello")]
499
+ )
500
+
501
+ print(f"Result: {request}")
502
+ print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{request.model}'")
503
+ assert request.model == "claude-sonnet-4-5"
504
+
505
+ print(f"Comparing messages length: Expected 1, Got {len(request.messages)}")
506
+ assert len(request.messages) == 1
507
+
508
+ print(f"Comparing stream: Expected False, Got {request.stream}")
509
+ assert request.stream is False
510
+
511
+ def test_requires_model(self):
512
+ """
513
+ What it does: Verifies that model is required.
514
+ Purpose: Ensure validation fails without model.
515
+ """
516
+ print("Setup: Attempting to create ChatCompletionRequest without model...")
517
+
518
+ print("Action: Creating model (should raise ValidationError)...")
519
+ with pytest.raises(ValidationError) as exc_info:
520
+ ChatCompletionRequest(messages=[ChatMessage(role="user", content="Hi")])
521
+
522
+ print(f"ValidationError raised: {exc_info.value}")
523
+ assert "model" in str(exc_info.value)
524
+
525
+ def test_requires_messages(self):
526
+ """
527
+ What it does: Verifies that messages is required.
528
+ Purpose: Ensure validation fails without messages.
529
+ """
530
+ print("Setup: Attempting to create ChatCompletionRequest without messages...")
531
+
532
+ print("Action: Creating model (should raise ValidationError)...")
533
+ with pytest.raises(ValidationError) as exc_info:
534
+ ChatCompletionRequest(model="claude-sonnet-4-5")
535
+
536
+ print(f"ValidationError raised: {exc_info.value}")
537
+ assert "messages" in str(exc_info.value)
538
+
539
+ def test_requires_at_least_one_message(self):
540
+ """
541
+ What it does: Verifies that at least one message is required.
542
+ Purpose: Ensure validation fails with empty messages.
543
+ """
544
+ print("Setup: Attempting to create ChatCompletionRequest with empty messages...")
545
+
546
+ print("Action: Creating model (should raise ValidationError)...")
547
+ with pytest.raises(ValidationError) as exc_info:
548
+ ChatCompletionRequest(model="claude-sonnet-4-5", messages=[])
549
+
550
+ print(f"ValidationError raised: {exc_info.value}")
551
+
552
+ def test_stream_defaults_to_false(self):
553
+ """
554
+ What it does: Verifies that stream defaults to False.
555
+ Purpose: Ensure default value is set correctly.
556
+ """
557
+ print("Setup: Creating ChatCompletionRequest without explicit stream...")
558
+ request = ChatCompletionRequest(
559
+ model="test",
560
+ messages=[ChatMessage(role="user", content="Hi")]
561
+ )
562
+
563
+ print(f"Comparing stream: Expected False, Got {request.stream}")
564
+ assert request.stream is False
565
+
566
+ def test_accepts_stream_true(self):
567
+ """
568
+ What it does: Verifies that stream=True is accepted.
569
+ Purpose: Ensure streaming requests work.
570
+ """
571
+ print("Setup: Creating ChatCompletionRequest with stream=True...")
572
+ request = ChatCompletionRequest(
573
+ model="test",
574
+ messages=[ChatMessage(role="user", content="Hi")],
575
+ stream=True
576
+ )
577
+
578
+ print(f"Comparing stream: Expected True, Got {request.stream}")
579
+ assert request.stream is True
580
+
581
+ def test_accepts_tools(self):
582
+ """
583
+ What it does: Verifies that tools are accepted.
584
+ Purpose: Ensure function calling works.
585
+ """
586
+ print("Setup: Creating ChatCompletionRequest with tools...")
587
+ request = ChatCompletionRequest(
588
+ model="test",
589
+ messages=[ChatMessage(role="user", content="Hi")],
590
+ tools=[Tool(function=ToolFunction(name="test_tool"))]
591
+ )
592
+
593
+ print(f"Comparing tools: Got {request.tools}")
594
+ assert request.tools is not None
595
+ assert len(request.tools) == 1
596
+
597
+ def test_accepts_generation_parameters(self):
598
+ """
599
+ What it does: Verifies that generation parameters are accepted.
600
+ Purpose: Ensure temperature, top_p, max_tokens work.
601
+ """
602
+ print("Setup: Creating ChatCompletionRequest with generation params...")
603
+ request = ChatCompletionRequest(
604
+ model="test",
605
+ messages=[ChatMessage(role="user", content="Hi")],
606
+ temperature=0.7,
607
+ top_p=0.9,
608
+ max_tokens=1000
609
+ )
610
+
611
+ print(f"Comparing temperature: Expected 0.7, Got {request.temperature}")
612
+ assert request.temperature == 0.7
613
+
614
+ print(f"Comparing top_p: Expected 0.9, Got {request.top_p}")
615
+ assert request.top_p == 0.9
616
+
617
+ print(f"Comparing max_tokens: Expected 1000, Got {request.max_tokens}")
618
+ assert request.max_tokens == 1000
619
+
620
+
621
+ # ==================================================================================================
622
+ # Tests for ChatCompletionUsage
623
+ # ==================================================================================================
624
+
625
+ class TestChatCompletionUsage:
626
+ """Tests for ChatCompletionUsage Pydantic model."""
627
+
628
+ def test_valid_usage(self):
629
+ """
630
+ What it does: Verifies creation of valid ChatCompletionUsage.
631
+ Purpose: Ensure model accepts valid usage data.
632
+ """
633
+ print("Setup: Creating ChatCompletionUsage with valid data...")
634
+ usage = ChatCompletionUsage(
635
+ prompt_tokens=100,
636
+ completion_tokens=50,
637
+ total_tokens=150
638
+ )
639
+
640
+ print(f"Result: {usage}")
641
+ print(f"Comparing prompt_tokens: Expected 100, Got {usage.prompt_tokens}")
642
+ assert usage.prompt_tokens == 100
643
+
644
+ print(f"Comparing completion_tokens: Expected 50, Got {usage.completion_tokens}")
645
+ assert usage.completion_tokens == 50
646
+
647
+ print(f"Comparing total_tokens: Expected 150, Got {usage.total_tokens}")
648
+ assert usage.total_tokens == 150
649
+
650
+ def test_defaults_to_zero(self):
651
+ """
652
+ What it does: Verifies that all fields default to 0.
653
+ Purpose: Ensure default values are set correctly.
654
+ """
655
+ print("Setup: Creating ChatCompletionUsage without explicit values...")
656
+ usage = ChatCompletionUsage()
657
+
658
+ print(f"Comparing prompt_tokens: Expected 0, Got {usage.prompt_tokens}")
659
+ assert usage.prompt_tokens == 0
660
+
661
+ print(f"Comparing completion_tokens: Expected 0, Got {usage.completion_tokens}")
662
+ assert usage.completion_tokens == 0
663
+
664
+ print(f"Comparing total_tokens: Expected 0, Got {usage.total_tokens}")
665
+ assert usage.total_tokens == 0
666
+
667
+ def test_credits_used_is_optional(self):
668
+ """
669
+ What it does: Verifies that credits_used is optional.
670
+ Purpose: Ensure Kiro-specific field is optional.
671
+ """
672
+ print("Setup: Creating ChatCompletionUsage without credits_used...")
673
+ usage = ChatCompletionUsage()
674
+
675
+ print(f"Comparing credits_used: Expected None, Got {usage.credits_used}")
676
+ assert usage.credits_used is None
677
+
678
+
679
+ # ==================================================================================================
680
+ # Tests for ChatCompletionChoice
681
+ # ==================================================================================================
682
+
683
+ class TestChatCompletionChoice:
684
+ """Tests for ChatCompletionChoice Pydantic model."""
685
+
686
+ def test_valid_choice(self):
687
+ """
688
+ What it does: Verifies creation of valid ChatCompletionChoice.
689
+ Purpose: Ensure model accepts valid choice data.
690
+ """
691
+ print("Setup: Creating ChatCompletionChoice with valid data...")
692
+ choice = ChatCompletionChoice(
693
+ index=0,
694
+ message={"role": "assistant", "content": "Hello!"},
695
+ finish_reason="stop"
696
+ )
697
+
698
+ print(f"Result: {choice}")
699
+ print(f"Comparing index: Expected 0, Got {choice.index}")
700
+ assert choice.index == 0
701
+
702
+ print(f"Comparing message: Got {choice.message}")
703
+ assert choice.message["role"] == "assistant"
704
+
705
+ print(f"Comparing finish_reason: Expected 'stop', Got '{choice.finish_reason}'")
706
+ assert choice.finish_reason == "stop"
707
+
708
+ def test_index_defaults_to_zero(self):
709
+ """
710
+ What it does: Verifies that index defaults to 0.
711
+ Purpose: Ensure default value is set correctly.
712
+ """
713
+ print("Setup: Creating ChatCompletionChoice without explicit index...")
714
+ choice = ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})
715
+
716
+ print(f"Comparing index: Expected 0, Got {choice.index}")
717
+ assert choice.index == 0
718
+
719
+ def test_requires_message(self):
720
+ """
721
+ What it does: Verifies that message is required.
722
+ Purpose: Ensure validation fails without message.
723
+ """
724
+ print("Setup: Attempting to create ChatCompletionChoice without message...")
725
+
726
+ print("Action: Creating model (should raise ValidationError)...")
727
+ with pytest.raises(ValidationError) as exc_info:
728
+ ChatCompletionChoice(index=0, finish_reason="stop")
729
+
730
+ print(f"ValidationError raised: {exc_info.value}")
731
+ assert "message" in str(exc_info.value)
732
+
733
+ def test_finish_reason_is_optional(self):
734
+ """
735
+ What it does: Verifies that finish_reason is optional.
736
+ Purpose: Ensure choices without finish_reason work.
737
+ """
738
+ print("Setup: Creating ChatCompletionChoice without finish_reason...")
739
+ choice = ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})
740
+
741
+ print(f"Comparing finish_reason: Expected None, Got {choice.finish_reason}")
742
+ assert choice.finish_reason is None
743
+
744
+
745
+ # ==================================================================================================
746
+ # Tests for ChatCompletionResponse
747
+ # ==================================================================================================
748
+
749
+ class TestChatCompletionResponse:
750
+ """Tests for ChatCompletionResponse Pydantic model."""
751
+
752
+ def test_valid_response(self):
753
+ """
754
+ What it does: Verifies creation of valid ChatCompletionResponse.
755
+ Purpose: Ensure model accepts valid response data.
756
+ """
757
+ print("Setup: Creating ChatCompletionResponse with valid data...")
758
+ response = ChatCompletionResponse(
759
+ id="chatcmpl-123",
760
+ model="claude-sonnet-4-5",
761
+ choices=[ChatCompletionChoice(
762
+ message={"role": "assistant", "content": "Hello!"},
763
+ finish_reason="stop"
764
+ )],
765
+ usage=ChatCompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
766
+ )
767
+
768
+ print(f"Result: {response}")
769
+ print(f"Comparing id: Expected 'chatcmpl-123', Got '{response.id}'")
770
+ assert response.id == "chatcmpl-123"
771
+
772
+ print(f"Comparing object: Expected 'chat.completion', Got '{response.object}'")
773
+ assert response.object == "chat.completion"
774
+
775
+ print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{response.model}'")
776
+ assert response.model == "claude-sonnet-4-5"
777
+
778
+ print(f"Comparing choices length: Expected 1, Got {len(response.choices)}")
779
+ assert len(response.choices) == 1
780
+
781
+ def test_object_defaults_to_chat_completion(self):
782
+ """
783
+ What it does: Verifies that object defaults to "chat.completion".
784
+ Purpose: Ensure default value is set correctly.
785
+ """
786
+ print("Setup: Creating ChatCompletionResponse without explicit object...")
787
+ response = ChatCompletionResponse(
788
+ id="test",
789
+ model="test",
790
+ choices=[ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})],
791
+ usage=ChatCompletionUsage()
792
+ )
793
+
794
+ print(f"Comparing object: Expected 'chat.completion', Got '{response.object}'")
795
+ assert response.object == "chat.completion"
796
+
797
+ def test_created_is_auto_generated(self):
798
+ """
799
+ What it does: Verifies that created timestamp is auto-generated.
800
+ Purpose: Ensure timestamp is set automatically.
801
+ """
802
+ print("Setup: Creating ChatCompletionResponse without explicit created...")
803
+ response = ChatCompletionResponse(
804
+ id="test",
805
+ model="test",
806
+ choices=[ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})],
807
+ usage=ChatCompletionUsage()
808
+ )
809
+
810
+ print(f"Comparing created: Got {response.created}")
811
+ assert response.created > 0
812
+ assert isinstance(response.created, int)
813
+
814
+ def test_requires_id(self):
815
+ """
816
+ What it does: Verifies that id is required.
817
+ Purpose: Ensure validation fails without id.
818
+ """
819
+ print("Setup: Attempting to create ChatCompletionResponse without id...")
820
+
821
+ print("Action: Creating model (should raise ValidationError)...")
822
+ with pytest.raises(ValidationError) as exc_info:
823
+ ChatCompletionResponse(
824
+ model="test",
825
+ choices=[ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})],
826
+ usage=ChatCompletionUsage()
827
+ )
828
+
829
+ print(f"ValidationError raised: {exc_info.value}")
830
+ assert "id" in str(exc_info.value)
831
+
832
+
833
+ # ==================================================================================================
834
+ # Tests for Streaming Models
835
+ # ==================================================================================================
836
+
837
+ class TestChatCompletionChunkDelta:
838
+ """Tests for ChatCompletionChunkDelta Pydantic model."""
839
+
840
+ def test_valid_delta_with_content(self):
841
+ """
842
+ What it does: Verifies creation of valid delta with content.
843
+ Purpose: Ensure model accepts content delta.
844
+ """
845
+ print("Setup: Creating ChatCompletionChunkDelta with content...")
846
+ delta = ChatCompletionChunkDelta(content="Hello")
847
+
848
+ print(f"Result: {delta}")
849
+ print(f"Comparing content: Expected 'Hello', Got '{delta.content}'")
850
+ assert delta.content == "Hello"
851
+
852
+ def test_valid_delta_with_role(self):
853
+ """
854
+ What it does: Verifies creation of valid delta with role.
855
+ Purpose: Ensure model accepts role delta (first chunk).
856
+ """
857
+ print("Setup: Creating ChatCompletionChunkDelta with role...")
858
+ delta = ChatCompletionChunkDelta(role="assistant")
859
+
860
+ print(f"Result: {delta}")
861
+ print(f"Comparing role: Expected 'assistant', Got '{delta.role}'")
862
+ assert delta.role == "assistant"
863
+
864
+ def test_all_fields_optional(self):
865
+ """
866
+ What it does: Verifies that all fields are optional.
867
+ Purpose: Ensure empty delta works.
868
+ """
869
+ print("Setup: Creating empty ChatCompletionChunkDelta...")
870
+ delta = ChatCompletionChunkDelta()
871
+
872
+ print(f"Comparing role: Expected None, Got {delta.role}")
873
+ assert delta.role is None
874
+
875
+ print(f"Comparing content: Expected None, Got {delta.content}")
876
+ assert delta.content is None
877
+
878
+ print(f"Comparing tool_calls: Expected None, Got {delta.tool_calls}")
879
+ assert delta.tool_calls is None
880
+
881
+ def test_accepts_tool_calls(self):
882
+ """
883
+ What it does: Verifies that tool_calls is accepted.
884
+ Purpose: Ensure streaming tool calls work.
885
+ """
886
+ print("Setup: Creating ChatCompletionChunkDelta with tool_calls...")
887
+ delta = ChatCompletionChunkDelta(
888
+ tool_calls=[{"index": 0, "id": "call_1", "function": {"name": "test"}}]
889
+ )
890
+
891
+ print(f"Comparing tool_calls: Got {delta.tool_calls}")
892
+ assert delta.tool_calls is not None
893
+ assert len(delta.tool_calls) == 1
894
+
895
+
896
+ class TestChatCompletionChunkChoice:
897
+ """Tests for ChatCompletionChunkChoice Pydantic model."""
898
+
899
+ def test_valid_chunk_choice(self):
900
+ """
901
+ What it does: Verifies creation of valid chunk choice.
902
+ Purpose: Ensure model accepts valid chunk choice.
903
+ """
904
+ print("Setup: Creating ChatCompletionChunkChoice with valid data...")
905
+ choice = ChatCompletionChunkChoice(
906
+ index=0,
907
+ delta=ChatCompletionChunkDelta(content="Hello")
908
+ )
909
+
910
+ print(f"Result: {choice}")
911
+ print(f"Comparing index: Expected 0, Got {choice.index}")
912
+ assert choice.index == 0
913
+
914
+ print(f"Comparing delta.content: Expected 'Hello', Got '{choice.delta.content}'")
915
+ assert choice.delta.content == "Hello"
916
+
917
+ def test_index_defaults_to_zero(self):
918
+ """
919
+ What it does: Verifies that index defaults to 0.
920
+ Purpose: Ensure default value is set correctly.
921
+ """
922
+ print("Setup: Creating ChatCompletionChunkChoice without explicit index...")
923
+ choice = ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta())
924
+
925
+ print(f"Comparing index: Expected 0, Got {choice.index}")
926
+ assert choice.index == 0
927
+
928
+ def test_finish_reason_is_optional(self):
929
+ """
930
+ What it does: Verifies that finish_reason is optional.
931
+ Purpose: Ensure intermediate chunks work.
932
+ """
933
+ print("Setup: Creating ChatCompletionChunkChoice without finish_reason...")
934
+ choice = ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta(content="Hi"))
935
+
936
+ print(f"Comparing finish_reason: Expected None, Got {choice.finish_reason}")
937
+ assert choice.finish_reason is None
938
+
939
+ def test_accepts_finish_reason(self):
940
+ """
941
+ What it does: Verifies that finish_reason is accepted.
942
+ Purpose: Ensure final chunk works.
943
+ """
944
+ print("Setup: Creating ChatCompletionChunkChoice with finish_reason...")
945
+ choice = ChatCompletionChunkChoice(
946
+ delta=ChatCompletionChunkDelta(),
947
+ finish_reason="stop"
948
+ )
949
+
950
+ print(f"Comparing finish_reason: Expected 'stop', Got '{choice.finish_reason}'")
951
+ assert choice.finish_reason == "stop"
952
+
953
+
954
+ class TestChatCompletionChunk:
955
+ """Tests for ChatCompletionChunk Pydantic model."""
956
+
957
+ def test_valid_chunk(self):
958
+ """
959
+ What it does: Verifies creation of valid chunk.
960
+ Purpose: Ensure model accepts valid chunk data.
961
+ """
962
+ print("Setup: Creating ChatCompletionChunk with valid data...")
963
+ chunk = ChatCompletionChunk(
964
+ id="chatcmpl-123",
965
+ model="claude-sonnet-4-5",
966
+ choices=[ChatCompletionChunkChoice(
967
+ delta=ChatCompletionChunkDelta(content="Hello")
968
+ )]
969
+ )
970
+
971
+ print(f"Result: {chunk}")
972
+ print(f"Comparing id: Expected 'chatcmpl-123', Got '{chunk.id}'")
973
+ assert chunk.id == "chatcmpl-123"
974
+
975
+ print(f"Comparing object: Expected 'chat.completion.chunk', Got '{chunk.object}'")
976
+ assert chunk.object == "chat.completion.chunk"
977
+
978
+ print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{chunk.model}'")
979
+ assert chunk.model == "claude-sonnet-4-5"
980
+
981
+ def test_object_defaults_to_chunk(self):
982
+ """
983
+ What it does: Verifies that object defaults to "chat.completion.chunk".
984
+ Purpose: Ensure default value is set correctly.
985
+ """
986
+ print("Setup: Creating ChatCompletionChunk without explicit object...")
987
+ chunk = ChatCompletionChunk(
988
+ id="test",
989
+ model="test",
990
+ choices=[ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta())]
991
+ )
992
+
993
+ print(f"Comparing object: Expected 'chat.completion.chunk', Got '{chunk.object}'")
994
+ assert chunk.object == "chat.completion.chunk"
995
+
996
+ def test_usage_is_optional(self):
997
+ """
998
+ What it does: Verifies that usage is optional.
999
+ Purpose: Ensure intermediate chunks work without usage.
1000
+ """
1001
+ print("Setup: Creating ChatCompletionChunk without usage...")
1002
+ chunk = ChatCompletionChunk(
1003
+ id="test",
1004
+ model="test",
1005
+ choices=[ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta())]
1006
+ )
1007
+
1008
+ print(f"Comparing usage: Expected None, Got {chunk.usage}")
1009
+ assert chunk.usage is None
1010
+
1011
+ def test_accepts_usage(self):
1012
+ """
1013
+ What it does: Verifies that usage is accepted.
1014
+ Purpose: Ensure final chunk with usage works.
1015
+ """
1016
+ print("Setup: Creating ChatCompletionChunk with usage...")
1017
+ chunk = ChatCompletionChunk(
1018
+ id="test",
1019
+ model="test",
1020
+ choices=[ChatCompletionChunkChoice(
1021
+ delta=ChatCompletionChunkDelta(),
1022
+ finish_reason="stop"
1023
+ )],
1024
+ usage=ChatCompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
1025
+ )
1026
+
1027
+ print(f"Comparing usage: Got {chunk.usage}")
1028
+ assert chunk.usage is not None
1029
+ assert chunk.usage.total_tokens == 15