mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
release: v1.0.0 (#32567)
Co-authored-by: Mohammad Mohtashim <45242107+keenborder786@users.noreply.github.com> Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev> Co-authored-by: ccurme <chester.curme@gmail.com> Co-authored-by: Christophe Bornet <cbornet@hotmail.com> Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com> Co-authored-by: Sadra Barikbin <sadraqazvin1@yahoo.com> Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
This commit is contained in:
@@ -24,7 +24,6 @@
|
||||
}),
|
||||
'openai_api_type': 'azure',
|
||||
'openai_api_version': '2021-10-01',
|
||||
'output_version': 'v0',
|
||||
'request_timeout': 60.0,
|
||||
'stop': list([
|
||||
]),
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
'lc': 1,
|
||||
'type': 'secret',
|
||||
}),
|
||||
'output_version': 'v0',
|
||||
'request_timeout': 60.0,
|
||||
'stop': list([
|
||||
]),
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
'lc': 1,
|
||||
'type': 'secret',
|
||||
}),
|
||||
'output_version': 'v0',
|
||||
'request_timeout': 60.0,
|
||||
'stop': list([
|
||||
]),
|
||||
|
||||
@@ -22,14 +22,18 @@ from langchain_core.messages import (
|
||||
ToolCall,
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.ai import UsageMetadata
|
||||
from langchain_core.messages.block_translators.openai import (
|
||||
_convert_from_v03_ai_message,
|
||||
)
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from langchain_core.runnables.base import RunnableBinding, RunnableSequence
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseReasoningItem
|
||||
from openai.types.responses.response import IncompleteDetails, Response, ResponseUsage
|
||||
from openai.types.responses.response import IncompleteDetails, Response
|
||||
from openai.types.responses.response_error import ResponseError
|
||||
from openai.types.responses.response_file_search_tool_call import (
|
||||
ResponseFileSearchToolCall,
|
||||
@@ -46,6 +50,7 @@ from openai.types.responses.response_reasoning_item import Summary
|
||||
from openai.types.responses.response_usage import (
|
||||
InputTokensDetails,
|
||||
OutputTokensDetails,
|
||||
ResponseUsage,
|
||||
)
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
from typing_extensions import Self, TypedDict
|
||||
@@ -53,7 +58,8 @@ from typing_extensions import Self, TypedDict
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_openai.chat_models._compat import (
|
||||
_FUNCTION_CALL_IDS_MAP_KEY,
|
||||
_convert_from_v03_ai_message,
|
||||
_convert_from_v1_to_chat_completions,
|
||||
_convert_from_v1_to_responses,
|
||||
_convert_to_v03_ai_message,
|
||||
)
|
||||
from langchain_openai.chat_models.base import (
|
||||
@@ -204,7 +210,6 @@ def test__convert_dict_to_message_tool_call() -> None:
|
||||
result = _convert_dict_to_message(message)
|
||||
expected_output = AIMessage(
|
||||
content="",
|
||||
additional_kwargs={"tool_calls": [raw_tool_call]},
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
name="GenerateUsername",
|
||||
@@ -238,7 +243,6 @@ def test__convert_dict_to_message_tool_call() -> None:
|
||||
result = _convert_dict_to_message(message)
|
||||
expected_output = AIMessage(
|
||||
content="",
|
||||
additional_kwargs={"tool_calls": raw_tool_calls},
|
||||
invalid_tool_calls=[
|
||||
InvalidToolCall(
|
||||
name="GenerateUsername",
|
||||
@@ -731,17 +735,22 @@ def test_format_message_content() -> None:
|
||||
assert _format_message_content(content) == [{"type": "text", "text": "hello"}]
|
||||
|
||||
# Standard multi-modal inputs
|
||||
content = [{"type": "image", "source_type": "url", "url": "https://..."}]
|
||||
contents = [
|
||||
{"type": "image", "source_type": "url", "url": "https://..."}, # v0
|
||||
{"type": "image", "url": "https://..."}, # v1
|
||||
]
|
||||
expected = [{"type": "image_url", "image_url": {"url": "https://..."}}]
|
||||
assert expected == _format_message_content(content)
|
||||
for content in contents:
|
||||
assert expected == _format_message_content([content])
|
||||
|
||||
content = [
|
||||
contents = [
|
||||
{
|
||||
"type": "image",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"mime_type": "image/png",
|
||||
}
|
||||
},
|
||||
{"type": "image", "base64": "<base64 data>", "mime_type": "image/png"},
|
||||
]
|
||||
expected = [
|
||||
{
|
||||
@@ -749,16 +758,23 @@ def test_format_message_content() -> None:
|
||||
"image_url": {"url": "data:image/png;base64,<base64 data>"},
|
||||
}
|
||||
]
|
||||
assert expected == _format_message_content(content)
|
||||
for content in contents:
|
||||
assert expected == _format_message_content([content])
|
||||
|
||||
content = [
|
||||
contents = [
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
"filename": "my_file",
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
"filename": "my_file",
|
||||
},
|
||||
]
|
||||
expected = [
|
||||
{
|
||||
@@ -769,11 +785,32 @@ def test_format_message_content() -> None:
|
||||
},
|
||||
}
|
||||
]
|
||||
assert expected == _format_message_content(content)
|
||||
for content in contents:
|
||||
assert expected == _format_message_content([content])
|
||||
|
||||
content = [{"type": "file", "source_type": "id", "id": "file-abc123"}]
|
||||
# Test warn if PDF is missing a filename
|
||||
pdf_block = {
|
||||
"type": "file",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
}
|
||||
expected = [
|
||||
# N.B. this format is invalid for OpenAI
|
||||
{
|
||||
"type": "file",
|
||||
"file": {"file_data": "data:application/pdf;base64,<base64 data>"},
|
||||
}
|
||||
]
|
||||
with pytest.warns(match="filename"):
|
||||
assert expected == _format_message_content([pdf_block])
|
||||
|
||||
contents = [
|
||||
{"type": "file", "source_type": "id", "id": "file-abc123"},
|
||||
{"type": "file", "file_id": "file-abc123"},
|
||||
]
|
||||
expected = [{"type": "file", "file": {"file_id": "file-abc123"}}]
|
||||
assert expected == _format_message_content(content)
|
||||
for content in contents:
|
||||
assert expected == _format_message_content([content])
|
||||
|
||||
|
||||
class GenerateUsername(BaseModel):
|
||||
@@ -1161,9 +1198,6 @@ def test_minimal_reasoning_effort_payload(
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
if use_responses_api:
|
||||
init_kwargs["output_version"] = "responses/v1"
|
||||
|
||||
llm = ChatOpenAI(**init_kwargs)
|
||||
|
||||
messages = [
|
||||
@@ -1189,14 +1223,14 @@ def test_minimal_reasoning_effort_payload(
|
||||
assert payload["max_completion_tokens"] == 100
|
||||
|
||||
|
||||
def test_output_version_compat() -> None:
|
||||
llm = ChatOpenAI(model="gpt-5", output_version="responses/v1")
|
||||
assert llm._use_responses_api({}) is True
|
||||
|
||||
|
||||
def test_verbosity_parameter_payload() -> None:
|
||||
"""Test verbosity parameter is included in request payload for Responses API."""
|
||||
llm = ChatOpenAI(
|
||||
model="gpt-5",
|
||||
verbosity="high",
|
||||
use_responses_api=True,
|
||||
output_version="responses/v1",
|
||||
)
|
||||
llm = ChatOpenAI(model="gpt-5", verbosity="high", use_responses_api=True)
|
||||
|
||||
messages = [{"role": "user", "content": "hello"}]
|
||||
payload = llm._get_request_payload(messages, stop=None)
|
||||
@@ -1231,7 +1265,7 @@ def test_structured_outputs_parser() -> None:
|
||||
serialized = dumps(llm_output)
|
||||
deserialized = loads(serialized)
|
||||
assert isinstance(deserialized, ChatGeneration)
|
||||
result = output_parser.invoke(deserialized.message)
|
||||
result = output_parser.invoke(cast(AIMessage, deserialized.message))
|
||||
assert result == parsed_response
|
||||
|
||||
|
||||
@@ -1288,7 +1322,7 @@ def test__construct_lc_result_from_responses_api_basic_text_response() -> None:
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
assert isinstance(result, ChatResult)
|
||||
assert len(result.generations) == 1
|
||||
@@ -1306,9 +1340,7 @@ def test__construct_lc_result_from_responses_api_basic_text_response() -> None:
|
||||
assert result.generations[0].message.response_metadata["model_name"] == "gpt-4o"
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
assert result.generations[0].message.content == [
|
||||
{"type": "text", "text": "Hello, world!", "annotations": [], "id": "msg_123"}
|
||||
]
|
||||
@@ -1344,7 +1376,7 @@ def test__construct_lc_result_from_responses_api_multiple_text_blocks() -> None:
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
assert len(result.generations[0].message.content) == 2
|
||||
assert result.generations[0].message.content == [
|
||||
@@ -1391,7 +1423,7 @@ def test__construct_lc_result_from_responses_api_multiple_messages() -> None:
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
assert result.generations[0].message.content == [
|
||||
{"type": "text", "text": "foo", "annotations": []},
|
||||
@@ -1407,9 +1439,7 @@ def test__construct_lc_result_from_responses_api_multiple_messages() -> None:
|
||||
assert result.generations[0].message.id == "msg_234"
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
|
||||
assert result.generations[0].message.content == [
|
||||
{"type": "text", "text": "foo", "annotations": [], "id": "msg_123"},
|
||||
@@ -1449,16 +1479,14 @@ def test__construct_lc_result_from_responses_api_refusal_response() -> None:
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
assert result.generations[0].message.additional_kwargs["refusal"] == (
|
||||
"I cannot assist with that request."
|
||||
)
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
assert result.generations[0].message.content == [
|
||||
{
|
||||
"type": "refusal",
|
||||
@@ -1490,7 +1518,7 @@ def test__construct_lc_result_from_responses_api_function_call_valid_json() -> N
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
msg: AIMessage = cast(AIMessage, result.generations[0].message)
|
||||
assert len(msg.tool_calls) == 1
|
||||
@@ -1507,9 +1535,7 @@ def test__construct_lc_result_from_responses_api_function_call_valid_json() -> N
|
||||
)
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
msg = cast(AIMessage, result.generations[0].message)
|
||||
assert msg.tool_calls
|
||||
assert msg.content == [
|
||||
@@ -1545,7 +1571,7 @@ def test__construct_lc_result_from_responses_api_function_call_invalid_json() ->
|
||||
],
|
||||
)
|
||||
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
msg: AIMessage = cast(AIMessage, result.generations[0].message)
|
||||
assert len(msg.invalid_tool_calls) == 1
|
||||
@@ -1599,7 +1625,7 @@ def test__construct_lc_result_from_responses_api_complex_response() -> None:
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
# Check message content
|
||||
assert result.generations[0].message.content == [
|
||||
@@ -1628,9 +1654,7 @@ def test__construct_lc_result_from_responses_api_complex_response() -> None:
|
||||
assert result.generations[0].message.response_metadata["user"] == "user_123"
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
msg = cast(AIMessage, result.generations[0].message)
|
||||
assert msg.response_metadata["metadata"] == {"key1": "value1", "key2": "value2"}
|
||||
assert msg.content == [
|
||||
@@ -1706,7 +1730,7 @@ def test__construct_lc_result_from_responses_api_web_search_response() -> None:
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
assert "tool_outputs" in result.generations[0].message.additional_kwargs
|
||||
assert len(result.generations[0].message.additional_kwargs["tool_outputs"]) == 1
|
||||
@@ -1724,9 +1748,7 @@ def test__construct_lc_result_from_responses_api_web_search_response() -> None:
|
||||
)
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
assert result.generations[0].message.content == [
|
||||
{
|
||||
"type": "web_search_call",
|
||||
@@ -1767,7 +1789,7 @@ def test__construct_lc_result_from_responses_api_file_search_response() -> None:
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
assert "tool_outputs" in result.generations[0].message.additional_kwargs
|
||||
assert len(result.generations[0].message.additional_kwargs["tool_outputs"]) == 1
|
||||
@@ -1808,9 +1830,7 @@ def test__construct_lc_result_from_responses_api_file_search_response() -> None:
|
||||
)
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
assert result.generations[0].message.content == [
|
||||
{
|
||||
"type": "file_search_call",
|
||||
@@ -1877,7 +1897,7 @@ def test__construct_lc_result_from_responses_api_mixed_search_responses() -> Non
|
||||
)
|
||||
|
||||
# v0
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
result = _construct_lc_result_from_responses_api(response, output_version="v0")
|
||||
|
||||
# Check message content
|
||||
assert result.generations[0].message.content == [
|
||||
@@ -1908,9 +1928,7 @@ def test__construct_lc_result_from_responses_api_mixed_search_responses() -> Non
|
||||
assert file_search["results"][0]["filename"] == "example.py"
|
||||
|
||||
# responses/v1
|
||||
result = _construct_lc_result_from_responses_api(
|
||||
response, output_version="responses/v1"
|
||||
)
|
||||
result = _construct_lc_result_from_responses_api(response)
|
||||
assert result.generations[0].message.content == [
|
||||
{
|
||||
"type": "text",
|
||||
@@ -2276,9 +2294,7 @@ def test__construct_responses_api_input_multiple_message_types() -> None:
|
||||
assert messages_copy == messages
|
||||
|
||||
# Test dict messages
|
||||
llm = ChatOpenAI(
|
||||
model="o4-mini", use_responses_api=True, output_version="responses/v1"
|
||||
)
|
||||
llm = ChatOpenAI(model="o4-mini", use_responses_api=True)
|
||||
message_dicts: list = [
|
||||
{"role": "developer", "content": "This is a developer message."},
|
||||
{
|
||||
@@ -2380,7 +2396,7 @@ def test_mcp_tracing() -> None:
|
||||
assert payload["tools"][0]["headers"]["Authorization"] == "Bearer PLACEHOLDER"
|
||||
|
||||
|
||||
def test_compat() -> None:
|
||||
def test_compat_responses_v03() -> None:
|
||||
# Check compatibility with v0.3 message format
|
||||
message_v03 = AIMessage(
|
||||
content=[
|
||||
@@ -2441,6 +2457,178 @@ def test_compat() -> None:
|
||||
assert message_v03_output is not message_v03
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message_v1", "expected"),
|
||||
[
|
||||
(
|
||||
AIMessage(
|
||||
[
|
||||
{"type": "reasoning", "reasoning": "Reasoning text"},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello, world!",
|
||||
"annotations": [
|
||||
{"type": "citation", "url": "https://example.com"}
|
||||
],
|
||||
},
|
||||
],
|
||||
id="chatcmpl-123",
|
||||
response_metadata={"model_provider": "openai", "model_name": "gpt-4.1"},
|
||||
),
|
||||
AIMessage(
|
||||
[{"type": "text", "text": "Hello, world!"}],
|
||||
id="chatcmpl-123",
|
||||
response_metadata={"model_provider": "openai", "model_name": "gpt-4.1"},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_convert_from_v1_to_chat_completions(
|
||||
message_v1: AIMessage, expected: AIMessage
|
||||
) -> None:
|
||||
result = _convert_from_v1_to_chat_completions(message_v1)
|
||||
assert result == expected
|
||||
assert result.tool_calls == message_v1.tool_calls # tool calls remain cached
|
||||
|
||||
# Check no mutation
|
||||
assert message_v1 != result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message_v1", "expected"),
|
||||
[
|
||||
(
|
||||
AIMessage(
|
||||
content_blocks=[
|
||||
{"type": "reasoning", "id": "abc123"},
|
||||
{"type": "reasoning", "id": "abc234", "reasoning": "foo "},
|
||||
{"type": "reasoning", "id": "abc234", "reasoning": "bar"},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "call_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "call_234",
|
||||
"name": "get_weather_2",
|
||||
"args": {"location": "New York"},
|
||||
"extras": {"item_id": "fc_123"},
|
||||
},
|
||||
{"type": "text", "text": "Hello "},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "world",
|
||||
"annotations": [
|
||||
{"type": "citation", "url": "https://example.com"},
|
||||
{
|
||||
"type": "citation",
|
||||
"title": "my doc",
|
||||
"extras": {"file_id": "file_123", "index": 1},
|
||||
},
|
||||
{
|
||||
"type": "non_standard_annotation",
|
||||
"value": {"bar": "baz"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"type": "image", "base64": "...", "id": "ig_123"},
|
||||
{
|
||||
"type": "server_tool_call",
|
||||
"name": "file_search",
|
||||
"id": "fs_123",
|
||||
"args": {"queries": ["query for file search"]},
|
||||
},
|
||||
{
|
||||
"type": "server_tool_result",
|
||||
"tool_call_id": "fs_123",
|
||||
"output": [{"file_id": "file-123"}],
|
||||
"status": "success",
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {"type": "something_else", "foo": "bar"},
|
||||
},
|
||||
],
|
||||
id="resp123",
|
||||
),
|
||||
[
|
||||
{"type": "reasoning", "id": "abc123", "summary": []},
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "abc234",
|
||||
"summary": [
|
||||
{"type": "summary_text", "text": "foo "},
|
||||
{"type": "summary_text", "text": "bar"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_123",
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "San Francisco"}',
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_234",
|
||||
"name": "get_weather_2",
|
||||
"arguments": '{"location": "New York"}',
|
||||
"id": "fc_123",
|
||||
},
|
||||
{"type": "text", "text": "Hello "},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "world",
|
||||
"annotations": [
|
||||
{"type": "url_citation", "url": "https://example.com"},
|
||||
{
|
||||
"type": "file_citation",
|
||||
"filename": "my doc",
|
||||
"index": 1,
|
||||
"file_id": "file_123",
|
||||
},
|
||||
{"bar": "baz"},
|
||||
],
|
||||
},
|
||||
{"type": "image_generation_call", "id": "ig_123", "result": "..."},
|
||||
{
|
||||
"type": "file_search_call",
|
||||
"id": "fs_123",
|
||||
"queries": ["query for file search"],
|
||||
"results": [{"file_id": "file-123"}],
|
||||
"status": "completed",
|
||||
},
|
||||
{"type": "something_else", "foo": "bar"},
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_convert_from_v1_to_responses(
|
||||
message_v1: AIMessage, expected: list[dict[str, Any]]
|
||||
) -> None:
|
||||
tcs: list[types.ToolCall] = [
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
"id": tool_call.get("id"),
|
||||
}
|
||||
for tool_call in message_v1.tool_calls
|
||||
]
|
||||
result = _convert_from_v1_to_responses(message_v1.content_blocks, tcs)
|
||||
assert result == expected
|
||||
|
||||
# Check no mutation
|
||||
assert message_v1 != result
|
||||
|
||||
|
||||
def test_get_last_messages() -> None:
|
||||
messages: list[BaseMessage] = [HumanMessage("Hello")]
|
||||
last_messages, previous_response_id = _get_last_messages(messages)
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from typing import Any, Optional
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessageChunk, BaseMessageChunk
|
||||
from openai.types.responses import (
|
||||
ResponseCompletedEvent,
|
||||
@@ -22,7 +23,7 @@ from openai.types.responses import (
|
||||
ResponseTextDeltaEvent,
|
||||
ResponseTextDoneEvent,
|
||||
)
|
||||
from openai.types.responses.response import Response, ResponseUsage
|
||||
from openai.types.responses.response import Response
|
||||
from openai.types.responses.response_output_text import ResponseOutputText
|
||||
from openai.types.responses.response_reasoning_item import Summary
|
||||
from openai.types.responses.response_reasoning_summary_part_added_event import (
|
||||
@@ -34,6 +35,7 @@ from openai.types.responses.response_reasoning_summary_part_done_event import (
|
||||
from openai.types.responses.response_usage import (
|
||||
InputTokensDetails,
|
||||
OutputTokensDetails,
|
||||
ResponseUsage,
|
||||
)
|
||||
from openai.types.shared.reasoning import Reasoning
|
||||
from openai.types.shared.response_format_text import ResponseFormatText
|
||||
@@ -339,7 +341,7 @@ responses_stream = [
|
||||
id="rs_234",
|
||||
summary=[],
|
||||
type="reasoning",
|
||||
encrypted_content=None,
|
||||
encrypted_content="encrypted-content",
|
||||
status=None,
|
||||
),
|
||||
output_index=2,
|
||||
@@ -418,7 +420,7 @@ responses_stream = [
|
||||
Summary(text="still more reasoning", type="summary_text"),
|
||||
],
|
||||
type="reasoning",
|
||||
encrypted_content=None,
|
||||
encrypted_content="encrypted-content",
|
||||
status=None,
|
||||
),
|
||||
output_index=2,
|
||||
@@ -564,7 +566,7 @@ responses_stream = [
|
||||
Summary(text="still more reasoning", type="summary_text"),
|
||||
],
|
||||
type="reasoning",
|
||||
encrypted_content=None,
|
||||
encrypted_content="encrypted-content",
|
||||
status=None,
|
||||
),
|
||||
ResponseOutputMessage(
|
||||
@@ -621,8 +623,104 @@ def _strip_none(obj: Any) -> Any:
|
||||
return obj
|
||||
|
||||
|
||||
def test_responses_stream() -> None:
|
||||
llm = ChatOpenAI(model="o4-mini", output_version="responses/v1")
|
||||
@pytest.mark.parametrize(
|
||||
("output_version", "expected_content"),
|
||||
[
|
||||
(
|
||||
"responses/v1",
|
||||
[
|
||||
{
|
||||
"id": "rs_123",
|
||||
"summary": [
|
||||
{
|
||||
"index": 0,
|
||||
"type": "summary_text",
|
||||
"text": "reasoning block one",
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"type": "summary_text",
|
||||
"text": "another reasoning block",
|
||||
},
|
||||
],
|
||||
"type": "reasoning",
|
||||
"index": 0,
|
||||
},
|
||||
{"type": "text", "text": "text block one", "index": 1, "id": "msg_123"},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "another text block",
|
||||
"index": 2,
|
||||
"id": "msg_123",
|
||||
},
|
||||
{
|
||||
"id": "rs_234",
|
||||
"summary": [
|
||||
{"index": 0, "type": "summary_text", "text": "more reasoning"},
|
||||
{
|
||||
"index": 1,
|
||||
"type": "summary_text",
|
||||
"text": "still more reasoning",
|
||||
},
|
||||
],
|
||||
"encrypted_content": "encrypted-content",
|
||||
"type": "reasoning",
|
||||
"index": 3,
|
||||
},
|
||||
{"type": "text", "text": "more", "index": 4, "id": "msg_234"},
|
||||
{"type": "text", "text": "text", "index": 5, "id": "msg_234"},
|
||||
],
|
||||
),
|
||||
(
|
||||
"v1",
|
||||
[
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "reasoning block one",
|
||||
"id": "rs_123",
|
||||
"index": "lc_rs_305f30",
|
||||
},
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "another reasoning block",
|
||||
"id": "rs_123",
|
||||
"index": "lc_rs_305f31",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "text block one",
|
||||
"index": "lc_txt_1",
|
||||
"id": "msg_123",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "another text block",
|
||||
"index": "lc_txt_2",
|
||||
"id": "msg_123",
|
||||
},
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "more reasoning",
|
||||
"id": "rs_234",
|
||||
"extras": {"encrypted_content": "encrypted-content"},
|
||||
"index": "lc_rs_335f30",
|
||||
},
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "still more reasoning",
|
||||
"id": "rs_234",
|
||||
"index": "lc_rs_335f31",
|
||||
},
|
||||
{"type": "text", "text": "more", "index": "lc_txt_4", "id": "msg_234"},
|
||||
{"type": "text", "text": "text", "index": "lc_txt_5", "id": "msg_234"},
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_responses_stream(output_version: str, expected_content: list[dict]) -> None:
|
||||
llm = ChatOpenAI(
|
||||
model="o4-mini", use_responses_api=True, output_version=output_version
|
||||
)
|
||||
mock_client = MagicMock()
|
||||
|
||||
def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:
|
||||
@@ -631,36 +729,14 @@ def test_responses_stream() -> None:
|
||||
mock_client.responses.create = mock_create
|
||||
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
chunks = []
|
||||
with patch.object(llm, "root_client", mock_client):
|
||||
for chunk in llm.stream("test"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
chunks.append(chunk)
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
|
||||
expected_content = [
|
||||
{
|
||||
"id": "rs_123",
|
||||
"summary": [
|
||||
{"index": 0, "type": "summary_text", "text": "reasoning block one"},
|
||||
{"index": 1, "type": "summary_text", "text": "another reasoning block"},
|
||||
],
|
||||
"type": "reasoning",
|
||||
"index": 0,
|
||||
},
|
||||
{"type": "text", "text": "text block one", "index": 1, "id": "msg_123"},
|
||||
{"type": "text", "text": "another text block", "index": 2, "id": "msg_123"},
|
||||
{
|
||||
"id": "rs_234",
|
||||
"summary": [
|
||||
{"index": 0, "type": "summary_text", "text": "more reasoning"},
|
||||
{"index": 1, "type": "summary_text", "text": "still more reasoning"},
|
||||
],
|
||||
"type": "reasoning",
|
||||
"index": 3,
|
||||
},
|
||||
{"type": "text", "text": "more", "index": 4, "id": "msg_234"},
|
||||
{"type": "text", "text": "text", "index": 5, "id": "msg_234"},
|
||||
]
|
||||
assert full.content == expected_content
|
||||
assert full.additional_kwargs == {}
|
||||
assert full.id == "resp_123"
|
||||
|
||||
Reference in New Issue
Block a user