Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "fastapi_poe"
version = "0.0.80"
version = "0.0.81"
authors = [
{ name="Yusheng Ding", email="yding@quora.com" },
{ name="Kris Yang", email="kryang@quora.com" },
Expand Down
19 changes: 14 additions & 5 deletions src/fastapi_poe/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,16 @@ class ToolDefinition(BaseModel):


class CustomToolDefinition(BaseModel):
"""Custom tool definition for OpenAI-compatible custom tools."""
"""Custom tool definition for OpenAI-compatible custom tools.

Corresponds to `chat_completion_custom_tool_param.Custom` but
with a looser format specification.

"""

name: str
description: str
format_: dict[str, Any] = Field(alias="format")
description: Optional[str] = None
format_: Optional[dict[str, Any]] = Field(default=None, alias="format")

model_config = ConfigDict(populate_by_name=True)

Expand Down Expand Up @@ -263,8 +268,12 @@ class ToolCallDefinition(BaseModel):
function: FunctionCallDefinition


class CustomToolCallDefinition(BaseModel):
"""Custom tool call in model response."""
class CustomCallDefinition(BaseModel):
"""Custom tool call in model response.

Corresponds to `chat_completion_message_custom_tool_call.Custom`.

"""

name: str
input_: str = Field(alias="input")
Expand Down
50 changes: 34 additions & 16 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pytest
from fastapi_poe.types import (
CostItem,
CustomToolCallDefinition,
CustomCallDefinition,
CustomToolDefinition,
MessageReaction,
PartialResponse,
Expand Down Expand Up @@ -255,13 +255,31 @@ def test_field_name_works_with_populate_by_name(self) -> None:
)
assert tool.format_ == {"type": "string"}

def test_requires_all_fields(self) -> None:
"""Test that all required fields are validated"""
def test_requires_name_field(self) -> None:
"""Test that name field is required"""
with pytest.raises(pydantic.ValidationError):
CustomToolDefinition(name="my_tool") # type: ignore
CustomToolDefinition() # type: ignore

with pytest.raises(pydantic.ValidationError):
CustomToolDefinition(description="desc", format={}) # type: ignore
def test_optional_fields(self) -> None:
"""Test that description and format are optional"""
tool = CustomToolDefinition(name="my_tool")
assert tool.name == "my_tool"
assert tool.description is None
assert tool.format_ is None

def test_with_only_name_and_description(self) -> None:
"""Test with only name and description"""
tool = CustomToolDefinition(name="tool", description="desc")
assert tool.name == "tool"
assert tool.description == "desc"
assert tool.format_ is None

def test_with_only_name_and_format(self) -> None:
"""Test with only name and format"""
tool = CustomToolDefinition(name="tool", format={"type": "string"})
assert tool.name == "tool"
assert tool.description is None
assert tool.format_ == {"type": "string"}

def test_serialization_uses_alias(self) -> None:
"""Test that serialization uses 'format' not 'format_'"""
Expand Down Expand Up @@ -309,46 +327,46 @@ def test_invalid_type_for_format(self) -> None:
CustomToolDefinition(name="tool", description="desc", format="not a dict") # type: ignore


class TestCustomToolCallDefinition:
class TestCustomCallDefinition:

def test_basic_instantiation(self) -> None:
"""Test creating CustomToolCallDefinition with alias 'input'"""
call = CustomToolCallDefinition(name="my_tool", input='{"arg": "value"}')
"""Test creating CustomCallDefinition with alias 'input'"""
call = CustomCallDefinition(name="my_tool", input='{"arg": "value"}')
assert call.name == "my_tool"
assert call.input_ == '{"arg": "value"}'

def test_field_name_works_with_populate_by_name(self) -> None:
"""Test that 'input_' field name also works due to populate_by_name=True"""
call = CustomToolCallDefinition(name="my_tool", input_='{"data": 123}') # type: ignore
call = CustomCallDefinition(name="my_tool", input_='{"data": 123}') # type: ignore
assert call.input_ == '{"data": 123}'

def test_requires_all_fields(self) -> None:
"""Test that all required fields are validated"""
with pytest.raises(pydantic.ValidationError):
CustomToolCallDefinition(name="my_tool") # type: ignore
CustomCallDefinition(name="my_tool") # type: ignore

with pytest.raises(pydantic.ValidationError):
CustomToolCallDefinition(input="data") # type: ignore
CustomCallDefinition(input="data") # type: ignore

def test_serialization_uses_alias(self) -> None:
"""Test that serialization uses 'input' not 'input_'"""
call = CustomToolCallDefinition(name="tool1", input="test_input")
call = CustomCallDefinition(name="tool1", input="test_input")
data = call.model_dump(by_alias=True)
assert "input" in data
assert "input_" not in data
assert data["input"] == "test_input"

def test_serialization_without_alias(self) -> None:
"""Test that serialization without by_alias uses 'input_'"""
call = CustomToolCallDefinition(name="tool1", input="test_input")
call = CustomCallDefinition(name="tool1", input="test_input")
data = call.model_dump(by_alias=False)
assert "input_" in data
assert "input" not in data
assert data["input_"] == "test_input"

def test_json_serialization(self) -> None:
"""Test JSON serialization with alias"""
call = CustomToolCallDefinition(name="calculator", input='{"operation": "add"}')
call = CustomCallDefinition(name="calculator", input='{"operation": "add"}')
json_str = call.model_dump_json(by_alias=True)
assert '"input"' in json_str
assert '"input_"' not in json_str
Expand All @@ -359,6 +377,6 @@ def test_deserialization_from_json(self) -> None:
"name": "calculator",
"input": '{"operation": "add", "a": 1, "b": 2}',
}
call = CustomToolCallDefinition(**json_data)
call = CustomCallDefinition(**json_data)
assert call.name == "calculator"
assert call.input_ == '{"operation": "add", "a": 1, "b": 2}'