libs/partners/ollama/langchain_ollama/chat_models.py PYTHON 1,795 lines View on github.com → Search inside
1"""Ollama chat models.23**Input Flow (LangChain -> Ollama)**45`_convert_messages_to_ollama_messages()`:67- Transforms LangChain messages to `ollama.Message` format8- Extracts text content, images (base64), and tool calls910`_chat_params()`:1112- Combines messages with model parameters (temperature, top_p, etc.)13- Attaches tools if provided14- Configures reasoning/thinking mode via `think` parameter15- Sets output format (raw, JSON, or JSON schema)1617**Output Flow (Ollama -> LangChain)**18191. **Ollama Response**2021Stream dictionary chunks containing:22- `message`: Dict with `role`, `content`, `tool_calls`, `thinking`23- `done`: Boolean indicating completion24- `done_reason`: Reason for completion (`stop`, `length`, `load`)25- Token counts/timing metadata26272. **Response Processing** (`_iterate_over_stream()`)2829- Extracts content from `message.content`30- Parses tool calls into `ToolCall`s31- Separates reasoning content when `reasoning=True` (stored in `additional_kwargs`)32- Builds usage metadata from token counts33343. **LangChain Output** (`ChatGenerationChunk` -> `AIMessage`)3536- **Streaming**: Yields `ChatGenerationChunk` with `AIMessageChunk` content37- **Non-streaming**: Returns `ChatResult` with complete `AIMessage`38- Tool calls attached to `AIMessage.tool_calls`39- Reasoning content in `AIMessage.additional_kwargs['reasoning_content']`40"""4142from __future__ import annotations4344import ast45import json46import logging47import warnings48from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence49from operator import itemgetter50from typing import Any, Literal, cast51from uuid import uuid45253from langchain_core.callbacks import CallbackManagerForLLMRun54from langchain_core.callbacks.manager import AsyncCallbackManagerForLLMRun55from langchain_core.exceptions import OutputParserException56from langchain_core.language_models import LanguageModelInput57from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams58from langchain_core.messages import (59    AIMessage,60    AIMessageChunk,61    BaseMessage,62    ChatMessage,63    HumanMessage,64    SystemMessage,65    ToolCall,66    ToolMessage,67    is_data_content_block,68)69from langchain_core.messages import content as types70from langchain_core.messages.ai import UsageMetadata71from langchain_core.messages.tool import tool_call72from langchain_core.output_parsers import (73    JsonOutputKeyToolsParser,74    JsonOutputParser,75    PydanticOutputParser,76    PydanticToolsParser,77)78from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult79from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough80from langchain_core.tools import BaseTool81from langchain_core.utils.function_calling import (82    convert_to_json_schema,83    convert_to_openai_tool,84)85from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass86from ollama import AsyncClient, Client, Message87from pydantic import BaseModel, PrivateAttr, field_validator, model_validator88from pydantic.json_schema import JsonSchemaValue89from pydantic.v1 import BaseModel as BaseModelV190from typing_extensions import Self, is_typeddict9192from langchain_ollama._compat import _convert_from_v1_to_ollama93from langchain_ollama._utils import (94    merge_auth_headers,95    parse_url_with_auth,96    validate_model,97)98from langchain_ollama._version import __version__99100log = logging.getLogger(__name__)101102103def _get_usage_metadata_from_generation_info(104    generation_info: Mapping[str, Any] | None,105) -> UsageMetadata | None:106    """Get usage metadata from Ollama generation info mapping."""107    if generation_info is None:108        return None109    input_tokens: int | None = generation_info.get("prompt_eval_count")110    output_tokens: int | None = generation_info.get("eval_count")111    if input_tokens is not None and output_tokens is not None:112        return UsageMetadata(113            input_tokens=input_tokens,114            output_tokens=output_tokens,115            total_tokens=input_tokens + output_tokens,116        )117    return None118119120def _parse_json_string(121    json_string: str,122    *,123    raw_tool_call: dict[str, Any],124    skip: bool,125) -> Any:126    """Attempt to parse a JSON string for tool calling.127128    It first tries to use the standard `json.loads`. If that fails, it falls129    back to `ast.literal_eval` to safely parse Python literals, which is more130    robust against models using single quotes or containing apostrophes.131132    Args:133        json_string: JSON string to parse.134        raw_tool_call: Raw tool call to include in error message.135        skip: Whether to ignore parsing errors and return the value anyways.136137    Returns:138        The parsed JSON string or Python literal.139140    Raises:141        OutputParserException: If the string is invalid and `skip=False`.142    """143    try:144        return json.loads(json_string)145    except json.JSONDecodeError:146        try:147            # Use ast.literal_eval to safely parse Python-style dicts148            # (e.g. with single quotes)149            return ast.literal_eval(json_string)150        except (SyntaxError, ValueError) as e:151            # If both fail, and we're not skipping, raise an informative error.152            if skip:153                return json_string154            msg = (155                f"Function {raw_tool_call['function']['name']} arguments:\n\n"156                f"{raw_tool_call['function']['arguments']}"157                "\n\nare not valid JSON or a Python literal. "158                f"Received error: {e}"159            )160            raise OutputParserException(msg) from e161    except TypeError as e:162        if skip:163            return json_string164        msg = (165            f"Function {raw_tool_call['function']['name']} arguments:\n\n"166            f"{raw_tool_call['function']['arguments']}\n\nare not a string or a "167            f"dictionary. Received TypeError {e}"168        )169        raise OutputParserException(msg) from e170171172def _parse_arguments_from_tool_call(173    raw_tool_call: dict[str, Any],174) -> dict[str, Any] | None:175    """Parse arguments by trying to parse any shallowly nested string-encoded JSON.176177    Band-aid fix for issue in Ollama with inconsistent tool call argument structure.178    Should be removed/changed if fixed upstream.179180    See https://github.com/ollama/ollama/issues/6155181    """182    if "function" not in raw_tool_call:183        return None184    function_name = raw_tool_call["function"]["name"]185    arguments = raw_tool_call["function"]["arguments"]186    parsed_arguments: dict = {}187    if isinstance(arguments, dict):188        for key, value in arguments.items():189            # Filter out metadata fields like 'functionName' that echo function name190            if key == "functionName" and value == function_name:191                continue192            if isinstance(value, str):193                parsed_value = _parse_json_string(194                    value, skip=True, raw_tool_call=raw_tool_call195                )196                if isinstance(parsed_value, (dict, list)):197                    parsed_arguments[key] = parsed_value198                else:199                    parsed_arguments[key] = value200            else:201                parsed_arguments[key] = value202    else:203        parsed_arguments = _parse_json_string(204            arguments, skip=False, raw_tool_call=raw_tool_call205        )206    return parsed_arguments207208209def _get_tool_calls_from_response(210    response: Mapping[str, Any],211) -> list[ToolCall]:212    """Get tool calls from Ollama response."""213    tool_calls = []214    if "message" in response and (215        raw_tool_calls := response["message"].get("tool_calls")216    ):217        tool_calls.extend(218            [219                tool_call(220                    id=str(uuid4()),221                    name=tc["function"]["name"],222                    args=_parse_arguments_from_tool_call(tc) or {},223                )224                for tc in raw_tool_calls225            ]226        )227    return tool_calls228229230def _lc_tool_call_to_openai_tool_call(tool_call_: ToolCall) -> dict:231    """Convert a LangChain tool call to an OpenAI tool call format."""232    return {233        "type": "function",234        "id": tool_call_["id"],235        "function": {236            "name": tool_call_["name"],237            "arguments": tool_call_["args"],238        },239    }240241242def _get_image_from_data_content_block(block: dict) -> str:243    """Format standard data content block to format expected by Ollama."""244    if block["type"] == "image":245        if block.get("source_type") == "base64":246            # v0 style247            return block["data"]248        if block.get("base64"):249            # v1 content blocks250            return block["base64"]251        error_message = "Image data only supported through in-line base64 format."252        raise ValueError(error_message)253254    error_message = f"Blocks of type {block['type']} not supported."255    raise ValueError(error_message)256257258def _is_pydantic_class(obj: Any) -> bool:259    return isinstance(obj, type) and is_basemodel_subclass(obj)260261262class ChatOllama(BaseChatModel):263    r"""Ollama chat model integration.264265    ???+ note "Setup"266267        Install `langchain-ollama` and download any models you want to use from ollama.268269        ```bash270        ollama pull gpt-oss:20b271        pip install -U langchain-ollama272        ```273274    Key init args  completion params:275        model: str276            Name of Ollama model to use.277        reasoning: bool | None278            Controls the reasoning/thinking mode for279            [supported models](https://ollama.com/search?c=thinking).280281            - `True`: Enables reasoning mode. The model's reasoning process will be282                captured and returned separately in the `additional_kwargs` of the283                response message, under `reasoning_content`. The main response284                content will not include the reasoning tags.285            - `False`: Disables reasoning mode. The model will not perform any reasoning,286                and the response will not include any reasoning content.287            - `None` (Default): The model will use its default reasoning behavior. Note288                however, if the model's default behavior *is* to perform reasoning, think tags289                (`<think>` and `</think>`) will be present within the main response content290                unless you set `reasoning` to `True`.291        temperature: float292            Sampling temperature. Ranges from `0.0` to `1.0`.293        num_predict: int | None294            Max number of tokens to generate.295296    See full list of supported init args and their descriptions in the params section.297298    Instantiate:299        ```python300        from langchain_ollama import ChatOllama301302        model = ChatOllama(303            model="gpt-oss:20b",304            validate_model_on_init=True,305            temperature=0.8,306            num_predict=256,307            # other params ...308        )309        ```310311    Invoke:312        ```python313        messages = [314            ("system", "You are a helpful translator. Translate the user sentence to French."),315            ("human", "I love programming."),316        ]317        model.invoke(messages)318        ```319320        ```python321        AIMessage(content='J'adore le programmation. (Note: "programming" can also refer to the act of writing code, so if you meant that, I could translate it as "J'adore programmer". But since you didn\'t specify, I assumed you were talking about the activity itself, which is what "le programmation" usually refers to.)', response_metadata={'model': 'llama3', 'created_at': '2024-07-04T03:37:50.182604Z', 'message': {'role': 'assistant', 'content': ''}, 'done_reason': 'stop', 'done': True, 'total_duration': 3576619666, 'load_duration': 788524916, 'prompt_eval_count': 32, 'prompt_eval_duration': 128125000, 'eval_count': 71, 'eval_duration': 2656556000}, id='run-ba48f958-6402-41a5-b461-5e250a4ebd36-0')322        ```323324    Stream:325        ```python326        for chunk in model.stream("Return the words Hello World!"):327            print(chunk.text, end="")328        ```329330        ```python331        content='Hello' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'332        content=' World' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'333        content='!' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'334        content='' response_metadata={'model': 'llama3', 'created_at': '2024-07-04T03:39:42.274449Z', 'message': {'role': 'assistant', 'content': ''}, 'done_reason': 'stop', 'done': True, 'total_duration': 411875125, 'load_duration': 1898166, 'prompt_eval_count': 14, 'prompt_eval_duration': 297320000, 'eval_count': 4, 'eval_duration': 111099000} id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'335336        ```337338        ```python339        stream = model.stream(messages)340        full = next(stream)341        for chunk in stream:342            full += chunk343        full344        ```345346        ```python347        AIMessageChunk(348            content='Je adore le programmation.(Note: "programmation" is the formal way to say "programming" in French, but informally, people might use the phrase "le développement logiciel" or simply "le code")',349            response_metadata={350                "model": "llama3",351                "created_at": "2024-07-04T03:38:54.933154Z",352                "message": {"role": "assistant", "content": ""},353                "done_reason": "stop",354                "done": True,355                "total_duration": 1977300042,356                "load_duration": 1345709,357                "prompt_eval_duration": 159343000,358                "eval_count": 47,359                "eval_duration": 1815123000,360            },361            id="run-3c81a3ed-3e79-4dd3-a796-04064d804890",362        )363        ```364365    Async:366        ```python367        await model.ainvoke("Hello how are you!")368        ```369370        ```python371        AIMessage(372            content="Hi there! I'm just an AI, so I don't have feelings or emotions like humans do. But I'm functioning properly and ready to help with any questions or tasks you may have! How can I assist you today?",373            response_metadata={374                "model": "llama3",375                "created_at": "2024-07-04T03:52:08.165478Z",376                "message": {"role": "assistant", "content": ""},377                "done_reason": "stop",378                "done": True,379                "total_duration": 2138492875,380                "load_duration": 1364000,381                "prompt_eval_count": 10,382                "prompt_eval_duration": 297081000,383                "eval_count": 47,384                "eval_duration": 1838524000,385            },386            id="run-29c510ae-49a4-4cdd-8f23-b972bfab1c49-0",387        )388        ```389390        ```python391        async for chunk in model.astream("Say hello world!"):392            print(chunk.content)393        ```394395        ```python396        HEL397        LO398        WORLD399        !400        ```401402        ```python403        messages = [("human", "Say hello world!"), ("human", "Say goodbye world!")]404        await model.abatch(messages)405        ```406407        ```python408        [409            AIMessage(410                content="HELLO, WORLD!",411                response_metadata={412                    "model": "llama3",413                    "created_at": "2024-07-04T03:55:07.315396Z",414                    "message": {"role": "assistant", "content": ""},415                    "done_reason": "stop",416                    "done": True,417                    "total_duration": 1696745458,418                    "load_duration": 1505000,419                    "prompt_eval_count": 8,420                    "prompt_eval_duration": 111627000,421                    "eval_count": 6,422                    "eval_duration": 185181000,423                },424                id="run-da6c7562-e25a-4a44-987a-2c83cd8c2686-0",425            ),426            AIMessage(427                content="It's been a blast chatting with you! Say goodbye to the world for me, and don't forget to come back and visit us again soon!",428                response_metadata={429                    "model": "llama3",430                    "created_at": "2024-07-04T03:55:07.018076Z",431                    "message": {"role": "assistant", "content": ""},432                    "done_reason": "stop",433                    "done": True,434                    "total_duration": 1399391083,435                    "load_duration": 1187417,436                    "prompt_eval_count": 20,437                    "prompt_eval_duration": 230349000,438                    "eval_count": 31,439                    "eval_duration": 1166047000,440                },441                id="run-96cad530-6f3e-4cf9-86b4-e0f8abba4cdb-0",442            ),443        ]444        ```445446    JSON mode:447        ```python448        json_model = ChatOllama(format="json")449        json_model.invoke(450            "Return a query for the weather in a random location and time of day with two keys: location and time_of_day. "451            "Respond using JSON only."452        ).content453        ```454455        ```python456        '{"location": "Pune, India", "time_of_day": "morning"}'457        ```458459    Tool Calling:460        ```python461        from langchain_ollama import ChatOllama462        from pydantic import BaseModel, Field463464465        class Multiply(BaseModel):466            a: int = Field(..., description="First integer")467            b: int = Field(..., description="Second integer")468469470        ans = await chat.invoke("What is 45*67")471        ans.tool_calls472        ```473474        ```python475        [476            {477                "name": "Multiply",478                "args": {"a": 45, "b": 67},479                "id": "420c3f3b-df10-4188-945f-eb3abdb40622",480                "type": "tool_call",481            }482        ]483        ```484485    Thinking / Reasoning:486        You can enable reasoning mode for models that support it by setting487        the `reasoning` parameter to `True` in either the constructor or488        the `invoke`/`stream` methods. This will enable the model to think489        through the problem and return the reasoning process separately in the490        `additional_kwargs` of the response message, under `reasoning_content`.491492        If `reasoning` is set to `None`, the model will use its default reasoning493        behavior, and any reasoning content will *not* be captured under the494        `reasoning_content` key, but will be present within the main response content495        as think tags (`<think>` and `</think>`).496497        !!! note498            This feature is only available for [models that support reasoning](https://ollama.com/search?c=thinking).499500        ```python501        from langchain_ollama import ChatOllama502503        model = ChatOllama(504            model="deepseek-r1:8b",505            validate_model_on_init=True,506            reasoning=True,507        )508509        model.invoke("how many r in the word strawberry?")510511        # or, on an invocation basis:512513        model.invoke("how many r in the word strawberry?", reasoning=True)514        # or model.stream("how many r in the word strawberry?", reasoning=True)515516        # If not provided, the invocation will default to the ChatOllama reasoning517        # param provided (None by default).518        ```519520        ```python521        AIMessage(content='The word "strawberry" contains **three \'r\' letters**. Here\'s a breakdown for clarity:\n\n- The spelling of "strawberry" has two parts ... be 3.\n\nTo be thorough, let\'s confirm with an online source or common knowledge.\n\nI can recall that "strawberry" has: s-t-r-a-w-b-e-r-r-y — yes, three r\'s.\n\nPerhaps it\'s misspelled by some, but standard is correct.\n\nSo I think the response should be 3.\n'}, response_metadata={'model': 'deepseek-r1:8b', 'created_at': '2025-07-08T19:33:55.891269Z', 'done': True, 'done_reason': 'stop', 'total_duration': 98232561292, 'load_duration': 28036792, 'prompt_eval_count': 10, 'prompt_eval_duration': 40171834, 'eval_count': 3615, 'eval_duration': 98163832416, 'model_name': 'deepseek-r1:8b'}, id='run--18f8269f-6a35-4a7c-826d-b89d52c753b3-0', usage_metadata={'input_tokens': 10, 'output_tokens': 3615, 'total_tokens': 3625})522523        ```524    """  # noqa: E501, pylint: disable=line-too-long525526    model: str527    """Model name to use."""528529    reasoning: bool | str | None = None530    """Controls the reasoning/thinking mode for [supported models](https://ollama.com/search?c=thinking).531532    - `True`: Enables reasoning mode. The model's reasoning process will be533        captured and returned separately in the `additional_kwargs` of the534        response message, under `reasoning_content`. The main response535        content will not include the reasoning tags.536    - `False`: Disables reasoning mode. The model will not perform any reasoning,537        and the response will not include any reasoning content.538    - `None` (Default): The model will use its default reasoning behavior. Note539        however, if the model's default behavior *is* to perform reasoning, think tags540        (`<think>` and `</think>`) will be present within the main response content541        unless you set `reasoning` to `True`.542    - `str`: e.g. `'low'`, `'medium'`, `'high'`. Enables reasoning with a custom543        intensity level. Currently, this is only supported `gpt-oss`. See the544        [Ollama docs](https://github.com/ollama/ollama-python/blob/da79e987f0ac0a4986bf396f043b36ef840370bc/ollama/_types.py#L210)545        for more information.546    """547548    validate_model_on_init: bool = False549    """Whether to validate the model exists in Ollama locally on initialization.550551    !!! version-added "Added in `langchain-ollama` 0.3.4"552    """553554    mirostat: int | None = None555    """Enable Mirostat sampling for controlling perplexity.556557    (Default: `0`, `0` = disabled, `1` = Mirostat, `2` = Mirostat 2.0)558    """559560    mirostat_eta: float | None = None561    """Influences how quickly the algorithm responds to feedback from generated text.562563    A lower learning rate will result in slower adjustments, while a higher learning564    rate will make the algorithm more responsive.565566    (Default: `0.1`)567    """568569    mirostat_tau: float | None = None570    """Controls the balance between coherence and diversity of the output.571572    A lower value will result in more focused and coherent text.573574    (Default: `5.0`)575    """576577    num_ctx: int | None = None578    """Sets the size of the context window used to generate the next token.579580    (Default: `2048`)581    """582583    num_gpu: int | None = None584    """The number of GPUs to use.585586    On macOS it defaults to `1` to enable metal support, `0` to disable.587    """588589    num_thread: int | None = None590    """Sets the number of threads to use during computation.591592    By default, Ollama will detect this for optimal performance. It is recommended to593    set this value to the number of physical CPU cores your system has (as opposed to594    the logical number of cores).595    """596597    num_predict: int | None = None598    """Maximum number of tokens to predict when generating text.599600    (Default: `128`, `-1` = infinite generation, `-2` = fill context)601    """602603    repeat_last_n: int | None = None604    """Sets how far back for the model to look back to prevent repetition.605606    (Default: `64`, `0` = disabled, `-1` = `num_ctx`)607    """608609    repeat_penalty: float | None = None610    """Sets how strongly to penalize repetitions.611612    A higher value (e.g., `1.5`) will penalize repetitions more strongly, while a613    lower value (e.g., `0.9`) will be more lenient. (Default: `1.1`)614    """615616    temperature: float | None = None617    """The temperature of the model.618619    Increasing the temperature will make the model answer more creatively.620621    (Default: `0.8`)622    """623624    seed: int | None = None625    """Sets the random number seed to use for generation.626627    Setting this to a specific number will make the model generate the same text for the628    same prompt.629    """630631    logprobs: bool | None = None632    """Whether to return logprobs.633634    !!! note635636        When streaming, per-token logprobs are available on each intermediate637        chunk (via `response_metadata["logprobs"]`) and are accumulated into the638        final aggregated response when using `invoke()`.639    """640641    top_logprobs: int | None = None642    """Number of most likely tokens to return at each token position, each with643    an associated log probability. Must be a positive integer.644645    If set without `logprobs=True`, `logprobs` will be enabled automatically.646    """647648    @field_validator("top_logprobs")649    @classmethod650    def _validate_top_logprobs(cls, v: int | None) -> int | None:651        if v is not None and v < 1:652            msg = "`top_logprobs` must be a positive integer."653            raise ValueError(msg)654        return v655656    stop: list[str] | None = None657    """Sets the stop tokens to use."""658659    tfs_z: float | None = None660    """Tail free sampling.661662    Used to reduce the impact of less probable tokens from the output.663664    A higher value (e.g., `2.0`) will reduce the impact more, while a value of `1.0`665    disables this setting.666667    (Default: `1`)668    """669670    top_k: int | None = None671    """Reduces the probability of generating nonsense.672673    A higher value (e.g. `100`) will give more diverse answers, while a lower value674    (e.g. `10`) will be more conservative.675676    (Default: `40`)677    """678679    top_p: float | None = None680    """Works together with top-k.681682    A higher value (e.g., `0.95`) will lead to more diverse text, while a lower value683    (e.g., `0.5`) will generate more focused and conservative text.684685    (Default: `0.9`)686    """687688    format: Literal["", "json"] | JsonSchemaValue | None = None689    """Specify the format of the output (options: `'json'`, JSON schema)."""690691    keep_alive: int | str | None = None692    """How long the model will stay loaded into memory."""693694    base_url: str | None = None695    """Base url the model is hosted under.696697    If none, defaults to the Ollama client default.698699    Supports `userinfo` auth in the format `http://username:password@localhost:11434`.700    Useful if your Ollama server is behind a proxy.701702    !!! warning703        `userinfo` is not secure and should only be used for local testing or704        in secure environments. Avoid using it in production or over unsecured705        networks.706707    !!! note708        If using `userinfo`, ensure that the Ollama server is configured to709        accept and validate these credentials.710711    !!! note712        `userinfo` headers are passed to both sync and async clients.713714    """715716    client_kwargs: dict | None = {}717    """Additional kwargs to pass to the httpx clients. Pass headers in here.718719    These arguments are passed to both synchronous and async clients.720721    Use `sync_client_kwargs` and `async_client_kwargs` to pass different arguments722    to synchronous and asynchronous clients.723    """724725    async_client_kwargs: dict | None = {}726    """Additional kwargs to merge with `client_kwargs` before passing to httpx client.727728    These are clients unique to the async client; for shared args use `client_kwargs`.729730    For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#asyncclient).731    """732733    sync_client_kwargs: dict | None = {}734    """Additional kwargs to merge with `client_kwargs` before passing to httpx client.735736    These are clients unique to the sync client; for shared args use `client_kwargs`.737738    For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#client).739    """740741    _client: Client = PrivateAttr()742    """The client to use for making requests."""743744    _async_client: AsyncClient = PrivateAttr()745    """The async client to use for making requests."""746747    def _chat_params(748        self,749        messages: list[BaseMessage],750        stop: list[str] | None = None,751        **kwargs: Any,752    ) -> dict[str, Any]:753        """Assemble the parameters for a chat completion request.754755        Args:756            messages: List of LangChain messages to send to the model.757            stop: Optional list of stop tokens to use for this invocation.758            **kwargs: Additional keyword arguments to include in the request.759760        Returns:761            A dictionary of parameters to pass to the Ollama client.762        """763        ollama_messages = self._convert_messages_to_ollama_messages(messages)764765        if self.stop is not None and stop is not None:766            msg = "`stop` found in both the input and default params."767            raise ValueError(msg)768        if self.stop is not None:769            stop = self.stop770771        options_dict = kwargs.pop("options", None)772        if options_dict is None:773            # Only include parameters that are explicitly set (not None)774            options_dict = {775                k: v776                for k, v in {777                    "mirostat": self.mirostat,778                    "mirostat_eta": self.mirostat_eta,779                    "mirostat_tau": self.mirostat_tau,780                    "num_ctx": self.num_ctx,781                    "num_gpu": self.num_gpu,782                    "num_thread": self.num_thread,783                    "num_predict": self.num_predict,784                    "repeat_last_n": self.repeat_last_n,785                    "repeat_penalty": self.repeat_penalty,786                    "temperature": self.temperature,787                    "seed": self.seed,788                    "stop": self.stop if stop is None else stop,789                    "tfs_z": self.tfs_z,790                    "top_k": self.top_k,791                    "top_p": self.top_p,792                }.items()793                if v is not None794            }795796        format_param = self._resolve_format_param(797            kwargs.pop("format", self.format),798            kwargs.pop("response_format", None),799        )800801        params = {802            "messages": ollama_messages,803            "stream": kwargs.pop("stream", True),804            "model": kwargs.pop("model", self.model),805            "think": kwargs.pop("reasoning", self.reasoning),806            "format": format_param,807            "logprobs": kwargs.pop("logprobs", self.logprobs),808            "top_logprobs": kwargs.pop("top_logprobs", self.top_logprobs),809            "options": options_dict,810            "keep_alive": kwargs.pop("keep_alive", self.keep_alive),811            **kwargs,812        }813814        # Filter out 'strict' argument if present, as it is not supported by Ollama815        # but may be passed by upstream libraries (e.g. LangChain ProviderStrategy)816        if "strict" in params:817            params.pop("strict")818819        if tools := kwargs.get("tools"):820            params["tools"] = tools821822        return params823824    def _resolve_format_param(825        self,826        format_param: str | dict[str, Any] | None,827        response_format: Any | None,828    ) -> str | dict[str, Any] | None:829        """Resolve the format parameter.830831        Converts an OpenAI-style `response_format` dict to the `format`832        parameter expected by Ollama.833834        Args:835            format_param: The explicit `format` value (takes priority).836            response_format: An OpenAI-style `response_format` dict.837838        Returns:839            The resolved format value to pass to the Ollama client.840        """841        if format_param is not None:842            if response_format is not None:843                warnings.warn(844                    "Both 'format' and 'response_format' were provided. "845                    "'response_format' will be ignored in favor of 'format'.",846                    UserWarning,847                    stacklevel=2,848                )849            return format_param850851        if response_format is None:852            return None853854        return self._convert_response_format(response_format)855856    def _convert_response_format(857        self,858        response_format: Any,859    ) -> str | dict[str, Any] | None:860        """Convert an OpenAI-style `response_format` to an Ollama `format` value.861862        Args:863            response_format: The `response_format` value to convert.864865        Returns:866            The Ollama-compatible `format` value, or `None` if conversion fails.867        """868        if not isinstance(response_format, dict):869            warnings.warn(870                f"Ignored invalid 'response_format' type: {type(response_format)}. "871                "Expected a dictionary.",872                UserWarning,873                stacklevel=2,874            )875            return None876877        fmt_type = response_format.get("type")878        if fmt_type == "json_object":879            return "json"880        if fmt_type == "json_schema":881            return self._extract_json_schema(response_format)882883        warnings.warn(884            f"Ignored unrecognized 'response_format' type: {fmt_type}. "885            "Expected 'json_object' or 'json_schema'.",886            UserWarning,887            stacklevel=2,888        )889        return None890891    def _extract_json_schema(892        self,893        response_format: dict[str, Any],894    ) -> dict[str, Any] | None:895        """Extract the raw JSON schema from an OpenAI `json_schema` envelope.896897        Args:898            response_format: A dict with `type: "json_schema"`.899900        Returns:901            The raw JSON schema dict, or `None` if extraction fails.902        """903        json_schema_block = response_format.get("json_schema")904        if not isinstance(json_schema_block, dict):905            warnings.warn(906                "response_format has type 'json_schema' but 'json_schema' "907                f"value is {type(json_schema_block)}, expected a dict "908                "containing a 'schema' key. "909                "The format parameter will not be set.",910                UserWarning,911                stacklevel=2,912            )913            return None914        schema = json_schema_block.get("schema")915        if schema is None:916            warnings.warn(917                "response_format has type 'json_schema' but no 'schema' "918                "key was found in 'json_schema'. "919                "The format parameter will not be set.",920                UserWarning,921                stacklevel=2,922            )923        return schema924925    @model_validator(mode="after")926    def _set_ollama_version(self) -> Self:927        """Set package version in metadata."""928        self._add_version("langchain-ollama", __version__)929        return self930931    @model_validator(mode="after")932    def _set_clients(self) -> Self:933        """Set clients to use for ollama."""934        if self.top_logprobs is not None and self.logprobs is not True:935            if self.logprobs is False:936                msg = (937                    "`top_logprobs` is set but `logprobs` is explicitly `False`. "938                    "Either set `logprobs=True` to use `top_logprobs`, or remove "939                    "`top_logprobs`."940                )941                raise ValueError(msg)942            # logprobs is None (unset)  auto-enable as convenience943            self.logprobs = True944            warnings.warn(945                "`top_logprobs` is set but `logprobs` was not explicitly enabled. "946                "Setting `logprobs=True` automatically.",947                UserWarning,948                stacklevel=2,949            )950951        client_kwargs = self.client_kwargs or {}952953        cleaned_url, auth_headers = parse_url_with_auth(self.base_url)954        merge_auth_headers(client_kwargs, auth_headers)955956        sync_client_kwargs = client_kwargs957        if self.sync_client_kwargs:958            sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}959960        async_client_kwargs = client_kwargs961        if self.async_client_kwargs:962            async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}963964        self._client = Client(host=cleaned_url, **sync_client_kwargs)965        self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)966        if self.validate_model_on_init:967            validate_model(self._client, self.model)968        return self969970    def _convert_messages_to_ollama_messages(971        self, messages: list[BaseMessage]972    ) -> Sequence[Message]:973        """Convert a BaseMessage list to list of messages for Ollama to consume.974975        Args:976            messages: List of BaseMessage to convert.977978        Returns:979            List of messages in Ollama format.980        """981        messages = list(messages)  # shallow copy to avoid mutating caller's list982        for idx, message in enumerate(messages):983            # Handle message content written in v1 format984            if (985                isinstance(message, AIMessage)986                and message.response_metadata.get("output_version") == "v1"987            ):988                # Unpack known v1 content to Ollama format for the request989                # Most types are passed through unchanged990                messages[idx] = message.model_copy(991                    update={992                        "content": _convert_from_v1_to_ollama(993                            cast("list[types.ContentBlock]", message.content),994                            message.response_metadata.get("model_provider"),995                        )996                    }997                )998999        ollama_messages: list = []1000        for message in messages:1001            role: str1002            tool_call_id: str | None = None1003            tool_calls: list[dict[str, Any]] | None = None1004            if isinstance(message, HumanMessage):1005                role = "user"1006            elif isinstance(message, AIMessage):1007                role = "assistant"1008                tool_calls = (1009                    [1010                        _lc_tool_call_to_openai_tool_call(tool_call)1011                        for tool_call in message.tool_calls1012                    ]1013                    if message.tool_calls1014                    else None1015                )1016            elif isinstance(message, SystemMessage):1017                role = "system"1018            elif isinstance(message, ChatMessage):1019                role = message.role1020            elif isinstance(message, ToolMessage):1021                role = "tool"1022                tool_call_id = message.tool_call_id1023            else:1024                msg = "Received unsupported message type for Ollama."1025                raise TypeError(msg)10261027            content = ""1028            images = []1029            if isinstance(message.content, str):1030                content = message.content1031            else:  # List1032                for content_part in message.content:1033                    if isinstance(content_part, str):1034                        if content:1035                            content += "\n"1036                        content += content_part1037                    elif content_part.get("type") == "text":1038                        if content:1039                            content += "\n"1040                        content += content_part["text"]1041                    elif content_part.get("type") == "tool_use":1042                        continue1043                    elif content_part.get("type") == "image_url":1044                        image_url = None1045                        temp_image_url = content_part.get("image_url")1046                        if isinstance(temp_image_url, str):1047                            image_url = temp_image_url1048                        elif (1049                            isinstance(temp_image_url, dict)1050                            and "url" in temp_image_url1051                            and isinstance(temp_image_url["url"], str)1052                        ):1053                            image_url = temp_image_url["url"]1054                        else:1055                            msg = (1056                                "Only string image_url or dict with string 'url' "1057                                "inside content parts are supported."1058                            )1059                            raise ValueError(msg)10601061                        image_url_components = image_url.split(",")1062                        # Support data:image/jpeg;base64,<image> format1063                        # and base64 strings1064                        if len(image_url_components) > 1:1065                            images.append(image_url_components[1])1066                        else:1067                            images.append(image_url_components[0])1068                    elif is_data_content_block(content_part):1069                        # Handles v1 "image" type1070                        image = _get_image_from_data_content_block(content_part)1071                        images.append(image)1072                    else:1073                        msg = (1074                            "Unsupported message content type. "1075                            "Must either have type 'text' or type 'image_url' "1076                            "with a string 'image_url' field."1077                        )1078                        raise ValueError(msg)1079            # Should convert to ollama.Message once role includes tool, and tool_call_id1080            # is in Message1081            msg_: dict = {1082                "role": role,1083                "content": content,1084                "images": images,1085            }1086            if tool_calls:1087                msg_["tool_calls"] = tool_calls1088            if tool_call_id:1089                msg_["tool_call_id"] = tool_call_id1090            if isinstance(message, AIMessage):1091                thinking = message.additional_kwargs.get("reasoning_content")1092                if thinking is not None:1093                    msg_["thinking"] = thinking1094            ollama_messages.append(msg_)10951096        return ollama_messages10971098    async def _acreate_chat_stream(1099        self,1100        messages: list[BaseMessage],1101        stop: list[str] | None = None,1102        **kwargs: Any,1103    ) -> AsyncIterator[Mapping[str, Any] | str]:1104        if not self._async_client:1105            msg = (1106                "Ollama async client is not initialized. "1107                "Make sure the model was properly constructed."1108            )1109            raise RuntimeError(msg)1110        chat_params = self._chat_params(messages, stop, **kwargs)11111112        if chat_params["stream"]:1113            async for part in await self._async_client.chat(**chat_params):1114                yield part1115        else:1116            yield await self._async_client.chat(**chat_params)11171118    def _create_chat_stream(1119        self,1120        messages: list[BaseMessage],1121        stop: list[str] | None = None,1122        **kwargs: Any,1123    ) -> Iterator[Mapping[str, Any] | str]:1124        if not self._client:1125            msg = (1126                "Ollama sync client is not initialized. "1127                "Make sure the model was properly constructed."1128            )1129            raise RuntimeError(msg)1130        chat_params = self._chat_params(messages, stop, **kwargs)11311132        if chat_params["stream"]:1133            yield from self._client.chat(**chat_params)1134        else:1135            yield self._client.chat(**chat_params)11361137    def _chat_stream_with_aggregation(1138        self,1139        messages: list[BaseMessage],1140        stop: list[str] | None = None,1141        run_manager: CallbackManagerForLLMRun | None = None,1142        verbose: bool = False,  # noqa: FBT0021143        **kwargs: Any,1144    ) -> ChatGenerationChunk:1145        final_chunk = None1146        for chunk in self._iterate_over_stream(messages, stop, **kwargs):1147            if final_chunk is None:1148                final_chunk = chunk1149            else:1150                final_chunk += chunk1151            if run_manager:1152                run_manager.on_llm_new_token(1153                    chunk.text,1154                    chunk=chunk,1155                    verbose=verbose,1156                )1157        if final_chunk is None:1158            msg = "No data received from Ollama stream."1159            raise ValueError(msg)11601161        return final_chunk11621163    async def _achat_stream_with_aggregation(1164        self,1165        messages: list[BaseMessage],1166        stop: list[str] | None = None,1167        run_manager: AsyncCallbackManagerForLLMRun | None = None,1168        verbose: bool = False,  # noqa: FBT0021169        **kwargs: Any,1170    ) -> ChatGenerationChunk:1171        final_chunk = None1172        async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1173            if final_chunk is None:1174                final_chunk = chunk1175            else:1176                final_chunk += chunk1177            if run_manager:1178                await run_manager.on_llm_new_token(1179                    chunk.text,1180                    chunk=chunk,1181                    verbose=verbose,1182                )1183        if final_chunk is None:1184            msg = "No data received from Ollama stream."1185            raise ValueError(msg)11861187        return final_chunk11881189    def _get_ls_params(1190        self, stop: list[str] | None = None, **kwargs: Any1191    ) -> LangSmithParams:1192        """Get standard params for tracing."""1193        params = self._get_invocation_params(stop=stop, **kwargs)1194        ls_params = LangSmithParams(1195            ls_provider="ollama",1196            ls_model_name=params.get("model", self.model),1197            ls_model_type="chat",1198            ls_temperature=params.get("temperature", self.temperature),1199        )1200        if ls_stop := stop or params.get("stop", None) or self.stop:1201            ls_params["ls_stop"] = ls_stop1202        return ls_params12031204    def _generate(1205        self,1206        messages: list[BaseMessage],1207        stop: list[str] | None = None,1208        run_manager: CallbackManagerForLLMRun | None = None,1209        **kwargs: Any,1210    ) -> ChatResult:1211        final_chunk = self._chat_stream_with_aggregation(1212            messages, stop, run_manager, verbose=self.verbose, **kwargs1213        )1214        generation_info = final_chunk.generation_info1215        chat_generation = ChatGeneration(1216            message=AIMessage(1217                content=final_chunk.text,1218                usage_metadata=cast(1219                    "AIMessageChunk", final_chunk.message1220                ).usage_metadata,1221                tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1222                additional_kwargs=final_chunk.message.additional_kwargs,1223            ),1224            generation_info=generation_info,1225        )1226        return ChatResult(generations=[chat_generation])12271228    def _iterate_over_stream(1229        self,1230        messages: list[BaseMessage],1231        stop: list[str] | None = None,1232        **kwargs: Any,1233    ) -> Iterator[ChatGenerationChunk]:1234        reasoning = kwargs.get("reasoning", self.reasoning)1235        for stream_resp in self._create_chat_stream(messages, stop, **kwargs):1236            if not isinstance(stream_resp, str):1237                content = (1238                    stream_resp["message"]["content"]1239                    if "message" in stream_resp and "content" in stream_resp["message"]1240                    else ""1241                )12421243                # Warn and skip responses with done_reason: 'load' and empty content1244                # These indicate the model was loaded but no actual generation occurred1245                is_load_response_with_empty_content = (1246                    stream_resp.get("done") is True1247                    and stream_resp.get("done_reason") == "load"1248                    and not content.strip()1249                )12501251                if is_load_response_with_empty_content:1252                    log.warning(1253                        "Ollama returned empty response with done_reason='load'."1254                        "This typically indicates the model was loaded but no content "1255                        "was generated. Skipping this response."1256                    )1257                    continue12581259                if stream_resp.get("done") is True:1260                    generation_info = dict(stream_resp)1261                    if "model" in generation_info:1262                        generation_info["model_name"] = generation_info["model"]1263                    generation_info["model_provider"] = "ollama"1264                    _ = generation_info.pop("message", None)1265                else:1266                    chunk_logprobs = stream_resp.get("logprobs")1267                    generation_info = (1268                        {"logprobs": chunk_logprobs}1269                        if chunk_logprobs is not None1270                        else None1271                    )12721273                additional_kwargs = {}1274                if (1275                    reasoning1276                    and "message" in stream_resp1277                    and (thinking_content := stream_resp["message"].get("thinking"))1278                ):1279                    additional_kwargs["reasoning_content"] = thinking_content12801281                chunk = ChatGenerationChunk(1282                    message=AIMessageChunk(1283                        content=content,1284                        additional_kwargs=additional_kwargs,1285                        usage_metadata=_get_usage_metadata_from_generation_info(1286                            stream_resp1287                        ),1288                        tool_calls=_get_tool_calls_from_response(stream_resp),1289                    ),1290                    generation_info=generation_info,1291                )12921293                yield chunk12941295    def _stream(1296        self,1297        messages: list[BaseMessage],1298        stop: list[str] | None = None,1299        run_manager: CallbackManagerForLLMRun | None = None,1300        **kwargs: Any,1301    ) -> Iterator[ChatGenerationChunk]:1302        for chunk in self._iterate_over_stream(messages, stop, **kwargs):1303            if run_manager:1304                run_manager.on_llm_new_token(1305                    chunk.text,1306                    verbose=self.verbose,1307                )1308            yield chunk13091310    async def _aiterate_over_stream(1311        self,1312        messages: list[BaseMessage],1313        stop: list[str] | None = None,1314        **kwargs: Any,1315    ) -> AsyncIterator[ChatGenerationChunk]:1316        reasoning = kwargs.get("reasoning", self.reasoning)1317        async for stream_resp in self._acreate_chat_stream(messages, stop, **kwargs):1318            if not isinstance(stream_resp, str):1319                content = (1320                    stream_resp["message"]["content"]1321                    if "message" in stream_resp and "content" in stream_resp["message"]1322                    else ""1323                )13241325                # Warn and skip responses with done_reason: 'load' and empty content1326                # These indicate the model was loaded but no actual generation occurred1327                is_load_response_with_empty_content = (1328                    stream_resp.get("done") is True1329                    and stream_resp.get("done_reason") == "load"1330                    and not content.strip()1331                )13321333                if is_load_response_with_empty_content:1334                    log.warning(1335                        "Ollama returned empty response with done_reason='load'. "1336                        "This typically indicates the model was loaded but no content "1337                        "was generated. Skipping this response."1338                    )1339                    continue13401341                if stream_resp.get("done") is True:1342                    generation_info = dict(stream_resp)1343                    if "model" in generation_info:1344                        generation_info["model_name"] = generation_info["model"]1345                    generation_info["model_provider"] = "ollama"1346                    _ = generation_info.pop("message", None)1347                else:1348                    chunk_logprobs = stream_resp.get("logprobs")1349                    generation_info = (1350                        {"logprobs": chunk_logprobs}1351                        if chunk_logprobs is not None1352                        else None1353                    )13541355                additional_kwargs = {}1356                if (1357                    reasoning1358                    and "message" in stream_resp1359                    and (thinking_content := stream_resp["message"].get("thinking"))1360                ):1361                    additional_kwargs["reasoning_content"] = thinking_content13621363                chunk = ChatGenerationChunk(1364                    message=AIMessageChunk(1365                        content=content,1366                        additional_kwargs=additional_kwargs,1367                        usage_metadata=_get_usage_metadata_from_generation_info(1368                            stream_resp1369                        ),1370                        tool_calls=_get_tool_calls_from_response(stream_resp),1371                    ),1372                    generation_info=generation_info,1373                )13741375                yield chunk13761377    async def _astream(1378        self,1379        messages: list[BaseMessage],1380        stop: list[str] | None = None,1381        run_manager: AsyncCallbackManagerForLLMRun | None = None,1382        **kwargs: Any,1383    ) -> AsyncIterator[ChatGenerationChunk]:1384        async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1385            if run_manager:1386                await run_manager.on_llm_new_token(1387                    chunk.text,1388                    verbose=self.verbose,1389                )1390            yield chunk13911392    async def _agenerate(1393        self,1394        messages: list[BaseMessage],1395        stop: list[str] | None = None,1396        run_manager: AsyncCallbackManagerForLLMRun | None = None,1397        **kwargs: Any,1398    ) -> ChatResult:1399        final_chunk = await self._achat_stream_with_aggregation(1400            messages, stop, run_manager, verbose=self.verbose, **kwargs1401        )1402        generation_info = final_chunk.generation_info1403        chat_generation = ChatGeneration(1404            message=AIMessage(1405                content=final_chunk.text,1406                usage_metadata=cast(1407                    "AIMessageChunk", final_chunk.message1408                ).usage_metadata,1409                tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1410                additional_kwargs=final_chunk.message.additional_kwargs,1411            ),1412            generation_info=generation_info,1413        )1414        return ChatResult(generations=[chat_generation])14151416    @property1417    def _llm_type(self) -> str:1418        """Return type of chat model."""1419        return "chat-ollama"14201421    def bind_tools(1422        self,1423        tools: Sequence[dict[str, Any] | type | Callable | BaseTool],1424        *,1425        tool_choice: dict | str | Literal["auto", "any"] | bool | None = None,  # noqa: PYI051, ARG0021426        **kwargs: Any,1427    ) -> Runnable[LanguageModelInput, AIMessage]:1428        """Bind tool-like objects to this chat model.14291430        Assumes model is compatible with OpenAI tool-calling API.14311432        Args:1433            tools: A list of tool definitions to bind to this chat model.14341435                Supports any tool definition handled by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1436            tool_choice: If provided, which tool for model to call. **This parameter1437                is currently ignored as it is not supported by Ollama.**1438            kwargs: Any additional parameters are passed directly to1439                `self.bind(**kwargs)`.1440        """  # noqa: E5011441        formatted_tools = [convert_to_openai_tool(tool) for tool in tools]1442        return super().bind(tools=formatted_tools, **kwargs)14431444    def with_structured_output(1445        self,1446        schema: dict | type,1447        *,1448        method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",1449        include_raw: bool = False,1450        **kwargs: Any,1451    ) -> Runnable[LanguageModelInput, dict | BaseModel]:1452        r"""Model wrapper that returns outputs formatted to match the given schema.14531454        Args:1455            schema: The output schema. Can be passed in as:14561457                - An OpenAI function/tool schema.1458                - A JSON Schema,1459                - A `TypedDict` class,1460                - Or a Pydantic class.14611462                If `schema` is a Pydantic class then the model output will be a1463                Pydantic instance of that class, and the model-generated fields will be1464                validated by the Pydantic class. Otherwise the model output will be a1465                dict and will not be validated.14661467                See `langchain_core.utils.function_calling.convert_to_openai_tool` for1468                more on how to properly specify types and descriptions of schema fields1469                when specifying a Pydantic or `TypedDict` class.14701471            method: The method for steering model generation, one of:14721473                - `'json_schema'`:1474                    Uses Ollama's [structured output API](https://ollama.com/blog/structured-outputs)1475                - `'function_calling'`:1476                    Uses Ollama's tool-calling API1477                - `'json_mode'`:1478                    Specifies `format='json'`. Note that if using JSON mode then you1479                    must include instructions for formatting the output into the1480                    desired schema into the model call.14811482            include_raw:1483                If `False` then only the parsed structured output is returned.14841485                If an error occurs during model output parsing it will be raised.14861487                If `True` then both the raw model response (a `BaseMessage`) and the1488                parsed model response will be returned.14891490                If an error occurs during output parsing it will be caught and returned1491                as well.14921493                The final output is always a `dict` with keys `'raw'`, `'parsed'`, and1494                `'parsing_error'`.14951496            kwargs: Additional keyword args aren't supported.14971498        Returns:1499            A `Runnable` that takes same inputs as a1500                `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is1501                `False` and `schema` is a Pydantic class, `Runnable` outputs an instance1502                of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is1503                `False` then `Runnable` outputs a `dict`.15041505                If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:15061507                - `'raw'`: `BaseMessage`1508                - `'parsed'`: `None` if there was a parsing error, otherwise the type1509                    depends on the `schema` as described above.1510                - `'parsing_error'`: `BaseException | None`15111512        !!! warning "Behavior changed in `langchain-ollama` 0.2.2"15131514            Added support for structured output API via `format` parameter.15151516        !!! warning "Behavior changed in `langchain-ollama` 0.3.0"15171518            Updated default `method` to `'json_schema'`.15191520        ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=False`"15211522            ```python1523            from typing import Optional15241525            from langchain_ollama import ChatOllama1526            from pydantic import BaseModel, Field152715281529            class AnswerWithJustification(BaseModel):1530                '''An answer to the user question along with justification for the answer.'''15311532                answer: str1533                justification: str | None = Field(1534                    default=...,1535                    description="A justification for the answer.",1536                )153715381539            model = ChatOllama(model="llama3.1", temperature=0)1540            structured_model = model.with_structured_output(AnswerWithJustification)15411542            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")15431544            # -> AnswerWithJustification(1545            #     answer='They weigh the same',1546            #     justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'1547            # )1548            ```15491550        ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=True`"15511552            ```python1553            from langchain_ollama import ChatOllama1554            from pydantic import BaseModel155515561557            class AnswerWithJustification(BaseModel):1558                '''An answer to the user question along with justification for the answer.'''15591560                answer: str1561                justification: str156215631564            model = ChatOllama(model="llama3.1", temperature=0)1565            structured_model = model.with_structured_output(1566                AnswerWithJustification,1567                include_raw=True,1568            )15691570            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1571            # -> {1572            #     'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),1573            #     'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),1574            #     'parsing_error': None1575            # }1576            ```15771578        ??? note "Example: `schema=Pydantic` class, `method='function_calling'`, `include_raw=False`"15791580            ```python1581            from typing import Optional15821583            from langchain_ollama import ChatOllama1584            from pydantic import BaseModel, Field158515861587            class AnswerWithJustification(BaseModel):1588                '''An answer to the user question along with justification for the answer.'''15891590                answer: str1591                justification: str | None = Field(1592                    default=...,1593                    description="A justification for the answer.",1594                )159515961597            model = ChatOllama(model="llama3.1", temperature=0)1598            structured_model = model.with_structured_output(1599                AnswerWithJustification,1600                method="function_calling",1601            )16021603            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")16041605            # -> AnswerWithJustification(1606            #     answer='They weigh the same',1607            #     justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'1608            # )1609            ```16101611        ??? note "Example: `schema=TypedDict` class, `method='function_calling'`, `include_raw=False`"16121613            ```python1614            from typing_extensions import Annotated, TypedDict16151616            from langchain_ollama import ChatOllama161716181619            class AnswerWithJustification(TypedDict):1620                '''An answer to the user question along with justification for the answer.'''16211622                answer: str1623                justification: Annotated[str | None, None, "A justification for the answer."]162416251626            model = ChatOllama(model="llama3.1", temperature=0)1627            structured_model = model.with_structured_output(AnswerWithJustification)16281629            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1630            # -> {1631            #     'answer': 'They weigh the same',1632            #     'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'1633            # }1634            ```16351636        ??? note "Example: `schema=OpenAI` function schema, `method='function_calling'`, `include_raw=False`"16371638            ```python1639            from langchain_ollama import ChatOllama16401641            oai_schema = {1642                'name': 'AnswerWithJustification',1643                'description': 'An answer to the user question along with justification for the answer.',1644                'parameters': {1645                    'type': 'object',1646                    'properties': {1647                        'answer': {'type': 'string'},1648                        'justification': {'description': 'A justification for the answer.', 'type': 'string'}1649                    },1650                    'required': ['answer']1651                }16521653                model = ChatOllama(model="llama3.1", temperature=0)1654                structured_model = model.with_structured_output(oai_schema)16551656                structured_model.invoke(1657                    "What weighs more a pound of bricks or a pound of feathers"1658                )1659                # -> {1660                #     'answer': 'They weigh the same',1661                #     'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'1662                # }1663            ```16641665        ??? note "Example: `schema=Pydantic` class, `method='json_mode'`, `include_raw=True`"16661667            ```python1668            from langchain_ollama import ChatOllama1669            from pydantic import BaseModel167016711672            class AnswerWithJustification(BaseModel):1673                answer: str1674                justification: str167516761677            model = ChatOllama(model="llama3.1", temperature=0)1678            structured_model = model.with_structured_output(1679                AnswerWithJustification, method="json_mode", include_raw=True1680            )16811682            structured_model.invoke(1683                "Answer the following question. "1684                "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"1685                "What's heavier a pound of bricks or a pound of feathers?"1686            )1687            # -> {1688            #     'raw': AIMessage(content='{\\n    "answer": "They are both the same weight.",\\n    "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \\n}'),1689            #     'parsed': AnswerWithJustification(answer='They are both the same weight.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'),1690            #     'parsing_error': None1691            # }1692            ```16931694        """  # noqa: E5011695        _ = kwargs.pop("strict", None)1696        if kwargs:1697            msg = f"Received unsupported arguments {kwargs}"1698            raise ValueError(msg)1699        is_pydantic_schema = _is_pydantic_class(schema)1700        if method == "function_calling":1701            if schema is None:1702                msg = (1703                    "schema must be specified when method is not 'json_mode'. "1704                    "Received None."1705                )1706                raise ValueError(msg)1707            formatted_tool = convert_to_openai_tool(schema)1708            tool_name = formatted_tool["function"]["name"]1709            llm = self.bind_tools(1710                [schema],1711                tool_choice=tool_name,1712                ls_structured_output_format={1713                    "kwargs": {"method": method},1714                    "schema": formatted_tool,1715                },1716            )1717            if is_pydantic_schema:1718                output_parser: Runnable = PydanticToolsParser(1719                    tools=[schema],  # ty: ignore[invalid-argument-type]1720                    first_tool_only=True,1721                )1722            else:1723                output_parser = JsonOutputKeyToolsParser(1724                    key_name=tool_name, first_tool_only=True1725                )1726        elif method == "json_mode":1727            llm = self.bind(1728                format="json",1729                ls_structured_output_format={1730                    "kwargs": {"method": method},1731                    "schema": schema,1732                },1733            )1734            output_parser = (1735                PydanticOutputParser(pydantic_object=schema)  # ty: ignore[invalid-argument-type]1736                if is_pydantic_schema1737                else JsonOutputParser()1738            )1739        elif method == "json_schema":1740            if schema is None:1741                msg = (1742                    "schema must be specified when method is not 'json_mode'. "1743                    "Received None."1744                )1745                raise ValueError(msg)1746            if is_pydantic_schema:1747                schema = cast("TypeBaseModel", schema)1748                if issubclass(schema, BaseModelV1):1749                    response_format = schema.schema()1750                else:1751                    response_format = schema.model_json_schema()1752                llm = self.bind(1753                    format=response_format,1754                    ls_structured_output_format={1755                        "kwargs": {"method": method},1756                        "schema": schema,1757                    },1758                )1759                output_parser = PydanticOutputParser(pydantic_object=schema)1760            else:1761                if is_typeddict(schema):1762                    response_format = convert_to_json_schema(schema)1763                    if "required" not in response_format:1764                        response_format["required"] = list(1765                            response_format["properties"].keys()1766                        )1767                else:1768                    # is JSON schema1769                    response_format = cast("dict", schema)1770                llm = self.bind(1771                    format=response_format,1772                    ls_structured_output_format={1773                        "kwargs": {"method": method},1774                        "schema": response_format,1775                    },1776                )1777                output_parser = JsonOutputParser()1778        else:1779            msg = (1780                f"Unrecognized method argument. Expected one of 'function_calling', "1781                f"'json_schema', or 'json_mode'. Received: '{method}'"1782            )1783            raise ValueError(msg)17841785        if include_raw:1786            parser_assign = RunnablePassthrough.assign(1787                parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None1788            )1789            parser_none = RunnablePassthrough.assign(parsed=lambda _: None)1790            parser_with_fallback = parser_assign.with_fallbacks(1791                [parser_none], exception_key="parsing_error"1792            )1793            return RunnableMap(raw=llm) | parser_with_fallback1794        return llm | output_parser

Code quality findings 27

Avoid due to security risks; use ast.literal_eval for safer evaluation of literals
eval-usage
return ast.literal_eval(json_string)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(arguments, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(value, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(parsed_value, (dict, list)):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
return isinstance(obj, type) and is_basemodel_subclass(obj)
Use logging module for better control and configurability
print-statement
print(chunk.text, end="")
Use logging module for better control and configurability
print-statement
print(chunk.content)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(response_format, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(json_schema_block, dict):
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
messages = list(messages) # shallow copy to avoid mutating caller's list
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, HumanMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(message, AIMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(message, SystemMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(message, ChatMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(message, ToolMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message.content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content_part, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(temp_image_url, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(temp_image_url, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(temp_image_url["url"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, AIMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(stream_resp, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(stream_resp, str):
Ensure functions have docstrings for documentation
missing-docstring
def bind_tools(
Ensure functions have docstrings for documentation
missing-docstring
def with_structured_output(
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
response_format["required"] = list(

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.