jwadow
feat(openai): support Cursor flat format, inverted model names, and improve tool_results handling (#49)
936f798 | # -*- coding: utf-8 -*- | |
| """ | |
| Unit tests for OpenAI Pydantic models. | |
| Comprehensive tests for all OpenAI-compatible API models: | |
| - Model listing (OpenAIModel, ModelList) | |
| - Chat messages (ChatMessage) | |
| - Tools (ToolFunction, Tool) | |
| - Requests (ChatCompletionRequest) | |
| - Responses (ChatCompletionChoice, ChatCompletionUsage, ChatCompletionResponse) | |
| - Streaming (ChatCompletionChunk, ChatCompletionChunkChoice, ChatCompletionChunkDelta) | |
| """ | |
| import pytest | |
| from pydantic import ValidationError | |
| from kiro.models_openai import ( | |
| # Model listing | |
| OpenAIModel, | |
| ModelList, | |
| # Chat messages | |
| ChatMessage, | |
| # Tools | |
| ToolFunction, | |
| Tool, | |
| # Requests | |
| ChatCompletionRequest, | |
| # Responses | |
| ChatCompletionChoice, | |
| ChatCompletionUsage, | |
| ChatCompletionResponse, | |
| # Streaming | |
| ChatCompletionChunkDelta, | |
| ChatCompletionChunkChoice, | |
| ChatCompletionChunk, | |
| ) | |
| # ================================================================================================== | |
| # Tests for OpenAIModel | |
| # ================================================================================================== | |
| class TestOpenAIModel: | |
| """Tests for OpenAIModel Pydantic model.""" | |
| def test_valid_model(self): | |
| """ | |
| What it does: Verifies creation of valid OpenAIModel. | |
| Purpose: Ensure model accepts valid data. | |
| """ | |
| print("Setup: Creating OpenAIModel with valid data...") | |
| model = OpenAIModel( | |
| id="claude-sonnet-4-5", | |
| description="Claude Sonnet 4.5 model" | |
| ) | |
| print(f"Result: {model}") | |
| print(f"Comparing id: Expected 'claude-sonnet-4-5', Got '{model.id}'") | |
| assert model.id == "claude-sonnet-4-5" | |
| print(f"Comparing object: Expected 'model', Got '{model.object}'") | |
| assert model.object == "model" | |
| print(f"Comparing owned_by: Expected 'anthropic', Got '{model.owned_by}'") | |
| assert model.owned_by == "anthropic" | |
| print(f"Comparing description: Got '{model.description}'") | |
| assert model.description == "Claude Sonnet 4.5 model" | |
| def test_requires_id(self): | |
| """ | |
| What it does: Verifies that id is required. | |
| Purpose: Ensure validation fails without id. | |
| """ | |
| print("Setup: Attempting to create OpenAIModel without id...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| OpenAIModel() | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "id" in str(exc_info.value) | |
| def test_object_defaults_to_model(self): | |
| """ | |
| What it does: Verifies that object defaults to "model". | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating OpenAIModel without explicit object...") | |
| model = OpenAIModel(id="test-model") | |
| print(f"Comparing object: Expected 'model', Got '{model.object}'") | |
| assert model.object == "model" | |
| def test_owned_by_defaults_to_anthropic(self): | |
| """ | |
| What it does: Verifies that owned_by defaults to "anthropic". | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating OpenAIModel without explicit owned_by...") | |
| model = OpenAIModel(id="test-model") | |
| print(f"Comparing owned_by: Expected 'anthropic', Got '{model.owned_by}'") | |
| assert model.owned_by == "anthropic" | |
| def test_created_is_auto_generated(self): | |
| """ | |
| What it does: Verifies that created timestamp is auto-generated. | |
| Purpose: Ensure timestamp is set automatically. | |
| """ | |
| print("Setup: Creating OpenAIModel without explicit created...") | |
| model = OpenAIModel(id="test-model") | |
| print(f"Comparing created: Got {model.created}") | |
| assert model.created > 0 | |
| assert isinstance(model.created, int) | |
| def test_description_is_optional(self): | |
| """ | |
| What it does: Verifies that description is optional. | |
| Purpose: Ensure models without description work. | |
| """ | |
| print("Setup: Creating OpenAIModel without description...") | |
| model = OpenAIModel(id="test-model") | |
| print(f"Comparing description: Expected None, Got {model.description}") | |
| assert model.description is None | |
| # ================================================================================================== | |
| # Tests for ModelList | |
| # ================================================================================================== | |
| class TestModelList: | |
| """Tests for ModelList Pydantic model.""" | |
| def test_valid_model_list(self): | |
| """ | |
| What it does: Verifies creation of valid ModelList. | |
| Purpose: Ensure model list accepts valid data. | |
| """ | |
| print("Setup: Creating ModelList with valid data...") | |
| model_list = ModelList( | |
| data=[ | |
| OpenAIModel(id="claude-sonnet-4-5"), | |
| OpenAIModel(id="claude-opus-4") | |
| ] | |
| ) | |
| print(f"Result: {model_list}") | |
| print(f"Comparing object: Expected 'list', Got '{model_list.object}'") | |
| assert model_list.object == "list" | |
| print(f"Comparing data length: Expected 2, Got {len(model_list.data)}") | |
| assert len(model_list.data) == 2 | |
| def test_object_defaults_to_list(self): | |
| """ | |
| What it does: Verifies that object defaults to "list". | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating ModelList without explicit object...") | |
| model_list = ModelList(data=[]) | |
| print(f"Comparing object: Expected 'list', Got '{model_list.object}'") | |
| assert model_list.object == "list" | |
| def test_requires_data(self): | |
| """ | |
| What it does: Verifies that data is required. | |
| Purpose: Ensure validation fails without data. | |
| """ | |
| print("Setup: Attempting to create ModelList without data...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ModelList() | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "data" in str(exc_info.value) | |
| def test_accepts_empty_list(self): | |
| """ | |
| What it does: Verifies that empty list is accepted. | |
| Purpose: Ensure empty model list works. | |
| """ | |
| print("Setup: Creating ModelList with empty data...") | |
| model_list = ModelList(data=[]) | |
| print(f"Comparing data: Expected [], Got {model_list.data}") | |
| assert model_list.data == [] | |
| # ================================================================================================== | |
| # Tests for ChatMessage | |
| # ================================================================================================== | |
| class TestChatMessage: | |
| """Tests for ChatMessage Pydantic model.""" | |
| def test_valid_user_message(self): | |
| """ | |
| What it does: Verifies creation of valid user message. | |
| Purpose: Ensure model accepts valid user message. | |
| """ | |
| print("Setup: Creating ChatMessage with user role...") | |
| message = ChatMessage(role="user", content="Hello!") | |
| print(f"Result: {message}") | |
| print(f"Comparing role: Expected 'user', Got '{message.role}'") | |
| assert message.role == "user" | |
| print(f"Comparing content: Expected 'Hello!', Got '{message.content}'") | |
| assert message.content == "Hello!" | |
| def test_valid_assistant_message(self): | |
| """ | |
| What it does: Verifies creation of valid assistant message. | |
| Purpose: Ensure model accepts valid assistant message. | |
| """ | |
| print("Setup: Creating ChatMessage with assistant role...") | |
| message = ChatMessage(role="assistant", content="Hi there!") | |
| print(f"Result: {message}") | |
| print(f"Comparing role: Expected 'assistant', Got '{message.role}'") | |
| assert message.role == "assistant" | |
| def test_valid_system_message(self): | |
| """ | |
| What it does: Verifies creation of valid system message. | |
| Purpose: Ensure model accepts valid system message. | |
| """ | |
| print("Setup: Creating ChatMessage with system role...") | |
| message = ChatMessage(role="system", content="You are helpful.") | |
| print(f"Result: {message}") | |
| print(f"Comparing role: Expected 'system', Got '{message.role}'") | |
| assert message.role == "system" | |
| def test_valid_tool_message(self): | |
| """ | |
| What it does: Verifies creation of valid tool message. | |
| Purpose: Ensure model accepts valid tool message. | |
| """ | |
| print("Setup: Creating ChatMessage with tool role...") | |
| message = ChatMessage( | |
| role="tool", | |
| content="Tool result", | |
| tool_call_id="call_123" | |
| ) | |
| print(f"Result: {message}") | |
| print(f"Comparing role: Expected 'tool', Got '{message.role}'") | |
| assert message.role == "tool" | |
| print(f"Comparing tool_call_id: Expected 'call_123', Got '{message.tool_call_id}'") | |
| assert message.tool_call_id == "call_123" | |
| def test_requires_role(self): | |
| """ | |
| What it does: Verifies that role is required. | |
| Purpose: Ensure validation fails without role. | |
| """ | |
| print("Setup: Attempting to create ChatMessage without role...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ChatMessage(content="Hello") | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "role" in str(exc_info.value) | |
| def test_content_is_optional(self): | |
| """ | |
| What it does: Verifies that content is optional. | |
| Purpose: Ensure messages without content work (e.g., tool calls). | |
| """ | |
| print("Setup: Creating ChatMessage without content...") | |
| message = ChatMessage(role="assistant") | |
| print(f"Comparing content: Expected None, Got {message.content}") | |
| assert message.content is None | |
| def test_accepts_list_content(self): | |
| """ | |
| What it does: Verifies that list content is accepted. | |
| Purpose: Ensure multimodal content works. | |
| """ | |
| print("Setup: Creating ChatMessage with list content...") | |
| message = ChatMessage( | |
| role="user", | |
| content=[ | |
| {"type": "text", "text": "What's in this image?"}, | |
| {"type": "image_url", "image_url": {"url": "https://example.com/img.jpg"}} | |
| ] | |
| ) | |
| print(f"Result: {message}") | |
| print(f"Comparing content type: Expected list, Got {type(message.content)}") | |
| assert isinstance(message.content, list) | |
| assert len(message.content) == 2 | |
| def test_accepts_tool_calls(self): | |
| """ | |
| What it does: Verifies that tool_calls is accepted. | |
| Purpose: Ensure assistant messages with tool calls work. | |
| """ | |
| print("Setup: Creating ChatMessage with tool_calls...") | |
| message = ChatMessage( | |
| role="assistant", | |
| content="I'll call a tool", | |
| tool_calls=[{ | |
| "id": "call_123", | |
| "type": "function", | |
| "function": {"name": "get_weather", "arguments": '{"location": "Moscow"}'} | |
| }] | |
| ) | |
| print(f"Result: {message}") | |
| print(f"Comparing tool_calls: Got {message.tool_calls}") | |
| assert message.tool_calls is not None | |
| assert len(message.tool_calls) == 1 | |
| def test_name_is_optional(self): | |
| """ | |
| What it does: Verifies that name is optional. | |
| Purpose: Ensure messages without name work. | |
| """ | |
| print("Setup: Creating ChatMessage without name...") | |
| message = ChatMessage(role="user", content="Hello") | |
| print(f"Comparing name: Expected None, Got {message.name}") | |
| assert message.name is None | |
| def test_accepts_name(self): | |
| """ | |
| What it does: Verifies that name is accepted. | |
| Purpose: Ensure named messages work. | |
| """ | |
| print("Setup: Creating ChatMessage with name...") | |
| message = ChatMessage(role="user", content="Hello", name="John") | |
| print(f"Comparing name: Expected 'John', Got '{message.name}'") | |
| assert message.name == "John" | |
| def test_extra_fields_allowed(self): | |
| """ | |
| What it does: Verifies that extra fields are allowed. | |
| Purpose: Ensure model_config extra="allow" works. | |
| """ | |
| print("Setup: Creating ChatMessage with extra field...") | |
| message = ChatMessage(role="user", content="Hello", custom_field="value") | |
| print(f"Comparing custom_field: Got '{message.custom_field}'") | |
| assert message.custom_field == "value" | |
| # ================================================================================================== | |
| # Tests for ToolFunction | |
| # ================================================================================================== | |
| class TestToolFunction: | |
| """Tests for ToolFunction Pydantic model.""" | |
| def test_valid_tool_function(self): | |
| """ | |
| What it does: Verifies creation of valid ToolFunction. | |
| Purpose: Ensure model accepts valid tool function. | |
| """ | |
| print("Setup: Creating ToolFunction with valid data...") | |
| func = ToolFunction( | |
| name="get_weather", | |
| description="Get weather for a location", | |
| parameters={ | |
| "type": "object", | |
| "properties": {"location": {"type": "string"}} | |
| } | |
| ) | |
| print(f"Result: {func}") | |
| print(f"Comparing name: Expected 'get_weather', Got '{func.name}'") | |
| assert func.name == "get_weather" | |
| print(f"Comparing description: Got '{func.description}'") | |
| assert func.description == "Get weather for a location" | |
| print(f"Comparing parameters: Got {func.parameters}") | |
| assert "properties" in func.parameters | |
| def test_requires_name(self): | |
| """ | |
| What it does: Verifies that name is required. | |
| Purpose: Ensure validation fails without name. | |
| """ | |
| print("Setup: Attempting to create ToolFunction without name...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ToolFunction(description="Test") | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "name" in str(exc_info.value) | |
| def test_description_is_optional(self): | |
| """ | |
| What it does: Verifies that description is optional. | |
| Purpose: Ensure functions without description work. | |
| """ | |
| print("Setup: Creating ToolFunction without description...") | |
| func = ToolFunction(name="test_func") | |
| print(f"Comparing description: Expected None, Got {func.description}") | |
| assert func.description is None | |
| def test_parameters_is_optional(self): | |
| """ | |
| What it does: Verifies that parameters is optional. | |
| Purpose: Ensure functions without parameters work. | |
| """ | |
| print("Setup: Creating ToolFunction without parameters...") | |
| func = ToolFunction(name="no_params_func") | |
| print(f"Comparing parameters: Expected None, Got {func.parameters}") | |
| assert func.parameters is None | |
| # ================================================================================================== | |
| # Tests for Tool | |
| # ================================================================================================== | |
| class TestTool: | |
| """Tests for Tool Pydantic model.""" | |
| def test_valid_tool(self): | |
| """ | |
| What it does: Verifies creation of valid Tool. | |
| Purpose: Ensure model accepts valid tool. | |
| """ | |
| print("Setup: Creating Tool with valid data...") | |
| tool = Tool( | |
| type="function", | |
| function=ToolFunction( | |
| name="get_weather", | |
| description="Get weather", | |
| parameters={} | |
| ) | |
| ) | |
| print(f"Result: {tool}") | |
| print(f"Comparing type: Expected 'function', Got '{tool.type}'") | |
| assert tool.type == "function" | |
| print(f"Comparing function.name: Expected 'get_weather', Got '{tool.function.name}'") | |
| assert tool.function.name == "get_weather" | |
| def test_type_defaults_to_function(self): | |
| """ | |
| What it does: Verifies that type defaults to "function". | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating Tool without explicit type...") | |
| tool = Tool(function=ToolFunction(name="test")) | |
| print(f"Comparing type: Expected 'function', Got '{tool.type}'") | |
| assert tool.type == "function" | |
| def test_function_is_optional_for_flat_format(self): | |
| """ | |
| What it does: Verifies that function is optional (for flat format compatibility). | |
| Purpose: Ensure flat format (Cursor-style) is supported without function field. | |
| """ | |
| print("Setup: Creating Tool with flat format (name, description, input_schema)...") | |
| tool = Tool( | |
| type="function", | |
| name="test_tool", | |
| description="A test tool", | |
| input_schema={"type": "object", "properties": {}} | |
| ) | |
| print(f"Result: {tool}") | |
| print(f"Comparing name: Expected 'test_tool', Got '{tool.name}'") | |
| assert tool.name == "test_tool" | |
| print(f"Comparing function: Expected None, Got {tool.function}") | |
| assert tool.function is None | |
| print(f"Comparing description: Expected 'A test tool', Got '{tool.description}'") | |
| assert tool.description == "A test tool" | |
| def test_standard_format_still_works(self): | |
| """ | |
| What it does: Verifies that standard OpenAI format still works. | |
| Purpose: Ensure backward compatibility with standard format. | |
| """ | |
| print("Setup: Creating Tool with standard OpenAI format (function field)...") | |
| tool = Tool( | |
| type="function", | |
| function=ToolFunction(name="standard_tool", description="Standard") | |
| ) | |
| print(f"Result: {tool}") | |
| print(f"Comparing function.name: Expected 'standard_tool', Got '{tool.function.name}'") | |
| assert tool.function.name == "standard_tool" | |
| print(f"Comparing name: Expected None, Got {tool.name}") | |
| assert tool.name is None | |
| # ================================================================================================== | |
| # Tests for ChatCompletionRequest | |
| # ================================================================================================== | |
| class TestChatCompletionRequest: | |
| """Tests for ChatCompletionRequest Pydantic model.""" | |
| def test_valid_request(self): | |
| """ | |
| What it does: Verifies creation of valid ChatCompletionRequest. | |
| Purpose: Ensure model accepts valid request. | |
| """ | |
| print("Setup: Creating ChatCompletionRequest with valid data...") | |
| request = ChatCompletionRequest( | |
| model="claude-sonnet-4-5", | |
| messages=[ChatMessage(role="user", content="Hello")] | |
| ) | |
| print(f"Result: {request}") | |
| print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{request.model}'") | |
| assert request.model == "claude-sonnet-4-5" | |
| print(f"Comparing messages length: Expected 1, Got {len(request.messages)}") | |
| assert len(request.messages) == 1 | |
| print(f"Comparing stream: Expected False, Got {request.stream}") | |
| assert request.stream is False | |
| def test_requires_model(self): | |
| """ | |
| What it does: Verifies that model is required. | |
| Purpose: Ensure validation fails without model. | |
| """ | |
| print("Setup: Attempting to create ChatCompletionRequest without model...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ChatCompletionRequest(messages=[ChatMessage(role="user", content="Hi")]) | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "model" in str(exc_info.value) | |
| def test_requires_messages(self): | |
| """ | |
| What it does: Verifies that messages is required. | |
| Purpose: Ensure validation fails without messages. | |
| """ | |
| print("Setup: Attempting to create ChatCompletionRequest without messages...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ChatCompletionRequest(model="claude-sonnet-4-5") | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "messages" in str(exc_info.value) | |
| def test_requires_at_least_one_message(self): | |
| """ | |
| What it does: Verifies that at least one message is required. | |
| Purpose: Ensure validation fails with empty messages. | |
| """ | |
| print("Setup: Attempting to create ChatCompletionRequest with empty messages...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ChatCompletionRequest(model="claude-sonnet-4-5", messages=[]) | |
| print(f"ValidationError raised: {exc_info.value}") | |
| def test_stream_defaults_to_false(self): | |
| """ | |
| What it does: Verifies that stream defaults to False. | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating ChatCompletionRequest without explicit stream...") | |
| request = ChatCompletionRequest( | |
| model="test", | |
| messages=[ChatMessage(role="user", content="Hi")] | |
| ) | |
| print(f"Comparing stream: Expected False, Got {request.stream}") | |
| assert request.stream is False | |
| def test_accepts_stream_true(self): | |
| """ | |
| What it does: Verifies that stream=True is accepted. | |
| Purpose: Ensure streaming requests work. | |
| """ | |
| print("Setup: Creating ChatCompletionRequest with stream=True...") | |
| request = ChatCompletionRequest( | |
| model="test", | |
| messages=[ChatMessage(role="user", content="Hi")], | |
| stream=True | |
| ) | |
| print(f"Comparing stream: Expected True, Got {request.stream}") | |
| assert request.stream is True | |
| def test_accepts_tools(self): | |
| """ | |
| What it does: Verifies that tools are accepted. | |
| Purpose: Ensure function calling works. | |
| """ | |
| print("Setup: Creating ChatCompletionRequest with tools...") | |
| request = ChatCompletionRequest( | |
| model="test", | |
| messages=[ChatMessage(role="user", content="Hi")], | |
| tools=[Tool(function=ToolFunction(name="test_tool"))] | |
| ) | |
| print(f"Comparing tools: Got {request.tools}") | |
| assert request.tools is not None | |
| assert len(request.tools) == 1 | |
| def test_accepts_generation_parameters(self): | |
| """ | |
| What it does: Verifies that generation parameters are accepted. | |
| Purpose: Ensure temperature, top_p, max_tokens work. | |
| """ | |
| print("Setup: Creating ChatCompletionRequest with generation params...") | |
| request = ChatCompletionRequest( | |
| model="test", | |
| messages=[ChatMessage(role="user", content="Hi")], | |
| temperature=0.7, | |
| top_p=0.9, | |
| max_tokens=1000 | |
| ) | |
| print(f"Comparing temperature: Expected 0.7, Got {request.temperature}") | |
| assert request.temperature == 0.7 | |
| print(f"Comparing top_p: Expected 0.9, Got {request.top_p}") | |
| assert request.top_p == 0.9 | |
| print(f"Comparing max_tokens: Expected 1000, Got {request.max_tokens}") | |
| assert request.max_tokens == 1000 | |
| # ================================================================================================== | |
| # Tests for ChatCompletionUsage | |
| # ================================================================================================== | |
| class TestChatCompletionUsage: | |
| """Tests for ChatCompletionUsage Pydantic model.""" | |
| def test_valid_usage(self): | |
| """ | |
| What it does: Verifies creation of valid ChatCompletionUsage. | |
| Purpose: Ensure model accepts valid usage data. | |
| """ | |
| print("Setup: Creating ChatCompletionUsage with valid data...") | |
| usage = ChatCompletionUsage( | |
| prompt_tokens=100, | |
| completion_tokens=50, | |
| total_tokens=150 | |
| ) | |
| print(f"Result: {usage}") | |
| print(f"Comparing prompt_tokens: Expected 100, Got {usage.prompt_tokens}") | |
| assert usage.prompt_tokens == 100 | |
| print(f"Comparing completion_tokens: Expected 50, Got {usage.completion_tokens}") | |
| assert usage.completion_tokens == 50 | |
| print(f"Comparing total_tokens: Expected 150, Got {usage.total_tokens}") | |
| assert usage.total_tokens == 150 | |
| def test_defaults_to_zero(self): | |
| """ | |
| What it does: Verifies that all fields default to 0. | |
| Purpose: Ensure default values are set correctly. | |
| """ | |
| print("Setup: Creating ChatCompletionUsage without explicit values...") | |
| usage = ChatCompletionUsage() | |
| print(f"Comparing prompt_tokens: Expected 0, Got {usage.prompt_tokens}") | |
| assert usage.prompt_tokens == 0 | |
| print(f"Comparing completion_tokens: Expected 0, Got {usage.completion_tokens}") | |
| assert usage.completion_tokens == 0 | |
| print(f"Comparing total_tokens: Expected 0, Got {usage.total_tokens}") | |
| assert usage.total_tokens == 0 | |
| def test_credits_used_is_optional(self): | |
| """ | |
| What it does: Verifies that credits_used is optional. | |
| Purpose: Ensure Kiro-specific field is optional. | |
| """ | |
| print("Setup: Creating ChatCompletionUsage without credits_used...") | |
| usage = ChatCompletionUsage() | |
| print(f"Comparing credits_used: Expected None, Got {usage.credits_used}") | |
| assert usage.credits_used is None | |
| # ================================================================================================== | |
| # Tests for ChatCompletionChoice | |
| # ================================================================================================== | |
| class TestChatCompletionChoice: | |
| """Tests for ChatCompletionChoice Pydantic model.""" | |
| def test_valid_choice(self): | |
| """ | |
| What it does: Verifies creation of valid ChatCompletionChoice. | |
| Purpose: Ensure model accepts valid choice data. | |
| """ | |
| print("Setup: Creating ChatCompletionChoice with valid data...") | |
| choice = ChatCompletionChoice( | |
| index=0, | |
| message={"role": "assistant", "content": "Hello!"}, | |
| finish_reason="stop" | |
| ) | |
| print(f"Result: {choice}") | |
| print(f"Comparing index: Expected 0, Got {choice.index}") | |
| assert choice.index == 0 | |
| print(f"Comparing message: Got {choice.message}") | |
| assert choice.message["role"] == "assistant" | |
| print(f"Comparing finish_reason: Expected 'stop', Got '{choice.finish_reason}'") | |
| assert choice.finish_reason == "stop" | |
| def test_index_defaults_to_zero(self): | |
| """ | |
| What it does: Verifies that index defaults to 0. | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating ChatCompletionChoice without explicit index...") | |
| choice = ChatCompletionChoice(message={"role": "assistant", "content": "Hi"}) | |
| print(f"Comparing index: Expected 0, Got {choice.index}") | |
| assert choice.index == 0 | |
| def test_requires_message(self): | |
| """ | |
| What it does: Verifies that message is required. | |
| Purpose: Ensure validation fails without message. | |
| """ | |
| print("Setup: Attempting to create ChatCompletionChoice without message...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ChatCompletionChoice(index=0, finish_reason="stop") | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "message" in str(exc_info.value) | |
| def test_finish_reason_is_optional(self): | |
| """ | |
| What it does: Verifies that finish_reason is optional. | |
| Purpose: Ensure choices without finish_reason work. | |
| """ | |
| print("Setup: Creating ChatCompletionChoice without finish_reason...") | |
| choice = ChatCompletionChoice(message={"role": "assistant", "content": "Hi"}) | |
| print(f"Comparing finish_reason: Expected None, Got {choice.finish_reason}") | |
| assert choice.finish_reason is None | |
| # ================================================================================================== | |
| # Tests for ChatCompletionResponse | |
| # ================================================================================================== | |
| class TestChatCompletionResponse: | |
| """Tests for ChatCompletionResponse Pydantic model.""" | |
| def test_valid_response(self): | |
| """ | |
| What it does: Verifies creation of valid ChatCompletionResponse. | |
| Purpose: Ensure model accepts valid response data. | |
| """ | |
| print("Setup: Creating ChatCompletionResponse with valid data...") | |
| response = ChatCompletionResponse( | |
| id="chatcmpl-123", | |
| model="claude-sonnet-4-5", | |
| choices=[ChatCompletionChoice( | |
| message={"role": "assistant", "content": "Hello!"}, | |
| finish_reason="stop" | |
| )], | |
| usage=ChatCompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15) | |
| ) | |
| print(f"Result: {response}") | |
| print(f"Comparing id: Expected 'chatcmpl-123', Got '{response.id}'") | |
| assert response.id == "chatcmpl-123" | |
| print(f"Comparing object: Expected 'chat.completion', Got '{response.object}'") | |
| assert response.object == "chat.completion" | |
| print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{response.model}'") | |
| assert response.model == "claude-sonnet-4-5" | |
| print(f"Comparing choices length: Expected 1, Got {len(response.choices)}") | |
| assert len(response.choices) == 1 | |
| def test_object_defaults_to_chat_completion(self): | |
| """ | |
| What it does: Verifies that object defaults to "chat.completion". | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating ChatCompletionResponse without explicit object...") | |
| response = ChatCompletionResponse( | |
| id="test", | |
| model="test", | |
| choices=[ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})], | |
| usage=ChatCompletionUsage() | |
| ) | |
| print(f"Comparing object: Expected 'chat.completion', Got '{response.object}'") | |
| assert response.object == "chat.completion" | |
| def test_created_is_auto_generated(self): | |
| """ | |
| What it does: Verifies that created timestamp is auto-generated. | |
| Purpose: Ensure timestamp is set automatically. | |
| """ | |
| print("Setup: Creating ChatCompletionResponse without explicit created...") | |
| response = ChatCompletionResponse( | |
| id="test", | |
| model="test", | |
| choices=[ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})], | |
| usage=ChatCompletionUsage() | |
| ) | |
| print(f"Comparing created: Got {response.created}") | |
| assert response.created > 0 | |
| assert isinstance(response.created, int) | |
| def test_requires_id(self): | |
| """ | |
| What it does: Verifies that id is required. | |
| Purpose: Ensure validation fails without id. | |
| """ | |
| print("Setup: Attempting to create ChatCompletionResponse without id...") | |
| print("Action: Creating model (should raise ValidationError)...") | |
| with pytest.raises(ValidationError) as exc_info: | |
| ChatCompletionResponse( | |
| model="test", | |
| choices=[ChatCompletionChoice(message={"role": "assistant", "content": "Hi"})], | |
| usage=ChatCompletionUsage() | |
| ) | |
| print(f"ValidationError raised: {exc_info.value}") | |
| assert "id" in str(exc_info.value) | |
| # ================================================================================================== | |
| # Tests for Streaming Models | |
| # ================================================================================================== | |
| class TestChatCompletionChunkDelta: | |
| """Tests for ChatCompletionChunkDelta Pydantic model.""" | |
| def test_valid_delta_with_content(self): | |
| """ | |
| What it does: Verifies creation of valid delta with content. | |
| Purpose: Ensure model accepts content delta. | |
| """ | |
| print("Setup: Creating ChatCompletionChunkDelta with content...") | |
| delta = ChatCompletionChunkDelta(content="Hello") | |
| print(f"Result: {delta}") | |
| print(f"Comparing content: Expected 'Hello', Got '{delta.content}'") | |
| assert delta.content == "Hello" | |
| def test_valid_delta_with_role(self): | |
| """ | |
| What it does: Verifies creation of valid delta with role. | |
| Purpose: Ensure model accepts role delta (first chunk). | |
| """ | |
| print("Setup: Creating ChatCompletionChunkDelta with role...") | |
| delta = ChatCompletionChunkDelta(role="assistant") | |
| print(f"Result: {delta}") | |
| print(f"Comparing role: Expected 'assistant', Got '{delta.role}'") | |
| assert delta.role == "assistant" | |
| def test_all_fields_optional(self): | |
| """ | |
| What it does: Verifies that all fields are optional. | |
| Purpose: Ensure empty delta works. | |
| """ | |
| print("Setup: Creating empty ChatCompletionChunkDelta...") | |
| delta = ChatCompletionChunkDelta() | |
| print(f"Comparing role: Expected None, Got {delta.role}") | |
| assert delta.role is None | |
| print(f"Comparing content: Expected None, Got {delta.content}") | |
| assert delta.content is None | |
| print(f"Comparing tool_calls: Expected None, Got {delta.tool_calls}") | |
| assert delta.tool_calls is None | |
| def test_accepts_tool_calls(self): | |
| """ | |
| What it does: Verifies that tool_calls is accepted. | |
| Purpose: Ensure streaming tool calls work. | |
| """ | |
| print("Setup: Creating ChatCompletionChunkDelta with tool_calls...") | |
| delta = ChatCompletionChunkDelta( | |
| tool_calls=[{"index": 0, "id": "call_1", "function": {"name": "test"}}] | |
| ) | |
| print(f"Comparing tool_calls: Got {delta.tool_calls}") | |
| assert delta.tool_calls is not None | |
| assert len(delta.tool_calls) == 1 | |
| class TestChatCompletionChunkChoice: | |
| """Tests for ChatCompletionChunkChoice Pydantic model.""" | |
| def test_valid_chunk_choice(self): | |
| """ | |
| What it does: Verifies creation of valid chunk choice. | |
| Purpose: Ensure model accepts valid chunk choice. | |
| """ | |
| print("Setup: Creating ChatCompletionChunkChoice with valid data...") | |
| choice = ChatCompletionChunkChoice( | |
| index=0, | |
| delta=ChatCompletionChunkDelta(content="Hello") | |
| ) | |
| print(f"Result: {choice}") | |
| print(f"Comparing index: Expected 0, Got {choice.index}") | |
| assert choice.index == 0 | |
| print(f"Comparing delta.content: Expected 'Hello', Got '{choice.delta.content}'") | |
| assert choice.delta.content == "Hello" | |
| def test_index_defaults_to_zero(self): | |
| """ | |
| What it does: Verifies that index defaults to 0. | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating ChatCompletionChunkChoice without explicit index...") | |
| choice = ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta()) | |
| print(f"Comparing index: Expected 0, Got {choice.index}") | |
| assert choice.index == 0 | |
| def test_finish_reason_is_optional(self): | |
| """ | |
| What it does: Verifies that finish_reason is optional. | |
| Purpose: Ensure intermediate chunks work. | |
| """ | |
| print("Setup: Creating ChatCompletionChunkChoice without finish_reason...") | |
| choice = ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta(content="Hi")) | |
| print(f"Comparing finish_reason: Expected None, Got {choice.finish_reason}") | |
| assert choice.finish_reason is None | |
| def test_accepts_finish_reason(self): | |
| """ | |
| What it does: Verifies that finish_reason is accepted. | |
| Purpose: Ensure final chunk works. | |
| """ | |
| print("Setup: Creating ChatCompletionChunkChoice with finish_reason...") | |
| choice = ChatCompletionChunkChoice( | |
| delta=ChatCompletionChunkDelta(), | |
| finish_reason="stop" | |
| ) | |
| print(f"Comparing finish_reason: Expected 'stop', Got '{choice.finish_reason}'") | |
| assert choice.finish_reason == "stop" | |
| class TestChatCompletionChunk: | |
| """Tests for ChatCompletionChunk Pydantic model.""" | |
| def test_valid_chunk(self): | |
| """ | |
| What it does: Verifies creation of valid chunk. | |
| Purpose: Ensure model accepts valid chunk data. | |
| """ | |
| print("Setup: Creating ChatCompletionChunk with valid data...") | |
| chunk = ChatCompletionChunk( | |
| id="chatcmpl-123", | |
| model="claude-sonnet-4-5", | |
| choices=[ChatCompletionChunkChoice( | |
| delta=ChatCompletionChunkDelta(content="Hello") | |
| )] | |
| ) | |
| print(f"Result: {chunk}") | |
| print(f"Comparing id: Expected 'chatcmpl-123', Got '{chunk.id}'") | |
| assert chunk.id == "chatcmpl-123" | |
| print(f"Comparing object: Expected 'chat.completion.chunk', Got '{chunk.object}'") | |
| assert chunk.object == "chat.completion.chunk" | |
| print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{chunk.model}'") | |
| assert chunk.model == "claude-sonnet-4-5" | |
| def test_object_defaults_to_chunk(self): | |
| """ | |
| What it does: Verifies that object defaults to "chat.completion.chunk". | |
| Purpose: Ensure default value is set correctly. | |
| """ | |
| print("Setup: Creating ChatCompletionChunk without explicit object...") | |
| chunk = ChatCompletionChunk( | |
| id="test", | |
| model="test", | |
| choices=[ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta())] | |
| ) | |
| print(f"Comparing object: Expected 'chat.completion.chunk', Got '{chunk.object}'") | |
| assert chunk.object == "chat.completion.chunk" | |
| def test_usage_is_optional(self): | |
| """ | |
| What it does: Verifies that usage is optional. | |
| Purpose: Ensure intermediate chunks work without usage. | |
| """ | |
| print("Setup: Creating ChatCompletionChunk without usage...") | |
| chunk = ChatCompletionChunk( | |
| id="test", | |
| model="test", | |
| choices=[ChatCompletionChunkChoice(delta=ChatCompletionChunkDelta())] | |
| ) | |
| print(f"Comparing usage: Expected None, Got {chunk.usage}") | |
| assert chunk.usage is None | |
| def test_accepts_usage(self): | |
| """ | |
| What it does: Verifies that usage is accepted. | |
| Purpose: Ensure final chunk with usage works. | |
| """ | |
| print("Setup: Creating ChatCompletionChunk with usage...") | |
| chunk = ChatCompletionChunk( | |
| id="test", | |
| model="test", | |
| choices=[ChatCompletionChunkChoice( | |
| delta=ChatCompletionChunkDelta(), | |
| finish_reason="stop" | |
| )], | |
| usage=ChatCompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15) | |
| ) | |
| print(f"Comparing usage: Got {chunk.usage}") | |
| assert chunk.usage is not None | |
| assert chunk.usage.total_tokens == 15 | |