fix(ollama): surface malformed tool-call JSON as OutputParserException

The ollama client parses streamed tool-call arguments internally and
raises a bare ResponseError (e.g. "error parsing tool call: raw='...',
err=...") when a model streams invalid JSON, before langchain_ollama
ever sees the raw arguments. This opaque, non-langchain_core exception
propagated straight out of invoke/stream calls and crashed the entire
agent run instead of being recognized as a tool-call parsing failure,
matching how malformed arguments are already handled elsewhere in this
file via OutputParserException.

Closes #34746

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Nishitha Mandopen-swe[bot] committed 2026-08-06 21:36:14 +00:00
1 parent 0cd7003c11
commit f6ea0ae395
2 files changed
+92 -10

No files matched your search

@@ -83,7 +83,7 @@ from langchain_core.utils.function_calling import (
convert_to_openai_tool,
)
from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
from ollama import AsyncClient, Client, Message
from ollama import AsyncClient, Client, Message, ResponseError
from pydantic import BaseModel, PrivateAttr, field_validator, model_validator
from pydantic.json_schema import JsonSchemaValue
from pydantic.v1 import BaseModel as BaseModelV1
@@ -117,6 +117,30 @@ def _get_usage_metadata_from_generation_info(
return None
def _reraise_if_malformed_tool_call_response(exc: ResponseError) -> None:
"""Re-raise an Ollama tool-call parsing failure as an `OutputParserException`.
The `ollama` client parses streamed tool-call arguments internally and
raises a bare `ResponseError` (e.g. `"error parsing tool call: raw='...',
err=..."`) when the model produces malformed JSON. That error never
reaches `_parse_json_string`, so it otherwise propagates as an opaque,
non-`langchain_core` exception that crashes the entire agent run instead
of being recognized as a tool-call parsing issue.
Args:
exc: The `ResponseError` raised by the `ollama` client.
Raises:
OutputParserException: If `exc` is a tool-call parsing failure.
"""
if "error parsing tool call" in str(exc.error):
msg = (
"Ollama returned a tool call with malformed JSON arguments that "
f"could not be parsed: {exc.error}"
)
raise OutputParserException(msg) from exc
def _parse_json_string(
json_string: str,
*,
@@ -1109,11 +1133,15 @@ class ChatOllama(BaseChatModel):
raise RuntimeError(msg)
chat_params = self._chat_params(messages, stop, **kwargs)
if chat_params["stream"]:
async for part in await self._async_client.chat(**chat_params):
yield part
else:
yield await self._async_client.chat(**chat_params)
try:
if chat_params["stream"]:
async for part in await self._async_client.chat(**chat_params):
yield part
else:
yield await self._async_client.chat(**chat_params)
except ResponseError as e:
_reraise_if_malformed_tool_call_response(e)
raise
def _create_chat_stream(
self,
@@ -1129,10 +1157,14 @@ class ChatOllama(BaseChatModel):
raise RuntimeError(msg)
chat_params = self._chat_params(messages, stop, **kwargs)
if chat_params["stream"]:
yield from self._client.chat(**chat_params)
else:
yield self._client.chat(**chat_params)
try:
if chat_params["stream"]:
yield from self._client.chat(**chat_params)
else:
yield self._client.chat(**chat_params)
except ResponseError as e:
_reraise_if_malformed_tool_call_response(e)
raise
def _chat_stream_with_aggregation(
self,
@@ -10,6 +10,7 @@ import pytest
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import AIMessage, BaseMessage, ChatMessage, HumanMessage
from langchain_tests.unit_tests import ChatModelUnitTests
from ollama import ResponseError
from langchain_ollama.chat_models import (
ChatOllama,
@@ -788,6 +789,55 @@ def test_invoke_raises_when_client_none() -> None:
llm.invoke([HumanMessage("Hello")])
def test_malformed_tool_call_response_error_is_output_parser_exception() -> None:
"""A malformed tool-call `ResponseError` from `ollama` becomes an
`OutputParserException` instead of propagating as an opaque error.
See issue #34746: Ollama's client raises a bare `ResponseError` when the
model streams invalid JSON tool-call arguments, crashing the agent with
a confusing error instead of a recognizable parsing failure.
"""
with patch("langchain_ollama.chat_models.Client") as mock_client_class:
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.chat.side_effect = ResponseError(
"error parsing tool call: raw='{\"foo\": ' err=unexpected end of JSON input"
)
llm = ChatOllama(model=MODEL_NAME)
with pytest.raises(OutputParserException, match="error parsing tool call"):
llm.invoke([HumanMessage("Hello")])
async def test_malformed_tool_call_response_error_output_parser_async() -> None:
"""Async counterpart of the malformed tool-call `ResponseError` handling."""
with patch("langchain_ollama.chat_models.AsyncClient") as mock_client_class:
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.chat = AsyncMock(
side_effect=ResponseError(
"error parsing tool call: raw='{\"foo\": ' err=unexpected end of "
"JSON input"
)
)
llm = ChatOllama(model=MODEL_NAME)
with pytest.raises(OutputParserException, match="error parsing tool call"):
await llm.ainvoke([HumanMessage("Hello")])
def test_unrelated_response_error_is_not_wrapped() -> None:
"""A `ResponseError` unrelated to tool-call parsing propagates unchanged."""
with patch("langchain_ollama.chat_models.Client") as mock_client_class:
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.chat.side_effect = ResponseError("model not found", 404)
llm = ChatOllama(model=MODEL_NAME)
with pytest.raises(ResponseError, match="model not found"):
llm.invoke([HumanMessage("Hello")])
def test_chat_ollama_ignores_strict_arg() -> None:
"""Test that ChatOllama ignores the 'strict' argument."""
response = [