Merge branch 'mdrxy/ollama_v1' of github.com:langchain-ai/langchain into mdrxy/ollama_v1

This commit is contained in:
Mason Daugherty 2025-08-04 16:06:51 -04:00
commit 63ade97a5f
No known key found for this signature in database
4 changed files with 104 additions and 72 deletions

View File

@ -284,31 +284,47 @@ def _convert_from_v1_to_chat_completions(message: AIMessageV1) -> AIMessageV1:
# v1 / Responses # v1 / Responses
def _convert_annotation_to_v1(annotation: dict[str, Any]) -> dict[str, Any]: def _convert_annotation_to_v1(annotation: dict[str, Any]) -> types.Annotation:
annotation_type = annotation.get("type") annotation_type = annotation.get("type")
if annotation_type == "url_citation": if annotation_type == "url_citation":
url_citation = {} known_fields = {
"type",
"url",
"title",
"cited_text",
"start_index",
"end_index",
}
url_citation = cast(types.Citation, {})
for field in ("end_index", "start_index", "title"): for field in ("end_index", "start_index", "title"):
if field in annotation: if field in annotation:
url_citation[field] = annotation[field] url_citation[field] = annotation[field]
url_citation["type"] = "citation" url_citation["type"] = "citation"
url_citation["url"] = annotation["url"] url_citation["url"] = annotation["url"]
for field in annotation:
if field not in known_fields:
if "extras" not in url_citation:
url_citation["extras"] = {}
url_citation["extras"][field] = annotation[field]
return url_citation return url_citation
elif annotation_type == "file_citation": elif annotation_type == "file_citation":
document_citation = {"type": "citation"} known_fields = {"type", "title", "cited_text", "start_index", "end_index"}
document_citation: types.Citation = {"type": "citation"}
if "filename" in annotation: if "filename" in annotation:
document_citation["title"] = annotation["filename"] document_citation["title"] = annotation.pop("filename")
if "file_id" in annotation: for field in annotation:
document_citation["file_id"] = annotation["file_id"] if field not in known_fields:
if "index" in annotation: if "extras" not in document_citation:
document_citation["file_index"] = annotation["index"] document_citation["extras"] = {}
document_citation["extras"][field] = annotation[field]
return document_citation return document_citation
# TODO: standardise container_file_citation? # TODO: standardise container_file_citation?
else: else:
non_standard_annotation = { non_standard_annotation: types.NonStandardAnnotation = {
"type": "non_standard_annotation", "type": "non_standard_annotation",
"value": annotation, "value": annotation,
} }
@ -320,23 +336,30 @@ def _explode_reasoning(block: dict[str, Any]) -> Iterable[types.ReasoningContent
yield cast(types.ReasoningContentBlock, block) yield cast(types.ReasoningContentBlock, block)
return return
known_fields = {"type", "reasoning", "id", "index"}
unknown_fields = [
field for field in block if field != "summary" and field not in known_fields
]
if unknown_fields:
block["extras"] = {}
for field in unknown_fields:
block["extras"][field] = block.pop(field)
if not block["summary"]: if not block["summary"]:
_ = block.pop("summary", None) _ = block.pop("summary", None)
yield cast(types.ReasoningContentBlock, block) yield cast(types.ReasoningContentBlock, block)
return return
# Common part for every exploded line, except 'summary' # Common part for every exploded line, except 'summary'
common = {k: v for k, v in block.items() if k != "summary"} common = {k: v for k, v in block.items() if k in known_fields}
# Optional keys that must appear only in the first exploded item # Optional keys that must appear only in the first exploded item
first_only = { first_only = block.pop("extras", None)
k: common.pop(k) for k in ("encrypted_content", "status") if k in common
}
for idx, part in enumerate(block["summary"]): for idx, part in enumerate(block["summary"]):
new_block = dict(common) new_block = dict(common)
new_block["reasoning"] = part.get("text", "") new_block["reasoning"] = part.get("text", "")
if idx == 0: if idx == 0 and first_only:
new_block.update(first_only) new_block.update(first_only)
yield cast(types.ReasoningContentBlock, new_block) yield cast(types.ReasoningContentBlock, new_block)
@ -370,9 +393,11 @@ def _convert_to_v1_from_responses(
new_block = {"type": "image", "base64": result} new_block = {"type": "image", "base64": result}
if output_format := block.get("output_format"): if output_format := block.get("output_format"):
new_block["mime_type"] = f"image/{output_format}" new_block["mime_type"] = f"image/{output_format}"
if "id" in block:
new_block["id"] = block["id"]
if "index" in block:
new_block["index"] = block["index"]
for extra_key in ( for extra_key in (
"id",
"index",
"status", "status",
"background", "background",
"output_format", "output_format",
@ -401,7 +426,9 @@ def _convert_to_v1_from_responses(
break break
if tool_call_block: if tool_call_block:
if "id" in block: if "id" in block:
tool_call_block["item_id"] = block["id"] if "extras" not in tool_call_block:
tool_call_block["extras"] = {}
tool_call_block["extras"]["item_id"] = block["id"] # type: ignore[typeddict-item]
if "index" in block: if "index" in block:
tool_call_block["index"] = block["index"] tool_call_block["index"] = block["index"]
yield tool_call_block yield tool_call_block
@ -479,17 +506,26 @@ def _convert_to_v1_from_responses(
def _convert_annotation_from_v1(annotation: types.Annotation) -> dict[str, Any]: def _convert_annotation_from_v1(annotation: types.Annotation) -> dict[str, Any]:
if annotation["type"] == "citation": 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: if "url" in annotation:
return {**annotation, "type": "url_citation"} # 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"]
new_ann: dict[str, Any] = {"type": "file_citation"} if extra_fields := annotation.get("extras"):
for field, value in extra_fields.items():
if "title" in annotation: new_ann[field] = value
new_ann["filename"] = annotation["title"]
if "file_id" in annotation:
new_ann["file_id"] = annotation["file_id"] # type: ignore[typeddict-item]
if "file_index" in annotation:
new_ann["index"] = annotation["file_index"] # type: ignore[typeddict-item]
return new_ann return new_ann
@ -515,7 +551,8 @@ def _implode_reasoning_blocks(blocks: list[dict[str, Any]]) -> Iterable[dict[str
elif "reasoning" not in block and "summary" not in block: elif "reasoning" not in block and "summary" not in block:
# {"type": "reasoning", "id": "rs_..."} # {"type": "reasoning", "id": "rs_..."}
oai_format = {**block, "summary": []} oai_format = {**block, "summary": []}
# Update key order if "extras" in oai_format:
oai_format.update(oai_format.pop("extras"))
oai_format["type"] = oai_format.pop("type", "reasoning") oai_format["type"] = oai_format.pop("type", "reasoning")
if "encrypted_content" in oai_format: if "encrypted_content" in oai_format:
oai_format["encrypted_content"] = oai_format.pop("encrypted_content") oai_format["encrypted_content"] = oai_format.pop("encrypted_content")
@ -530,6 +567,8 @@ def _implode_reasoning_blocks(blocks: list[dict[str, Any]]) -> Iterable[dict[str
] ]
# 'common' is every field except the exploded 'reasoning' # 'common' is every field except the exploded 'reasoning'
common = {k: v for k, v in block.items() if k != "reasoning"} common = {k: v for k, v in block.items() if k != "reasoning"}
if "extras" in common:
common.update(common.pop("extras"))
i += 1 i += 1
while i < n: while i < n:
@ -623,12 +662,12 @@ def _convert_from_v1_to_responses(
new_content.append(new_block) new_content.append(new_block)
elif block["type"] == "tool_call": elif block["type"] == "tool_call":
new_block = {"type": "function_call", "call_id": block["id"]} new_block = {"type": "function_call", "call_id": block["id"]}
if "item_id" in block: if "extras" in block and "item_id" in block["extras"]:
new_block["id"] = block["item_id"] # type: ignore[typeddict-item] new_block["id"] = block["extras"]["item_id"]
if "name" in block: if "name" in block:
new_block["name"] = block["name"] new_block["name"] = block["name"]
if "arguments" in block: if "extras" in block and "arguments" in block["extras"]:
new_block["arguments"] = block["arguments"] # type: ignore[typeddict-item] new_block["arguments"] = block["extras"]["arguments"]
if any(key not in block for key in ("name", "arguments")): if any(key not in block for key in ("name", "arguments")):
matching_tool_calls = [ matching_tool_calls = [
call for call in tool_calls if call["id"] == block["id"] call for call in tool_calls if call["id"] == block["id"]

View File

@ -50,6 +50,11 @@ def _check_response(
key in annotation key in annotation
for key in ["end_index", "start_index", "title", "type", "url"] for key in ["end_index", "start_index", "title", "type", "url"]
) )
elif annotation["type"] == "citation":
assert all(key in annotation for key in ["title", "type"])
if "url" in annotation:
assert "start_index" in annotation
assert "end_index" in annotation
if output_version == "v1": if output_version == "v1":
text_content = response.text text_content = response.text

View File

@ -2421,31 +2421,24 @@ def test_convert_from_v1_to_chat_completions(
"name": "get_weather", "name": "get_weather",
"args": {"location": "San Francisco"}, "args": {"location": "San Francisco"},
}, },
cast( {
ToolCall, "type": "tool_call",
{ "id": "call_234",
"type": "tool_call", "name": "get_weather_2",
"id": "call_234", "args": {"location": "New York"},
"name": "get_weather_2", "extras": {"item_id": "fc_123"},
"args": {"location": "New York"}, },
"item_id": "fc_123",
},
),
{"type": "text", "text": "Hello "}, {"type": "text", "text": "Hello "},
{ {
"type": "text", "type": "text",
"text": "world", "text": "world",
"annotations": [ "annotations": [
{"type": "citation", "url": "https://example.com"}, {"type": "citation", "url": "https://example.com"},
cast( {
types.Citation, "type": "citation",
{ "title": "my doc",
"type": "citation", "extras": {"file_id": "file_123", "index": 1},
"title": "my doc", },
"file_index": 1,
"file_id": "file_123",
},
),
{ {
"type": "non_standard_annotation", "type": "non_standard_annotation",
"value": {"bar": "baz"}, "value": {"bar": "baz"},
@ -2583,31 +2576,24 @@ def test_convert_from_v1_to_responses(
"name": "get_weather", "name": "get_weather",
"args": {"location": "San Francisco"}, "args": {"location": "San Francisco"},
}, },
cast( {
ToolCall, "type": "tool_call",
{ "id": "call_234",
"type": "tool_call", "name": "get_weather_2",
"id": "call_234", "args": {"location": "New York"},
"name": "get_weather_2", "extras": {"item_id": "fc_123"},
"args": {"location": "New York"}, },
"item_id": "fc_123",
},
),
{"type": "text", "text": "Hello "}, {"type": "text", "text": "Hello "},
{ {
"type": "text", "type": "text",
"text": "world", "text": "world",
"annotations": [ "annotations": [
{"type": "citation", "url": "https://example.com"}, {"type": "citation", "url": "https://example.com"},
cast( {
types.Citation, "type": "citation",
{ "title": "my doc",
"type": "citation", "extras": {"file_id": "file_123", "index": 1},
"title": "my doc", },
"file_index": 1,
"file_id": "file_123",
},
),
{"type": "non_standard_annotation", "value": {"bar": "baz"}}, {"type": "non_standard_annotation", "value": {"bar": "baz"}},
], ],
}, },

View File

@ -338,7 +338,7 @@ responses_stream = [
id="rs_234", id="rs_234",
summary=[], summary=[],
type="reasoning", type="reasoning",
encrypted_content=None, encrypted_content="encrypted-content",
status=None, status=None,
), ),
output_index=2, output_index=2,
@ -417,7 +417,7 @@ responses_stream = [
Summary(text="still more reasoning", type="summary_text"), Summary(text="still more reasoning", type="summary_text"),
], ],
type="reasoning", type="reasoning",
encrypted_content=None, encrypted_content="encrypted-content",
status=None, status=None,
), ),
output_index=2, output_index=2,
@ -563,7 +563,7 @@ responses_stream = [
Summary(text="still more reasoning", type="summary_text"), Summary(text="still more reasoning", type="summary_text"),
], ],
type="reasoning", type="reasoning",
encrypted_content=None, encrypted_content="encrypted-content",
status=None, status=None,
), ),
ResponseOutputMessage( ResponseOutputMessage(
@ -659,6 +659,7 @@ def test_responses_stream() -> None:
{"index": 0, "type": "summary_text", "text": "more reasoning"}, {"index": 0, "type": "summary_text", "text": "more reasoning"},
{"index": 1, "type": "summary_text", "text": "still more reasoning"}, {"index": 1, "type": "summary_text", "text": "still more reasoning"},
], ],
"encrypted_content": "encrypted-content",
"type": "reasoning", "type": "reasoning",
"index": 3, "index": 3,
}, },
@ -723,6 +724,7 @@ def test_responses_stream_v1() -> None:
"type": "reasoning", "type": "reasoning",
"reasoning": "more reasoning", "reasoning": "more reasoning",
"id": "rs_234", "id": "rs_234",
"extras": {"encrypted_content": "encrypted-content"},
"index": 4, "index": 4,
}, },
{ {