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:
@@ -1,6 +1,11 @@
|
||||
"""This module converts between AIMessage output formats for the Responses API.
|
||||
"""Converts between AIMessage output formats, governed by ``output_version``.
|
||||
|
||||
ChatOpenAI v0.3 stores reasoning and tool outputs in AIMessage.additional_kwargs:
|
||||
``output_version`` is an attribute on ChatOpenAI.
|
||||
|
||||
Supported values are ``None``, ``'v0'``, and ``'responses/v1'``.
|
||||
|
||||
``'v0'`` corresponds to the format as of ``ChatOpenAI`` v0.3. For the Responses API, it
|
||||
stores reasoning and tool outputs in ``AIMessage.additional_kwargs``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -27,8 +32,9 @@ ChatOpenAI v0.3 stores reasoning and tool outputs in AIMessage.additional_kwargs
|
||||
id="msg_123",
|
||||
)
|
||||
|
||||
To retain information about response item sequencing (and to accommodate multiple
|
||||
reasoning items), ChatOpenAI now stores these items in the content sequence:
|
||||
``'responses/v1'`` is only applicable to the Responses API. It retains information
|
||||
about response item sequencing and accommodates multiple reasoning items by
|
||||
representing these items in the content sequence:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -56,24 +62,26 @@ There are other, small improvements as well-- e.g., we store message IDs on text
|
||||
content blocks, rather than on the AIMessage.id, which now stores the response ID.
|
||||
|
||||
For backwards compatibility, this module provides functions to convert between the
|
||||
old and new formats. The functions are used internally by ChatOpenAI.
|
||||
|
||||
formats. The functions are used internally by ChatOpenAI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Union
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import Any, Union, cast
|
||||
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.messages import AIMessage, is_data_content_block
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
_FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
|
||||
|
||||
|
||||
# v0.3 / Responses
|
||||
def _convert_to_v03_ai_message(
|
||||
message: AIMessage, has_reasoning: bool = False
|
||||
) -> AIMessage:
|
||||
"""Mutate an AIMessage to the old-style v0.3 format."""
|
||||
"""Mutate an `AIMessage` to the old-style v0.3 format."""
|
||||
if isinstance(message.content, list):
|
||||
new_content: list[Union[dict, str]] = []
|
||||
for block in message.content:
|
||||
@@ -142,115 +150,299 @@ def _convert_to_v03_ai_message(
|
||||
return message
|
||||
|
||||
|
||||
def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
||||
"""Convert an old-style v0.3 AIMessage into the new content-block format."""
|
||||
# Only update ChatOpenAI v0.3 AIMessages
|
||||
# TODO: structure provenance into AIMessage
|
||||
is_chatopenai_v03 = (
|
||||
isinstance(message.content, list)
|
||||
and all(isinstance(b, dict) for b in message.content)
|
||||
) and (
|
||||
any(
|
||||
item in message.additional_kwargs
|
||||
for item in [
|
||||
"reasoning",
|
||||
"tool_outputs",
|
||||
"refusal",
|
||||
_FUNCTION_CALL_IDS_MAP_KEY,
|
||||
# v1 / Chat Completions
|
||||
def _convert_from_v1_to_chat_completions(message: AIMessage) -> AIMessage:
|
||||
"""Convert a v1 message to the Chat Completions format."""
|
||||
if isinstance(message.content, list):
|
||||
new_content: list = []
|
||||
for block in message.content:
|
||||
if isinstance(block, dict):
|
||||
block_type = block.get("type")
|
||||
if block_type == "text":
|
||||
# Strip annotations
|
||||
new_content.append({"type": "text", "text": block["text"]})
|
||||
elif block_type in ("reasoning", "tool_call"):
|
||||
pass
|
||||
else:
|
||||
new_content.append(block)
|
||||
else:
|
||||
new_content.append(block)
|
||||
return message.model_copy(update={"content": new_content})
|
||||
|
||||
return message
|
||||
|
||||
|
||||
# v1 / Responses
|
||||
def _convert_annotation_from_v1(annotation: types.Annotation) -> dict[str, Any]:
|
||||
"""Convert a v1 `Annotation` to the v0.3 format (for Responses API)."""
|
||||
if annotation["type"] == "citation":
|
||||
new_ann: dict[str, Any] = {}
|
||||
for field in ("end_index", "start_index"):
|
||||
if field in annotation:
|
||||
new_ann[field] = annotation[field]
|
||||
|
||||
if "url" in annotation:
|
||||
# URL citation
|
||||
if "title" in annotation:
|
||||
new_ann["title"] = annotation["title"]
|
||||
new_ann["type"] = "url_citation"
|
||||
new_ann["url"] = annotation["url"]
|
||||
else:
|
||||
# Document citation
|
||||
new_ann["type"] = "file_citation"
|
||||
if "title" in annotation:
|
||||
new_ann["filename"] = annotation["title"]
|
||||
|
||||
if extra_fields := annotation.get("extras"):
|
||||
new_ann.update(dict(extra_fields.items()))
|
||||
|
||||
return new_ann
|
||||
|
||||
if annotation["type"] == "non_standard_annotation":
|
||||
return annotation["value"]
|
||||
|
||||
return dict(annotation)
|
||||
|
||||
|
||||
def _implode_reasoning_blocks(blocks: list[dict[str, Any]]) -> Iterable[dict[str, Any]]:
|
||||
i = 0
|
||||
n = len(blocks)
|
||||
|
||||
while i < n:
|
||||
block = blocks[i]
|
||||
|
||||
# Skip non-reasoning blocks or blocks already in Responses format
|
||||
if block.get("type") != "reasoning" or "summary" in block:
|
||||
yield dict(block)
|
||||
i += 1
|
||||
continue
|
||||
elif "reasoning" not in block and "summary" not in block:
|
||||
# {"type": "reasoning", "id": "rs_..."}
|
||||
oai_format = {**block, "summary": []}
|
||||
if "extras" in oai_format:
|
||||
oai_format.update(oai_format.pop("extras"))
|
||||
oai_format["type"] = oai_format.pop("type", "reasoning")
|
||||
if "encrypted_content" in oai_format:
|
||||
oai_format["encrypted_content"] = oai_format.pop("encrypted_content")
|
||||
yield oai_format
|
||||
i += 1
|
||||
continue
|
||||
else:
|
||||
pass
|
||||
|
||||
summary: list[dict[str, str]] = [
|
||||
{"type": "summary_text", "text": block.get("reasoning", "")}
|
||||
]
|
||||
# 'common' is every field except the exploded 'reasoning'
|
||||
common = {k: v for k, v in block.items() if k != "reasoning"}
|
||||
if "extras" in common:
|
||||
common.update(common.pop("extras"))
|
||||
|
||||
i += 1
|
||||
while i < n:
|
||||
next_ = blocks[i]
|
||||
if next_.get("type") == "reasoning" and "reasoning" in next_:
|
||||
summary.append(
|
||||
{"type": "summary_text", "text": next_.get("reasoning", "")}
|
||||
)
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
|
||||
merged = dict(common)
|
||||
merged["summary"] = summary
|
||||
merged["type"] = merged.pop("type", "reasoning")
|
||||
yield merged
|
||||
|
||||
|
||||
def _consolidate_calls(items: Iterable[dict[str, Any]]) -> Iterator[dict[str, Any]]:
|
||||
"""Generator that walks through *items* and, whenever it meets the pair.
|
||||
|
||||
{"type": "server_tool_call", "name": "web_search", "id": X, ...}
|
||||
{"type": "server_tool_result", "id": X}
|
||||
|
||||
merges them into
|
||||
|
||||
{"id": X,
|
||||
"output": ...,
|
||||
"status": ...,
|
||||
"type": "web_search_call"}
|
||||
|
||||
keeping every other element untouched.
|
||||
"""
|
||||
items = iter(items) # make sure we have a true iterator
|
||||
for current in items:
|
||||
# Only a call can start a pair worth collapsing
|
||||
if current.get("type") != "server_tool_call":
|
||||
yield current
|
||||
continue
|
||||
|
||||
try:
|
||||
nxt = next(items) # look-ahead one element
|
||||
except StopIteration: # no “result” - just yield the call back
|
||||
yield current
|
||||
break
|
||||
|
||||
# If this really is the matching “result” - collapse
|
||||
if nxt.get("type") == "server_tool_result" and nxt.get(
|
||||
"tool_call_id"
|
||||
) == current.get("id"):
|
||||
if current.get("name") == "web_search":
|
||||
collapsed = {"id": current["id"]}
|
||||
if "args" in current:
|
||||
# N.B. as of 2025-09-17 OpenAI raises BadRequestError if sources
|
||||
# are passed back in
|
||||
collapsed["action"] = current["args"]
|
||||
|
||||
if status := nxt.get("status"):
|
||||
if status == "success":
|
||||
collapsed["status"] = "completed"
|
||||
elif status == "error":
|
||||
collapsed["status"] = "failed"
|
||||
elif nxt.get("extras", {}).get("status"):
|
||||
collapsed["status"] = nxt["extras"]["status"]
|
||||
else:
|
||||
pass
|
||||
collapsed["type"] = "web_search_call"
|
||||
|
||||
if current.get("name") == "file_search":
|
||||
collapsed = {"id": current["id"]}
|
||||
if "args" in current and "queries" in current["args"]:
|
||||
collapsed["queries"] = current["args"]["queries"]
|
||||
|
||||
if "output" in nxt:
|
||||
collapsed["results"] = nxt["output"]
|
||||
if status := nxt.get("status"):
|
||||
if status == "success":
|
||||
collapsed["status"] = "completed"
|
||||
elif status == "error":
|
||||
collapsed["status"] = "failed"
|
||||
elif nxt.get("extras", {}).get("status"):
|
||||
collapsed["status"] = nxt["extras"]["status"]
|
||||
else:
|
||||
pass
|
||||
collapsed["type"] = "file_search_call"
|
||||
|
||||
elif current.get("name") == "code_interpreter":
|
||||
collapsed = {"id": current["id"]}
|
||||
if "args" in current and "code" in current["args"]:
|
||||
collapsed["code"] = current["args"]["code"]
|
||||
for key in ("container_id",):
|
||||
if key in current:
|
||||
collapsed[key] = current[key]
|
||||
elif key in current.get("extras", {}):
|
||||
collapsed[key] = current["extras"][key]
|
||||
else:
|
||||
pass
|
||||
|
||||
if "output" in nxt:
|
||||
collapsed["outputs"] = nxt["output"]
|
||||
if status := nxt.get("status"):
|
||||
if status == "success":
|
||||
collapsed["status"] = "completed"
|
||||
elif status == "error":
|
||||
collapsed["status"] = "failed"
|
||||
elif nxt.get("extras", {}).get("status"):
|
||||
collapsed["status"] = nxt["extras"]["status"]
|
||||
collapsed["type"] = "code_interpreter_call"
|
||||
|
||||
elif current.get("name") == "remote_mcp":
|
||||
collapsed = {"id": current["id"]}
|
||||
if "args" in current:
|
||||
collapsed["arguments"] = json.dumps(
|
||||
current["args"], separators=(",", ":")
|
||||
)
|
||||
elif "arguments" in current.get("extras", {}):
|
||||
collapsed["arguments"] = current["extras"]["arguments"]
|
||||
else:
|
||||
pass
|
||||
|
||||
if tool_name := current.get("extras", {}).get("tool_name"):
|
||||
collapsed["name"] = tool_name
|
||||
if server_label := current.get("extras", {}).get("server_label"):
|
||||
collapsed["server_label"] = server_label
|
||||
collapsed["type"] = "mcp_call"
|
||||
|
||||
if error := nxt.get("extras", {}).get("error"):
|
||||
collapsed["error"] = error
|
||||
if "output" in nxt:
|
||||
collapsed["output"] = nxt["output"]
|
||||
for k, v in current.get("extras", {}).items():
|
||||
if k not in ("server_label", "arguments", "tool_name", "error"):
|
||||
collapsed[k] = v
|
||||
|
||||
elif current.get("name") == "mcp_list_tools":
|
||||
collapsed = {"id": current["id"]}
|
||||
if server_label := current.get("extras", {}).get("server_label"):
|
||||
collapsed["server_label"] = server_label
|
||||
if "output" in nxt:
|
||||
collapsed["tools"] = nxt["output"]
|
||||
collapsed["type"] = "mcp_list_tools"
|
||||
if error := nxt.get("extras", {}).get("error"):
|
||||
collapsed["error"] = error
|
||||
for k, v in current.get("extras", {}).items():
|
||||
if k not in ("server_label", "error"):
|
||||
collapsed[k] = v
|
||||
else:
|
||||
pass
|
||||
|
||||
yield collapsed
|
||||
|
||||
else:
|
||||
# Not a matching pair - emit both, in original order
|
||||
yield current
|
||||
yield nxt
|
||||
|
||||
|
||||
def _convert_from_v1_to_responses(
|
||||
content: list[types.ContentBlock], tool_calls: list[types.ToolCall]
|
||||
) -> list[dict[str, Any]]:
|
||||
new_content: list = []
|
||||
for block in content:
|
||||
if block["type"] == "text" and "annotations" in block:
|
||||
# Need a copy because we're changing the annotations list
|
||||
new_block = dict(block)
|
||||
new_block["annotations"] = [
|
||||
_convert_annotation_from_v1(a) for a in block["annotations"]
|
||||
]
|
||||
)
|
||||
or (
|
||||
isinstance(message.id, str)
|
||||
and message.id.startswith("msg_")
|
||||
and (response_id := message.response_metadata.get("id"))
|
||||
and isinstance(response_id, str)
|
||||
and response_id.startswith("resp_")
|
||||
)
|
||||
)
|
||||
if not is_chatopenai_v03:
|
||||
return message
|
||||
|
||||
content_order = [
|
||||
"reasoning",
|
||||
"code_interpreter_call",
|
||||
"mcp_call",
|
||||
"image_generation_call",
|
||||
"text",
|
||||
"refusal",
|
||||
"function_call",
|
||||
"computer_call",
|
||||
"mcp_list_tools",
|
||||
"mcp_approval_request",
|
||||
# N. B. "web_search_call" and "file_search_call" were not passed back in
|
||||
# in v0.3
|
||||
]
|
||||
|
||||
# Build a bucket for every known block type
|
||||
buckets: dict[str, list] = {key: [] for key in content_order}
|
||||
unknown_blocks = []
|
||||
|
||||
# Reasoning
|
||||
if reasoning := message.additional_kwargs.get("reasoning"):
|
||||
buckets["reasoning"].append(reasoning)
|
||||
|
||||
# Refusal
|
||||
if refusal := message.additional_kwargs.get("refusal"):
|
||||
buckets["refusal"].append({"type": "refusal", "refusal": refusal})
|
||||
|
||||
# Text
|
||||
for block in message.content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
block_copy = block.copy()
|
||||
if isinstance(message.id, str) and message.id.startswith("msg_"):
|
||||
block_copy["id"] = message.id
|
||||
buckets["text"].append(block_copy)
|
||||
else:
|
||||
unknown_blocks.append(block)
|
||||
|
||||
# Function calls
|
||||
function_call_ids = message.additional_kwargs.get(_FUNCTION_CALL_IDS_MAP_KEY)
|
||||
for tool_call in message.tool_calls:
|
||||
function_call = {
|
||||
"type": "function_call",
|
||||
"name": tool_call["name"],
|
||||
"arguments": json.dumps(tool_call["args"], ensure_ascii=False),
|
||||
"call_id": tool_call["id"],
|
||||
}
|
||||
if function_call_ids is not None and (
|
||||
_id := function_call_ids.get(tool_call["id"])
|
||||
new_content.append(new_block)
|
||||
elif block["type"] == "tool_call":
|
||||
new_block = {"type": "function_call", "call_id": block["id"]}
|
||||
if "extras" in block and "item_id" in block["extras"]:
|
||||
new_block["id"] = block["extras"]["item_id"]
|
||||
if "name" in block:
|
||||
new_block["name"] = block["name"]
|
||||
if "extras" in block and "arguments" in block["extras"]:
|
||||
new_block["arguments"] = block["extras"]["arguments"]
|
||||
if any(key not in block for key in ("name", "arguments")):
|
||||
matching_tool_calls = [
|
||||
call for call in tool_calls if call["id"] == block["id"]
|
||||
]
|
||||
if matching_tool_calls:
|
||||
tool_call = matching_tool_calls[0]
|
||||
if "name" not in block:
|
||||
new_block["name"] = tool_call["name"]
|
||||
if "arguments" not in block:
|
||||
new_block["arguments"] = json.dumps(tool_call["args"])
|
||||
new_content.append(new_block)
|
||||
elif (
|
||||
is_data_content_block(cast(dict, block))
|
||||
and block["type"] == "image"
|
||||
and "base64" in block
|
||||
and isinstance(block.get("id"), str)
|
||||
and block["id"].startswith("ig_")
|
||||
):
|
||||
function_call["id"] = _id
|
||||
buckets["function_call"].append(function_call)
|
||||
|
||||
# Tool outputs
|
||||
tool_outputs = message.additional_kwargs.get("tool_outputs", [])
|
||||
for block in tool_outputs:
|
||||
if isinstance(block, dict) and (key := block.get("type")) and key in buckets:
|
||||
buckets[key].append(block)
|
||||
new_block = {"type": "image_generation_call", "result": block["base64"]}
|
||||
for extra_key in ("id", "status"):
|
||||
if extra_key in block:
|
||||
new_block[extra_key] = block[extra_key] # type: ignore[typeddict-item]
|
||||
elif extra_key in block.get("extras", {}):
|
||||
new_block[extra_key] = block["extras"][extra_key]
|
||||
new_content.append(new_block)
|
||||
elif block["type"] == "non_standard" and "value" in block:
|
||||
new_content.append(block["value"])
|
||||
else:
|
||||
unknown_blocks.append(block)
|
||||
new_content.append(block)
|
||||
|
||||
# Re-assemble the content list in the canonical order
|
||||
new_content = []
|
||||
for key in content_order:
|
||||
new_content.extend(buckets[key])
|
||||
new_content.extend(unknown_blocks)
|
||||
|
||||
new_additional_kwargs = dict(message.additional_kwargs)
|
||||
new_additional_kwargs.pop("reasoning", None)
|
||||
new_additional_kwargs.pop("refusal", None)
|
||||
new_additional_kwargs.pop("tool_outputs", None)
|
||||
|
||||
if "id" in message.response_metadata:
|
||||
new_id = message.response_metadata["id"]
|
||||
else:
|
||||
new_id = message.id
|
||||
|
||||
return message.model_copy(
|
||||
update={
|
||||
"content": new_content,
|
||||
"additional_kwargs": new_additional_kwargs,
|
||||
"id": new_id,
|
||||
},
|
||||
deep=False,
|
||||
)
|
||||
new_content = list(_implode_reasoning_blocks(new_content))
|
||||
return list(_consolidate_calls(new_content))
|
||||
|
||||
@@ -174,7 +174,7 @@ class AzureChatOpenAI(BaseChatOpenAI):
|
||||
.. code-block:: python
|
||||
|
||||
for chunk in llm.stream(messages):
|
||||
print(chunk.text(), end="")
|
||||
print(chunk.text, end="")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ from typing import (
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
@@ -32,7 +31,6 @@ from urllib.parse import urlparse
|
||||
import certifi
|
||||
import openai
|
||||
import tiktoken
|
||||
from langchain_core._api.deprecation import deprecated
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForLLMRun,
|
||||
CallbackManagerForLLMRun,
|
||||
@@ -61,14 +59,18 @@ from langchain_core.messages import (
|
||||
ToolCall,
|
||||
ToolMessage,
|
||||
ToolMessageChunk,
|
||||
convert_to_openai_data_block,
|
||||
is_data_content_block,
|
||||
)
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.ai import (
|
||||
InputTokenDetails,
|
||||
OutputTokenDetails,
|
||||
UsageMetadata,
|
||||
)
|
||||
from langchain_core.messages.block_translators.openai import (
|
||||
_convert_from_v03_ai_message,
|
||||
convert_to_openai_data_block,
|
||||
)
|
||||
from langchain_core.messages.tool import tool_call_chunk
|
||||
from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
|
||||
from langchain_core.output_parsers.openai_tools import (
|
||||
@@ -107,7 +109,8 @@ from langchain_openai.chat_models._client_utils import (
|
||||
_get_default_httpx_client,
|
||||
)
|
||||
from langchain_openai.chat_models._compat import (
|
||||
_convert_from_v03_ai_message,
|
||||
_convert_from_v1_to_chat_completions,
|
||||
_convert_from_v1_to_responses,
|
||||
_convert_to_v03_ai_message,
|
||||
)
|
||||
|
||||
@@ -155,7 +158,6 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
|
||||
tool_calls = []
|
||||
invalid_tool_calls = []
|
||||
if raw_tool_calls := _dict.get("tool_calls"):
|
||||
additional_kwargs["tool_calls"] = raw_tool_calls
|
||||
for raw_tool_call in raw_tool_calls:
|
||||
try:
|
||||
tool_calls.append(parse_tool_call(raw_tool_call, return_id=True))
|
||||
@@ -199,7 +201,11 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
|
||||
return ChatMessage(content=_dict.get("content", ""), role=role, id=id_) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _format_message_content(content: Any) -> Any:
|
||||
def _format_message_content(
|
||||
content: Any,
|
||||
api: Literal["chat/completions", "responses"] = "chat/completions",
|
||||
role: Optional[str] = None,
|
||||
) -> Any:
|
||||
"""Format message content."""
|
||||
if content and isinstance(content, list):
|
||||
formatted_content = []
|
||||
@@ -211,8 +217,14 @@ def _format_message_content(content: Any) -> Any:
|
||||
and block["type"] in ("tool_use", "thinking", "reasoning_content")
|
||||
):
|
||||
continue
|
||||
if isinstance(block, dict) and is_data_content_block(block):
|
||||
formatted_content.append(convert_to_openai_data_block(block))
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and is_data_content_block(block)
|
||||
# Responses API messages handled separately in _compat (parsed into
|
||||
# image generation calls)
|
||||
and not (api == "responses" and str(role).lower().startswith("ai"))
|
||||
):
|
||||
formatted_content.append(convert_to_openai_data_block(block, api=api))
|
||||
# Anthropic image blocks
|
||||
elif (
|
||||
isinstance(block, dict)
|
||||
@@ -244,16 +256,14 @@ def _format_message_content(content: Any) -> Any:
|
||||
return formatted_content
|
||||
|
||||
|
||||
def _convert_message_to_dict(message: BaseMessage) -> dict:
|
||||
"""Convert a LangChain message to a dictionary.
|
||||
|
||||
Args:
|
||||
message: The LangChain message.
|
||||
|
||||
Returns:
|
||||
The dictionary.
|
||||
"""
|
||||
message_dict: dict[str, Any] = {"content": _format_message_content(message.content)}
|
||||
def _convert_message_to_dict(
|
||||
message: BaseMessage,
|
||||
api: Literal["chat/completions", "responses"] = "chat/completions",
|
||||
) -> dict:
|
||||
"""Convert a LangChain message to dictionary format expected by OpenAI."""
|
||||
message_dict: dict[str, Any] = {
|
||||
"content": _format_message_content(message.content, api=api, role=message.type)
|
||||
}
|
||||
if (name := message.name or message.additional_kwargs.get("name")) is not None:
|
||||
message_dict["name"] = name
|
||||
|
||||
@@ -288,15 +298,25 @@ def _convert_message_to_dict(message: BaseMessage) -> dict:
|
||||
if "function_call" in message_dict or "tool_calls" in message_dict:
|
||||
message_dict["content"] = message_dict["content"] or None
|
||||
|
||||
if "audio" in message.additional_kwargs:
|
||||
# openai doesn't support passing the data back - only the id
|
||||
# https://platform.openai.com/docs/guides/audio/multi-turn-conversations
|
||||
audio: Optional[dict[str, Any]] = None
|
||||
for block in message.content:
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "audio"
|
||||
and (id_ := block.get("id"))
|
||||
and api != "responses"
|
||||
):
|
||||
# openai doesn't support passing the data back - only the id
|
||||
# https://platform.openai.com/docs/guides/audio/multi-turn-conversations
|
||||
audio = {"id": id_}
|
||||
if not audio and "audio" in message.additional_kwargs:
|
||||
raw_audio = message.additional_kwargs["audio"]
|
||||
audio = (
|
||||
{"id": message.additional_kwargs["audio"]["id"]}
|
||||
if "id" in raw_audio
|
||||
else raw_audio
|
||||
)
|
||||
if audio:
|
||||
message_dict["audio"] = audio
|
||||
elif isinstance(message, SystemMessage):
|
||||
message_dict["role"] = message.additional_kwargs.get(
|
||||
@@ -331,7 +351,6 @@ def _convert_delta_to_message_chunk(
|
||||
additional_kwargs["function_call"] = function_call
|
||||
tool_call_chunks = []
|
||||
if raw_tool_calls := _dict.get("tool_calls"):
|
||||
additional_kwargs["tool_calls"] = raw_tool_calls
|
||||
try:
|
||||
tool_call_chunks = [
|
||||
tool_call_chunk(
|
||||
@@ -426,10 +445,6 @@ def _handle_openai_bad_request(e: openai.BadRequestError) -> None:
|
||||
raise
|
||||
|
||||
|
||||
class _FunctionCall(TypedDict):
|
||||
name: str
|
||||
|
||||
|
||||
_BM = TypeVar("_BM", bound=BaseModel)
|
||||
_DictOrPydanticClass = Union[dict[str, Any], type[_BM], type]
|
||||
_DictOrPydantic = Union[dict, _BM]
|
||||
@@ -656,11 +671,7 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
llm = ChatOpenAI(
|
||||
model="o4-mini",
|
||||
use_responses_api=True,
|
||||
output_version="responses/v1",
|
||||
)
|
||||
llm = ChatOpenAI(model="o4-mini", use_responses_api=True)
|
||||
llm.invoke([HumanMessage("How are you?")], previous_response_id="resp_123")
|
||||
|
||||
.. versionadded:: 0.3.26
|
||||
@@ -675,7 +686,9 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
.. versionadded:: 0.3.9
|
||||
"""
|
||||
|
||||
output_version: Literal["v0", "responses/v1"] = "v0"
|
||||
output_version: Optional[str] = Field(
|
||||
default_factory=from_env("LC_OUTPUT_VERSION", default=None)
|
||||
)
|
||||
"""Version of AIMessage output format to use.
|
||||
|
||||
This field is used to roll-out new output formats for chat model AIMessages
|
||||
@@ -685,13 +698,15 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
|
||||
- ``'v0'``: AIMessage format as of langchain-openai 0.3.x.
|
||||
- ``'responses/v1'``: Formats Responses API output
|
||||
items into AIMessage content blocks.
|
||||
|
||||
Currently only impacts the Responses API. ``output_version='responses/v1'`` is
|
||||
recommended.
|
||||
items into AIMessage content blocks (Responses API only)
|
||||
- ``"v1"``: v1 of LangChain cross-provider standard.
|
||||
|
||||
.. versionadded:: 0.3.25
|
||||
|
||||
.. versionchanged:: 1.0.0
|
||||
|
||||
Default updated to ``"responses/v1"``.
|
||||
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
@@ -891,10 +906,15 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
)
|
||||
if len(choices) == 0:
|
||||
# logprobs is implicitly None
|
||||
return ChatGenerationChunk(
|
||||
generation_chunk = ChatGenerationChunk(
|
||||
message=default_chunk_class(content="", usage_metadata=usage_metadata),
|
||||
generation_info=base_generation_info,
|
||||
)
|
||||
if self.output_version == "v1":
|
||||
generation_chunk.message.content = []
|
||||
generation_chunk.message.response_metadata["output_version"] = "v1"
|
||||
|
||||
return generation_chunk
|
||||
|
||||
choice = choices[0]
|
||||
if choice["delta"] is None:
|
||||
@@ -913,6 +933,8 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
generation_info["system_fingerprint"] = system_fingerprint
|
||||
if service_tier := chunk.get("service_tier"):
|
||||
generation_info["service_tier"] = service_tier
|
||||
if isinstance(message_chunk, AIMessageChunk):
|
||||
message_chunk.chunk_position = "last"
|
||||
|
||||
logprobs = choice.get("logprobs")
|
||||
if logprobs:
|
||||
@@ -921,6 +943,7 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
if usage_metadata and isinstance(message_chunk, AIMessageChunk):
|
||||
message_chunk.usage_metadata = usage_metadata
|
||||
|
||||
message_chunk.response_metadata["model_provider"] = "openai"
|
||||
return ChatGenerationChunk(
|
||||
message=message_chunk, generation_info=generation_info or None
|
||||
)
|
||||
@@ -1219,7 +1242,12 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
else:
|
||||
payload = _construct_responses_api_payload(messages, payload)
|
||||
else:
|
||||
payload["messages"] = [_convert_message_to_dict(m) for m in messages]
|
||||
payload["messages"] = [
|
||||
_convert_message_to_dict(_convert_from_v1_to_chat_completions(m))
|
||||
if isinstance(m, AIMessage)
|
||||
else _convert_message_to_dict(m)
|
||||
for m in messages
|
||||
]
|
||||
return payload
|
||||
|
||||
def _create_chat_result(
|
||||
@@ -1268,6 +1296,7 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
generations.append(gen)
|
||||
llm_output = {
|
||||
"token_usage": token_usage,
|
||||
"model_provider": "openai",
|
||||
"model_name": response_dict.get("model", self.model_name),
|
||||
"system_fingerprint": response_dict.get("system_fingerprint", ""),
|
||||
}
|
||||
@@ -1508,7 +1537,7 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
|
||||
def get_num_tokens_from_messages(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
messages: Sequence[BaseMessage],
|
||||
tools: Optional[
|
||||
Sequence[Union[dict[str, Any], type, Callable, BaseTool]]
|
||||
] = None,
|
||||
@@ -1603,66 +1632,6 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
num_tokens += 3
|
||||
return num_tokens
|
||||
|
||||
@deprecated(
|
||||
since="0.2.1",
|
||||
alternative="langchain_openai.chat_models.base.ChatOpenAI.bind_tools",
|
||||
removal="1.0.0",
|
||||
)
|
||||
def bind_functions(
|
||||
self,
|
||||
functions: Sequence[Union[dict[str, Any], type[BaseModel], Callable, BaseTool]],
|
||||
function_call: Optional[
|
||||
Union[_FunctionCall, str, Literal["auto", "none"]] # noqa: PYI051
|
||||
] = None,
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
"""Bind functions (and other objects) to this chat model.
|
||||
|
||||
Assumes model is compatible with OpenAI function-calling API.
|
||||
|
||||
.. note::
|
||||
Using ``bind_tools()`` is recommended instead, as the ``functions`` and
|
||||
``function_call`` request parameters are officially marked as deprecated by
|
||||
OpenAI.
|
||||
|
||||
Args:
|
||||
functions: A list of function definitions to bind to this chat model.
|
||||
Can be a dictionary, pydantic model, or callable. Pydantic
|
||||
models and callables will be automatically converted to
|
||||
their schema dictionary representation.
|
||||
function_call: Which function to require the model to call.
|
||||
Must be the name of the single provided function or
|
||||
``'auto'`` to automatically determine which function to call
|
||||
(if any).
|
||||
**kwargs: Any additional parameters to pass to the
|
||||
:class:`~langchain.runnable.Runnable` constructor.
|
||||
"""
|
||||
formatted_functions = [convert_to_openai_function(fn) for fn in functions]
|
||||
if function_call is not None:
|
||||
function_call = (
|
||||
{"name": function_call}
|
||||
if isinstance(function_call, str)
|
||||
and function_call not in ("auto", "none")
|
||||
else function_call
|
||||
)
|
||||
if isinstance(function_call, dict) and len(formatted_functions) != 1:
|
||||
msg = (
|
||||
"When specifying `function_call`, you must provide exactly one "
|
||||
"function."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
if (
|
||||
isinstance(function_call, dict)
|
||||
and formatted_functions[0]["name"] != function_call["name"]
|
||||
):
|
||||
msg = (
|
||||
f"Function call {function_call} was specified, but the only "
|
||||
f"provided function was {formatted_functions[0]['name']}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
kwargs = {**kwargs, "function_call": function_call}
|
||||
return super().bind(functions=formatted_functions, **kwargs)
|
||||
|
||||
def bind_tools(
|
||||
self,
|
||||
tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]],
|
||||
@@ -1673,7 +1642,7 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
strict: Optional[bool] = None,
|
||||
parallel_tool_calls: Optional[bool] = None,
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
) -> Runnable[LanguageModelInput, AIMessage]:
|
||||
"""Bind tool-like objects to this chat model.
|
||||
|
||||
Assumes model is compatible with OpenAI tool-calling API.
|
||||
@@ -2174,7 +2143,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
.. code-block:: python
|
||||
|
||||
for chunk in llm.stream(messages):
|
||||
print(chunk.text(), end="")
|
||||
print(chunk.text, end="")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -2404,7 +2373,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
output_version="responses/v1",
|
||||
)
|
||||
response = llm.invoke("Hi, I'm Bob.")
|
||||
response.text()
|
||||
response.text
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -2416,7 +2385,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
"What is my name?",
|
||||
previous_response_id=response.response_metadata["id"],
|
||||
)
|
||||
second_response.text()
|
||||
second_response.text
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -2465,7 +2434,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override]
|
||||
response = llm.invoke("What is 3^3?")
|
||||
|
||||
# Response text
|
||||
print(f"Output: {response.text()}")
|
||||
print(f"Output: {response.text}")
|
||||
|
||||
# Reasoning summaries
|
||||
for block in response.content:
|
||||
@@ -3386,6 +3355,20 @@ def _oai_structured_outputs_parser(
|
||||
return parsed
|
||||
if ai_msg.additional_kwargs.get("refusal"):
|
||||
raise OpenAIRefusalError(ai_msg.additional_kwargs["refusal"])
|
||||
if any(
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "non_standard"
|
||||
and "refusal" in block["value"]
|
||||
for block in ai_msg.content
|
||||
):
|
||||
refusal = next(
|
||||
block["value"]["refusal"]
|
||||
for block in ai_msg.content
|
||||
if isinstance(block, dict)
|
||||
and block["type"] == "non_standard"
|
||||
and "refusal" in block["value"]
|
||||
)
|
||||
raise OpenAIRefusalError(refusal)
|
||||
if ai_msg.tool_calls:
|
||||
return None
|
||||
msg = (
|
||||
@@ -3503,7 +3486,7 @@ def _get_last_messages(
|
||||
msg = messages[i]
|
||||
if isinstance(msg, AIMessage):
|
||||
response_id = msg.response_metadata.get("id")
|
||||
if response_id:
|
||||
if response_id and response_id.startswith("resp_"):
|
||||
return messages[i + 1 :], response_id
|
||||
# Continue searching for an AIMessage with a valid response_id
|
||||
|
||||
@@ -3660,23 +3643,42 @@ def _ensure_valid_tool_message_content(tool_output: Any) -> Union[str, list[dict
|
||||
return _stringify(tool_output)
|
||||
|
||||
|
||||
def _make_computer_call_output_from_message(message: ToolMessage) -> dict:
|
||||
computer_call_output: dict = {
|
||||
"call_id": message.tool_call_id,
|
||||
"type": "computer_call_output",
|
||||
}
|
||||
def _make_computer_call_output_from_message(
|
||||
message: ToolMessage,
|
||||
) -> Optional[dict[str, Any]]:
|
||||
computer_call_output: Optional[dict[str, Any]] = None
|
||||
if isinstance(message.content, list):
|
||||
# Use first input_image block
|
||||
output = next(
|
||||
block
|
||||
for block in message.content
|
||||
if cast(dict, block)["type"] == "input_image"
|
||||
)
|
||||
else:
|
||||
for block in message.content:
|
||||
if (
|
||||
message.additional_kwargs.get("type") == "computer_call_output"
|
||||
and isinstance(block, dict)
|
||||
and block.get("type") == "input_image"
|
||||
):
|
||||
# Use first input_image block
|
||||
computer_call_output = {
|
||||
"call_id": message.tool_call_id,
|
||||
"type": "computer_call_output",
|
||||
"output": block,
|
||||
}
|
||||
break
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "non_standard"
|
||||
and block.get("value", {}).get("type") == "computer_call_output"
|
||||
):
|
||||
computer_call_output = block["value"]
|
||||
break
|
||||
elif message.additional_kwargs.get("type") == "computer_call_output":
|
||||
# string, assume image_url
|
||||
output = {"type": "input_image", "image_url": message.content}
|
||||
computer_call_output["output"] = output
|
||||
if "acknowledged_safety_checks" in message.additional_kwargs:
|
||||
computer_call_output = {
|
||||
"call_id": message.tool_call_id,
|
||||
"type": "computer_call_output",
|
||||
"output": {"type": "input_image", "image_url": message.content},
|
||||
}
|
||||
if (
|
||||
computer_call_output is not None
|
||||
and "acknowledged_safety_checks" in message.additional_kwargs
|
||||
):
|
||||
computer_call_output["acknowledged_safety_checks"] = message.additional_kwargs[
|
||||
"acknowledged_safety_checks"
|
||||
]
|
||||
@@ -3693,6 +3695,13 @@ def _make_custom_tool_output_from_message(message: ToolMessage) -> Optional[dict
|
||||
"output": block.get("output") or "",
|
||||
}
|
||||
break
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "non_standard"
|
||||
and block.get("value", {}).get("type") == "custom_tool_call_output"
|
||||
):
|
||||
custom_tool_output = block["value"]
|
||||
break
|
||||
|
||||
return custom_tool_output
|
||||
|
||||
@@ -3718,20 +3727,40 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
|
||||
for lc_msg in messages:
|
||||
if isinstance(lc_msg, AIMessage):
|
||||
lc_msg = _convert_from_v03_ai_message(lc_msg)
|
||||
msg = _convert_message_to_dict(lc_msg)
|
||||
msg = _convert_message_to_dict(lc_msg, api="responses")
|
||||
if isinstance(msg.get("content"), list) and all(
|
||||
isinstance(block, dict) for block in msg["content"]
|
||||
):
|
||||
tcs: list[types.ToolCall] = [
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
"id": tool_call.get("id"),
|
||||
}
|
||||
for tool_call in lc_msg.tool_calls
|
||||
]
|
||||
msg["content"] = _convert_from_v1_to_responses(msg["content"], tcs)
|
||||
else:
|
||||
msg = _convert_message_to_dict(lc_msg, api="responses")
|
||||
# Get content from non-standard content blocks
|
||||
if isinstance(msg["content"], list):
|
||||
for i, block in enumerate(msg["content"]):
|
||||
if isinstance(block, dict) and block.get("type") == "non_standard":
|
||||
msg["content"][i] = block["value"]
|
||||
# "name" parameter unsupported
|
||||
if "name" in msg:
|
||||
msg.pop("name")
|
||||
if msg["role"] == "tool":
|
||||
tool_output = msg["content"]
|
||||
computer_call_output = _make_computer_call_output_from_message(
|
||||
cast(ToolMessage, lc_msg)
|
||||
)
|
||||
custom_tool_output = _make_custom_tool_output_from_message(lc_msg) # type: ignore[arg-type]
|
||||
if custom_tool_output:
|
||||
input_.append(custom_tool_output)
|
||||
elif lc_msg.additional_kwargs.get("type") == "computer_call_output":
|
||||
computer_call_output = _make_computer_call_output_from_message(
|
||||
cast(ToolMessage, lc_msg)
|
||||
)
|
||||
if computer_call_output:
|
||||
input_.append(computer_call_output)
|
||||
elif custom_tool_output:
|
||||
input_.append(custom_tool_output)
|
||||
else:
|
||||
tool_output = _ensure_valid_tool_message_content(tool_output)
|
||||
function_call_output = {
|
||||
@@ -3872,12 +3901,18 @@ def _construct_lc_result_from_responses_api(
|
||||
response: Response,
|
||||
schema: Optional[type[_BM]] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
output_version: Literal["v0", "responses/v1"] = "v0",
|
||||
output_version: Optional[str] = None,
|
||||
) -> ChatResult:
|
||||
"""Construct ChatResponse from OpenAI Response API response."""
|
||||
if response.error:
|
||||
raise ValueError(response.error)
|
||||
|
||||
if output_version is None:
|
||||
# Sentinel value of None lets us know if output_version is set explicitly.
|
||||
# Explicitly setting `output_version="responses/v1"` separately enables the
|
||||
# Responses API.
|
||||
output_version = "responses/v1"
|
||||
|
||||
response_metadata = {
|
||||
k: v
|
||||
for k, v in response.model_dump(exclude_none=True, mode="json").items()
|
||||
@@ -3899,6 +3934,7 @@ def _construct_lc_result_from_responses_api(
|
||||
if metadata:
|
||||
response_metadata.update(metadata)
|
||||
# for compatibility with chat completion calls.
|
||||
response_metadata["model_provider"] = "openai"
|
||||
response_metadata["model_name"] = response_metadata.get("model")
|
||||
if response.usage:
|
||||
usage_metadata = _create_usage_metadata_responses(response.usage.model_dump())
|
||||
@@ -4012,6 +4048,7 @@ def _construct_lc_result_from_responses_api(
|
||||
additional_kwargs["parsed"] = parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
message = AIMessage(
|
||||
content=content_blocks,
|
||||
id=response.id,
|
||||
@@ -4023,8 +4060,7 @@ def _construct_lc_result_from_responses_api(
|
||||
)
|
||||
if output_version == "v0":
|
||||
message = _convert_to_v03_ai_message(message)
|
||||
else:
|
||||
pass
|
||||
|
||||
return ChatResult(generations=[ChatGeneration(message=message)])
|
||||
|
||||
|
||||
@@ -4036,7 +4072,7 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
schema: Optional[type[_BM]] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
has_reasoning: bool = False,
|
||||
output_version: Literal["v0", "responses/v1"] = "v0",
|
||||
output_version: Optional[str] = None,
|
||||
) -> tuple[int, int, int, Optional[ChatGenerationChunk]]:
|
||||
def _advance(output_idx: int, sub_idx: Optional[int] = None) -> None:
|
||||
"""Advance indexes tracked during streaming.
|
||||
@@ -4083,11 +4119,19 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
current_sub_index = sub_idx
|
||||
current_output_index = output_idx
|
||||
|
||||
if output_version is None:
|
||||
# Sentinel value of None lets us know if output_version is set explicitly.
|
||||
# Explicitly setting `output_version="responses/v1"` separately enables the
|
||||
# Responses API.
|
||||
output_version = "responses/v1"
|
||||
|
||||
content = []
|
||||
tool_call_chunks: list = []
|
||||
additional_kwargs: dict = {}
|
||||
response_metadata = metadata or {}
|
||||
response_metadata["model_provider"] = "openai"
|
||||
usage_metadata = None
|
||||
chunk_position: Optional[Literal["last"]] = None
|
||||
id = None
|
||||
if chunk.type == "response.output_text.delta":
|
||||
_advance(chunk.output_index, chunk.content_index)
|
||||
@@ -4099,9 +4143,12 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
annotation = chunk.annotation
|
||||
else:
|
||||
annotation = chunk.annotation.model_dump(exclude_none=True, mode="json")
|
||||
content.append({"annotations": [annotation], "index": current_index})
|
||||
|
||||
content.append(
|
||||
{"type": "text", "annotations": [annotation], "index": current_index}
|
||||
)
|
||||
elif chunk.type == "response.output_text.done":
|
||||
content.append({"id": chunk.item_id, "index": current_index})
|
||||
content.append({"type": "text", "id": chunk.item_id, "index": current_index})
|
||||
elif chunk.type == "response.created":
|
||||
id = chunk.response.id
|
||||
response_metadata["id"] = chunk.response.id # Backwards compatibility
|
||||
@@ -4122,6 +4169,7 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
response_metadata = {
|
||||
k: v for k, v in msg.response_metadata.items() if k != "id"
|
||||
}
|
||||
chunk_position = "last"
|
||||
elif chunk.type == "response.output_item.added" and chunk.item.type == "message":
|
||||
if output_version == "v0":
|
||||
id = chunk.item.id
|
||||
@@ -4194,6 +4242,7 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
content.append({"type": "refusal", "refusal": chunk.refusal})
|
||||
elif chunk.type == "response.output_item.added" and chunk.item.type == "reasoning":
|
||||
_advance(chunk.output_index)
|
||||
current_sub_index = 0
|
||||
reasoning = chunk.item.model_dump(exclude_none=True, mode="json")
|
||||
reasoning["index"] = current_index
|
||||
content.append(reasoning)
|
||||
@@ -4207,6 +4256,7 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
],
|
||||
"index": current_index,
|
||||
"type": "reasoning",
|
||||
"id": chunk.item_id,
|
||||
}
|
||||
)
|
||||
elif chunk.type == "response.image_generation_call.partial_image":
|
||||
@@ -4237,14 +4287,14 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
response_metadata=response_metadata,
|
||||
additional_kwargs=additional_kwargs,
|
||||
id=id,
|
||||
chunk_position=chunk_position,
|
||||
)
|
||||
if output_version == "v0":
|
||||
message = cast(
|
||||
AIMessageChunk,
|
||||
_convert_to_v03_ai_message(message, has_reasoning=has_reasoning),
|
||||
)
|
||||
else:
|
||||
pass
|
||||
|
||||
return (
|
||||
current_index,
|
||||
current_output_index,
|
||||
|
||||
Reference in New Issue
Block a user