Skip to content

Commit 19cf6e8

Browse files
vertexai: Fixed content coercion while streaming Claude thinking response (#834)
1 parent 5720b13 commit 19cf6e8

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

libs/vertexai/langchain_google_vertexai/_anthropic_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,20 @@ def _tools_in_params(params: dict) -> bool:
425425
return "tools" in params or (
426426
"extra_body" in params and params["extra_body"].get("tools")
427427
)
428+
429+
430+
def _thinking_in_params(params: dict) -> bool:
431+
return params.get("thinking", {}).get("type") == "enabled"
432+
433+
434+
def _documents_in_params(params: dict) -> bool:
435+
for message in params.get("messages", []):
436+
if isinstance(message.get("content"), list):
437+
for block in message["content"]:
438+
if (
439+
isinstance(block, dict)
440+
and block.get("type") == "document"
441+
and block.get("citations", {}).get("enabled")
442+
):
443+
return True
444+
return False

libs/vertexai/langchain_google_vertexai/model_garden.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@
5555
_extract_tool_calls,
5656
)
5757
from langchain_google_vertexai._anthropic_utils import (
58+
_documents_in_params,
5859
_format_messages_anthropic,
5960
_make_message_chunk_from_anthropic_event,
61+
_thinking_in_params,
6062
_tools_in_params,
6163
convert_to_anthropic_tool,
6264
)
@@ -378,7 +380,11 @@ def _stream_with_retry(**params: Any) -> Any:
378380
return self.client.messages.create(**params, stream=True)
379381

380382
stream = _stream_with_retry(**params)
381-
coerce_content_to_string = not _tools_in_params(params)
383+
coerce_content_to_string = (
384+
not _tools_in_params(params)
385+
and not _documents_in_params(params)
386+
and not _thinking_in_params(params)
387+
)
382388
for event in stream:
383389
msg = _make_message_chunk_from_anthropic_event(
384390
event,
@@ -414,7 +420,11 @@ async def _astream_with_retry(**params: Any) -> Any:
414420
return await self.async_client.messages.create(**params, stream=True)
415421

416422
stream = await _astream_with_retry(**params)
417-
coerce_content_to_string = not _tools_in_params(params)
423+
coerce_content_to_string = (
424+
not _tools_in_params(params)
425+
and not _documents_in_params(params)
426+
and not _thinking_in_params(params)
427+
)
418428
async for event in stream:
419429
msg = _make_message_chunk_from_anthropic_event(
420430
event,

libs/vertexai/tests/integration_tests/test_model_garden.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@ def test_anthropic_stream() -> None:
142142
assert isinstance(chunk, AIMessageChunk)
143143

144144

145+
@pytest.mark.extended
146+
def test_anthropic_thinking_stream() -> None:
147+
project = os.environ["PROJECT_ID"]
148+
location = "us-east5"
149+
model = ChatAnthropicVertex(
150+
project=project,
151+
location=location,
152+
model_kwargs={
153+
"thinking": {
154+
"type": "enabled",
155+
"budget_tokens": 1024, # budget tokens >= 1024
156+
},
157+
},
158+
max_tokens=2048, # max_tokens must be greater than budget_tokens
159+
)
160+
question = (
161+
"Hello, could you recommend a good movie for me to watch this evening, please?"
162+
)
163+
message = HumanMessage(content=question)
164+
sync_response = model.stream([message], model="claude-3-7-sonnet@20250219")
165+
for chunk in sync_response:
166+
assert isinstance(chunk, AIMessageChunk)
167+
168+
145169
@pytest.mark.extended
146170
async def test_anthropic_async() -> None:
147171
project = os.environ["PROJECT_ID"]

libs/vertexai/tests/unit_tests/test_anthropic_utils.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
from langchain_core.messages.tool import tool_call as create_tool_call
1717

1818
from langchain_google_vertexai._anthropic_utils import (
19+
_documents_in_params,
1920
_format_message_anthropic,
2021
_format_messages_anthropic,
2122
_make_message_chunk_from_anthropic_event,
23+
_thinking_in_params,
2224
)
2325

2426

@@ -838,3 +840,54 @@ def test_make_thinking_message_chunk_from_anthropic_event() -> None:
838840
)
839841
assert isinstance(thinking_chunk, AIMessageChunk)
840842
assert isinstance(signature_chunk, AIMessageChunk)
843+
844+
845+
def test_thinking_in_params_true() -> None:
846+
"""Test _thinking_in_params when thinking.type is 'enabled'."""
847+
params = {"thinking": {"type": "enabled", "budget_tokens": 1024}}
848+
849+
assert _thinking_in_params(params)
850+
851+
852+
def test_thinking_in_params_false_different_type() -> None:
853+
"""Test _thinking_in_params when thinking.type is 'disabled'."""
854+
params = {"thinking": {"type": "disabled", "budget_tokens": 1024}}
855+
856+
assert not _thinking_in_params(params)
857+
858+
859+
def test_documents_in_params_true() -> None:
860+
"""Test _documents_in_params when document with citations is enabled."""
861+
params = {
862+
"messages": [
863+
{
864+
"role": "user",
865+
"content": [{"type": "document", "citations": {"enabled": True}}],
866+
}
867+
]
868+
}
869+
870+
assert _documents_in_params(params)
871+
872+
873+
def test_documents_in_params_false_citations_disabled() -> None:
874+
"""Test _documents_in_params when citations are not enabled."""
875+
params = {
876+
"messages": [
877+
{
878+
"role": "user",
879+
"content": [{"type": "document", "citations": {"enabled": False}}],
880+
}
881+
]
882+
}
883+
884+
assert not _documents_in_params(params)
885+
886+
887+
def test_documents_in_params_false_no_document() -> None:
888+
"""Test _documents_in_params when there are no documents."""
889+
params = {
890+
"messages": [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
891+
}
892+
893+
assert not _documents_in_params(params)

0 commit comments

Comments
 (0)