Files
567-labs--instructor/tests/test_openai_responses_tools.py
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

167 lines
4.8 KiB
Python

from unittest.mock import MagicMock
from pydantic import BaseModel
from openai import pydantic_function_tool
from instructor.v2.providers.openai.handlers import (
OpenAIResponsesToolsHandler,
reask_responses_tools,
)
def _tool_parameters_schema(model: type[BaseModel]) -> dict:
return pydantic_function_tool(model)["function"]["parameters"]
class ResponseToolModel(BaseModel):
"""Extract a structured response for the user."""
name: str
class AlternateModel(BaseModel):
title: str
count: int
def test_responses_tools_preserves_function_description() -> None:
expected_description = pydantic_function_tool(ResponseToolModel)["function"][
"description"
]
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(ResponseToolModel, {})
assert kwargs["tools"][0]["description"] == expected_description
def test_responses_tools_sets_text_format() -> None:
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(ResponseToolModel, {})
fmt = kwargs["text"]["format"]
assert fmt["type"] == "json_schema"
assert fmt["name"] == "ResponseToolModel"
assert fmt["strict"] is True
assert fmt["schema"] == _tool_parameters_schema(ResponseToolModel)
assert fmt["schema"].get("additionalProperties") is False
def test_responses_tools_overrides_conflicting_text_format() -> None:
conflicting_text = {
"format": {
"type": "json_schema",
"name": "AlternateModel",
"strict": True,
"schema": AlternateModel.model_json_schema(),
},
"verbosity": "low",
}
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(
ResponseToolModel,
{"text": conflicting_text},
)
fmt = kwargs["text"]["format"]
assert fmt["name"] == "ResponseToolModel"
assert fmt["schema"] == _tool_parameters_schema(ResponseToolModel)
assert kwargs["text"]["verbosity"] == "low"
assert kwargs["text"] is not conflicting_text
def test_responses_tools_preserves_matching_text_format() -> None:
matching_text = {
"format": {
"type": "json_schema",
"name": "ResponseToolModel",
"strict": True,
"schema": _tool_parameters_schema(ResponseToolModel),
}
}
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(
ResponseToolModel,
{"text": matching_text},
)
assert kwargs["text"] is matching_text
def test_responses_tools_none_model_no_text() -> None:
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(None, {})
assert "text" not in kwargs
def _make_mock_response(arguments: str | None) -> MagicMock:
tool_call = MagicMock()
tool_call.type = "function_call"
tool_call.arguments = arguments
tool_call.name = "ResponseToolModel"
tool_call.id = "call_123"
response = MagicMock()
response.output = [tool_call]
return response
def test_reask_responses_tools_empty_args_message() -> None:
response = _make_mock_response("{}")
error = ValueError(
"1 validation error for ResponseToolModel\nname\n Field required"
)
result = reask_responses_tools({"messages": []}, response, error)
msg = result["messages"][0]["content"]
assert "empty arguments" in msg
assert "MUST populate ALL required fields" in msg
assert "fix the errors with" not in msg
def test_reask_responses_tools_nonempty_args_message() -> None:
response = _make_mock_response('{"name": 123}')
error = ValueError(
"1 validation error for ResponseToolModel\nname\n Input should be a valid string"
)
result = reask_responses_tools({"messages": []}, response, error)
msg = result["messages"][0]["content"]
assert "fix the errors with" in msg
assert '{"name": 123}' in msg
def test_reask_responses_tools_none_arguments() -> None:
response = _make_mock_response(None)
result = reask_responses_tools({"messages": []}, response, ValueError("required"))
msg = result["messages"][0]["content"]
assert "MUST populate ALL required fields" in msg
def test_responses_tools_overrides_text_type_format() -> None:
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(
ResponseToolModel,
{"text": {"format": {"type": "text"}}},
)
assert kwargs["text"]["format"]["type"] == "json_schema"
assert kwargs["text"]["format"]["name"] == "ResponseToolModel"
def test_parse_response_warns_on_empty_args(caplog) -> None:
import logging
response = _make_mock_response("{}")
response.choices = []
with caplog.at_level(logging.WARNING, logger="instructor"):
try:
OpenAIResponsesToolsHandler().parse_response(response, ResponseToolModel)
except Exception:
pass
assert any("empty arguments" in record.message for record in caplog.records)