libs/partners/ollama/langchain_ollama/chat_models.py PYTHON 1,788 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)9899log = logging.getLogger(__name__)100101102def _get_usage_metadata_from_generation_info(103    generation_info: Mapping[str, Any] | None,104) -> UsageMetadata | None:105    """Get usage metadata from Ollama generation info mapping."""106    if generation_info is None:107        return None108    input_tokens: int | None = generation_info.get("prompt_eval_count")109    output_tokens: int | None = generation_info.get("eval_count")110    if input_tokens is not None and output_tokens is not None:111        return UsageMetadata(112            input_tokens=input_tokens,113            output_tokens=output_tokens,114            total_tokens=input_tokens + output_tokens,115        )116    return None117118119def _parse_json_string(120    json_string: str,121    *,122    raw_tool_call: dict[str, Any],123    skip: bool,124) -> Any:125    """Attempt to parse a JSON string for tool calling.126127    It first tries to use the standard `json.loads`. If that fails, it falls128    back to `ast.literal_eval` to safely parse Python literals, which is more129    robust against models using single quotes or containing apostrophes.130131    Args:132        json_string: JSON string to parse.133        raw_tool_call: Raw tool call to include in error message.134        skip: Whether to ignore parsing errors and return the value anyways.135136    Returns:137        The parsed JSON string or Python literal.138139    Raises:140        OutputParserException: If the string is invalid and `skip=False`.141    """142    try:143        return json.loads(json_string)144    except json.JSONDecodeError:145        try:146            # Use ast.literal_eval to safely parse Python-style dicts147            # (e.g. with single quotes)148            return ast.literal_eval(json_string)149        except (SyntaxError, ValueError) as e:150            # If both fail, and we're not skipping, raise an informative error.151            if skip:152                return json_string153            msg = (154                f"Function {raw_tool_call['function']['name']} arguments:\n\n"155                f"{raw_tool_call['function']['arguments']}"156                "\n\nare not valid JSON or a Python literal. "157                f"Received error: {e}"158            )159            raise OutputParserException(msg) from e160    except TypeError as e:161        if skip:162            return json_string163        msg = (164            f"Function {raw_tool_call['function']['name']} arguments:\n\n"165            f"{raw_tool_call['function']['arguments']}\n\nare not a string or a "166            f"dictionary. Received TypeError {e}"167        )168        raise OutputParserException(msg) from e169170171def _parse_arguments_from_tool_call(172    raw_tool_call: dict[str, Any],173) -> dict[str, Any] | None:174    """Parse arguments by trying to parse any shallowly nested string-encoded JSON.175176    Band-aid fix for issue in Ollama with inconsistent tool call argument structure.177    Should be removed/changed if fixed upstream.178179    See https://github.com/ollama/ollama/issues/6155180    """181    if "function" not in raw_tool_call:182        return None183    function_name = raw_tool_call["function"]["name"]184    arguments = raw_tool_call["function"]["arguments"]185    parsed_arguments: dict = {}186    if isinstance(arguments, dict):187        for key, value in arguments.items():188            # Filter out metadata fields like 'functionName' that echo function name189            if key == "functionName" and value == function_name:190                continue191            if isinstance(value, str):192                parsed_value = _parse_json_string(193                    value, skip=True, raw_tool_call=raw_tool_call194                )195                if isinstance(parsed_value, (dict, list)):196                    parsed_arguments[key] = parsed_value197                else:198                    parsed_arguments[key] = value199            else:200                parsed_arguments[key] = value201    else:202        parsed_arguments = _parse_json_string(203            arguments, skip=False, raw_tool_call=raw_tool_call204        )205    return parsed_arguments206207208def _get_tool_calls_from_response(209    response: Mapping[str, Any],210) -> list[ToolCall]:211    """Get tool calls from Ollama response."""212    tool_calls = []213    if "message" in response and (214        raw_tool_calls := response["message"].get("tool_calls")215    ):216        tool_calls.extend(217            [218                tool_call(219                    id=str(uuid4()),220                    name=tc["function"]["name"],221                    args=_parse_arguments_from_tool_call(tc) or {},222                )223                for tc in raw_tool_calls224            ]225        )226    return tool_calls227228229def _lc_tool_call_to_openai_tool_call(tool_call_: ToolCall) -> dict:230    """Convert a LangChain tool call to an OpenAI tool call format."""231    return {232        "type": "function",233        "id": tool_call_["id"],234        "function": {235            "name": tool_call_["name"],236            "arguments": tool_call_["args"],237        },238    }239240241def _get_image_from_data_content_block(block: dict) -> str:242    """Format standard data content block to format expected by Ollama."""243    if block["type"] == "image":244        if block.get("source_type") == "base64":245            # v0 style246            return block["data"]247        if block.get("base64"):248            # v1 content blocks249            return block["base64"]250        error_message = "Image data only supported through in-line base64 format."251        raise ValueError(error_message)252253    error_message = f"Blocks of type {block['type']} not supported."254    raise ValueError(error_message)255256257def _is_pydantic_class(obj: Any) -> bool:258    return isinstance(obj, type) and is_basemodel_subclass(obj)259260261class ChatOllama(BaseChatModel):262    r"""Ollama chat model integration.263264    ???+ note "Setup"265266        Install `langchain-ollama` and download any models you want to use from ollama.267268        ```bash269        ollama pull gpt-oss:20b270        pip install -U langchain-ollama271        ```272273    Key init args  completion params:274        model: str275            Name of Ollama model to use.276        reasoning: bool | None277            Controls the reasoning/thinking mode for278            [supported models](https://ollama.com/search?c=thinking).279280            - `True`: Enables reasoning mode. The model's reasoning process will be281                captured and returned separately in the `additional_kwargs` of the282                response message, under `reasoning_content`. The main response283                content will not include the reasoning tags.284            - `False`: Disables reasoning mode. The model will not perform any reasoning,285                and the response will not include any reasoning content.286            - `None` (Default): The model will use its default reasoning behavior. Note287                however, if the model's default behavior *is* to perform reasoning, think tags288                (`<think>` and `</think>`) will be present within the main response content289                unless you set `reasoning` to `True`.290        temperature: float291            Sampling temperature. Ranges from `0.0` to `1.0`.292        num_predict: int | None293            Max number of tokens to generate.294295    See full list of supported init args and their descriptions in the params section.296297    Instantiate:298        ```python299        from langchain_ollama import ChatOllama300301        model = ChatOllama(302            model="gpt-oss:20b",303            validate_model_on_init=True,304            temperature=0.8,305            num_predict=256,306            # other params ...307        )308        ```309310    Invoke:311        ```python312        messages = [313            ("system", "You are a helpful translator. Translate the user sentence to French."),314            ("human", "I love programming."),315        ]316        model.invoke(messages)317        ```318319        ```python320        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')321        ```322323    Stream:324        ```python325        for chunk in model.stream("Return the words Hello World!"):326            print(chunk.text, end="")327        ```328329        ```python330        content='Hello' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'331        content=' World' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'332        content='!' id='run-327ff5ad-45c8-49fe-965c-0a93982e9be1'333        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'334335        ```336337        ```python338        stream = model.stream(messages)339        full = next(stream)340        for chunk in stream:341            full += chunk342        full343        ```344345        ```python346        AIMessageChunk(347            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")',348            response_metadata={349                "model": "llama3",350                "created_at": "2024-07-04T03:38:54.933154Z",351                "message": {"role": "assistant", "content": ""},352                "done_reason": "stop",353                "done": True,354                "total_duration": 1977300042,355                "load_duration": 1345709,356                "prompt_eval_duration": 159343000,357                "eval_count": 47,358                "eval_duration": 1815123000,359            },360            id="run-3c81a3ed-3e79-4dd3-a796-04064d804890",361        )362        ```363364    Async:365        ```python366        await model.ainvoke("Hello how are you!")367        ```368369        ```python370        AIMessage(371            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?",372            response_metadata={373                "model": "llama3",374                "created_at": "2024-07-04T03:52:08.165478Z",375                "message": {"role": "assistant", "content": ""},376                "done_reason": "stop",377                "done": True,378                "total_duration": 2138492875,379                "load_duration": 1364000,380                "prompt_eval_count": 10,381                "prompt_eval_duration": 297081000,382                "eval_count": 47,383                "eval_duration": 1838524000,384            },385            id="run-29c510ae-49a4-4cdd-8f23-b972bfab1c49-0",386        )387        ```388389        ```python390        async for chunk in model.astream("Say hello world!"):391            print(chunk.content)392        ```393394        ```python395        HEL396        LO397        WORLD398        !399        ```400401        ```python402        messages = [("human", "Say hello world!"), ("human", "Say goodbye world!")]403        await model.abatch(messages)404        ```405406        ```python407        [408            AIMessage(409                content="HELLO, WORLD!",410                response_metadata={411                    "model": "llama3",412                    "created_at": "2024-07-04T03:55:07.315396Z",413                    "message": {"role": "assistant", "content": ""},414                    "done_reason": "stop",415                    "done": True,416                    "total_duration": 1696745458,417                    "load_duration": 1505000,418                    "prompt_eval_count": 8,419                    "prompt_eval_duration": 111627000,420                    "eval_count": 6,421                    "eval_duration": 185181000,422                },423                id="run-da6c7562-e25a-4a44-987a-2c83cd8c2686-0",424            ),425            AIMessage(426                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!",427                response_metadata={428                    "model": "llama3",429                    "created_at": "2024-07-04T03:55:07.018076Z",430                    "message": {"role": "assistant", "content": ""},431                    "done_reason": "stop",432                    "done": True,433                    "total_duration": 1399391083,434                    "load_duration": 1187417,435                    "prompt_eval_count": 20,436                    "prompt_eval_duration": 230349000,437                    "eval_count": 31,438                    "eval_duration": 1166047000,439                },440                id="run-96cad530-6f3e-4cf9-86b4-e0f8abba4cdb-0",441            ),442        ]443        ```444445    JSON mode:446        ```python447        json_model = ChatOllama(format="json")448        json_model.invoke(449            "Return a query for the weather in a random location and time of day with two keys: location and time_of_day. "450            "Respond using JSON only."451        ).content452        ```453454        ```python455        '{"location": "Pune, India", "time_of_day": "morning"}'456        ```457458    Tool Calling:459        ```python460        from langchain_ollama import ChatOllama461        from pydantic import BaseModel, Field462463464        class Multiply(BaseModel):465            a: int = Field(..., description="First integer")466            b: int = Field(..., description="Second integer")467468469        ans = await chat.invoke("What is 45*67")470        ans.tool_calls471        ```472473        ```python474        [475            {476                "name": "Multiply",477                "args": {"a": 45, "b": 67},478                "id": "420c3f3b-df10-4188-945f-eb3abdb40622",479                "type": "tool_call",480            }481        ]482        ```483484    Thinking / Reasoning:485        You can enable reasoning mode for models that support it by setting486        the `reasoning` parameter to `True` in either the constructor or487        the `invoke`/`stream` methods. This will enable the model to think488        through the problem and return the reasoning process separately in the489        `additional_kwargs` of the response message, under `reasoning_content`.490491        If `reasoning` is set to `None`, the model will use its default reasoning492        behavior, and any reasoning content will *not* be captured under the493        `reasoning_content` key, but will be present within the main response content494        as think tags (`<think>` and `</think>`).495496        !!! note497            This feature is only available for [models that support reasoning](https://ollama.com/search?c=thinking).498499        ```python500        from langchain_ollama import ChatOllama501502        model = ChatOllama(503            model="deepseek-r1:8b",504            validate_model_on_init=True,505            reasoning=True,506        )507508        model.invoke("how many r in the word strawberry?")509510        # or, on an invocation basis:511512        model.invoke("how many r in the word strawberry?", reasoning=True)513        # or model.stream("how many r in the word strawberry?", reasoning=True)514515        # If not provided, the invocation will default to the ChatOllama reasoning516        # param provided (None by default).517        ```518519        ```python520        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})521522        ```523    """  # noqa: E501, pylint: disable=line-too-long524525    model: str526    """Model name to use."""527528    reasoning: bool | str | None = None529    """Controls the reasoning/thinking mode for [supported models](https://ollama.com/search?c=thinking).530531    - `True`: Enables reasoning mode. The model's reasoning process will be532        captured and returned separately in the `additional_kwargs` of the533        response message, under `reasoning_content`. The main response534        content will not include the reasoning tags.535    - `False`: Disables reasoning mode. The model will not perform any reasoning,536        and the response will not include any reasoning content.537    - `None` (Default): The model will use its default reasoning behavior. Note538        however, if the model's default behavior *is* to perform reasoning, think tags539        (`<think>` and `</think>`) will be present within the main response content540        unless you set `reasoning` to `True`.541    - `str`: e.g. `'low'`, `'medium'`, `'high'`. Enables reasoning with a custom542        intensity level. Currently, this is only supported `gpt-oss`. See the543        [Ollama docs](https://github.com/ollama/ollama-python/blob/da79e987f0ac0a4986bf396f043b36ef840370bc/ollama/_types.py#L210)544        for more information.545    """546547    validate_model_on_init: bool = False548    """Whether to validate the model exists in Ollama locally on initialization.549550    !!! version-added "Added in `langchain-ollama` 0.3.4"551    """552553    mirostat: int | None = None554    """Enable Mirostat sampling for controlling perplexity.555556    (Default: `0`, `0` = disabled, `1` = Mirostat, `2` = Mirostat 2.0)557    """558559    mirostat_eta: float | None = None560    """Influences how quickly the algorithm responds to feedback from generated text.561562    A lower learning rate will result in slower adjustments, while a higher learning563    rate will make the algorithm more responsive.564565    (Default: `0.1`)566    """567568    mirostat_tau: float | None = None569    """Controls the balance between coherence and diversity of the output.570571    A lower value will result in more focused and coherent text.572573    (Default: `5.0`)574    """575576    num_ctx: int | None = None577    """Sets the size of the context window used to generate the next token.578579    (Default: `2048`)580    """581582    num_gpu: int | None = None583    """The number of GPUs to use.584585    On macOS it defaults to `1` to enable metal support, `0` to disable.586    """587588    num_thread: int | None = None589    """Sets the number of threads to use during computation.590591    By default, Ollama will detect this for optimal performance. It is recommended to592    set this value to the number of physical CPU cores your system has (as opposed to593    the logical number of cores).594    """595596    num_predict: int | None = None597    """Maximum number of tokens to predict when generating text.598599    (Default: `128`, `-1` = infinite generation, `-2` = fill context)600    """601602    repeat_last_n: int | None = None603    """Sets how far back for the model to look back to prevent repetition.604605    (Default: `64`, `0` = disabled, `-1` = `num_ctx`)606    """607608    repeat_penalty: float | None = None609    """Sets how strongly to penalize repetitions.610611    A higher value (e.g., `1.5`) will penalize repetitions more strongly, while a612    lower value (e.g., `0.9`) will be more lenient. (Default: `1.1`)613    """614615    temperature: float | None = None616    """The temperature of the model.617618    Increasing the temperature will make the model answer more creatively.619620    (Default: `0.8`)621    """622623    seed: int | None = None624    """Sets the random number seed to use for generation.625626    Setting this to a specific number will make the model generate the same text for the627    same prompt.628    """629630    logprobs: bool | None = None631    """Whether to return logprobs.632633    !!! note634635        When streaming, per-token logprobs are available on each intermediate636        chunk (via `response_metadata["logprobs"]`) and are accumulated into the637        final aggregated response when using `invoke()`.638    """639640    top_logprobs: int | None = None641    """Number of most likely tokens to return at each token position, each with642    an associated log probability. Must be a positive integer.643644    If set without `logprobs=True`, `logprobs` will be enabled automatically.645    """646647    @field_validator("top_logprobs")648    @classmethod649    def _validate_top_logprobs(cls, v: int | None) -> int | None:650        if v is not None and v < 1:651            msg = "`top_logprobs` must be a positive integer."652            raise ValueError(msg)653        return v654655    stop: list[str] | None = None656    """Sets the stop tokens to use."""657658    tfs_z: float | None = None659    """Tail free sampling.660661    Used to reduce the impact of less probable tokens from the output.662663    A higher value (e.g., `2.0`) will reduce the impact more, while a value of `1.0`664    disables this setting.665666    (Default: `1`)667    """668669    top_k: int | None = None670    """Reduces the probability of generating nonsense.671672    A higher value (e.g. `100`) will give more diverse answers, while a lower value673    (e.g. `10`) will be more conservative.674675    (Default: `40`)676    """677678    top_p: float | None = None679    """Works together with top-k.680681    A higher value (e.g., `0.95`) will lead to more diverse text, while a lower value682    (e.g., `0.5`) will generate more focused and conservative text.683684    (Default: `0.9`)685    """686687    format: Literal["", "json"] | JsonSchemaValue | None = None688    """Specify the format of the output (options: `'json'`, JSON schema)."""689690    keep_alive: int | str | None = None691    """How long the model will stay loaded into memory."""692693    base_url: str | None = None694    """Base url the model is hosted under.695696    If none, defaults to the Ollama client default.697698    Supports `userinfo` auth in the format `http://username:password@localhost:11434`.699    Useful if your Ollama server is behind a proxy.700701    !!! warning702        `userinfo` is not secure and should only be used for local testing or703        in secure environments. Avoid using it in production or over unsecured704        networks.705706    !!! note707        If using `userinfo`, ensure that the Ollama server is configured to708        accept and validate these credentials.709710    !!! note711        `userinfo` headers are passed to both sync and async clients.712713    """714715    client_kwargs: dict | None = {}716    """Additional kwargs to pass to the httpx clients. Pass headers in here.717718    These arguments are passed to both synchronous and async clients.719720    Use `sync_client_kwargs` and `async_client_kwargs` to pass different arguments721    to synchronous and asynchronous clients.722    """723724    async_client_kwargs: dict | None = {}725    """Additional kwargs to merge with `client_kwargs` before passing to httpx client.726727    These are clients unique to the async client; for shared args use `client_kwargs`.728729    For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#asyncclient).730    """731732    sync_client_kwargs: dict | None = {}733    """Additional kwargs to merge with `client_kwargs` before passing to httpx client.734735    These are clients unique to the sync client; for shared args use `client_kwargs`.736737    For a full list of the params, see the [httpx documentation](https://www.python-httpx.org/api/#client).738    """739740    _client: Client = PrivateAttr()741    """The client to use for making requests."""742743    _async_client: AsyncClient = PrivateAttr()744    """The async client to use for making requests."""745746    def _chat_params(747        self,748        messages: list[BaseMessage],749        stop: list[str] | None = None,750        **kwargs: Any,751    ) -> dict[str, Any]:752        """Assemble the parameters for a chat completion request.753754        Args:755            messages: List of LangChain messages to send to the model.756            stop: Optional list of stop tokens to use for this invocation.757            **kwargs: Additional keyword arguments to include in the request.758759        Returns:760            A dictionary of parameters to pass to the Ollama client.761        """762        ollama_messages = self._convert_messages_to_ollama_messages(messages)763764        if self.stop is not None and stop is not None:765            msg = "`stop` found in both the input and default params."766            raise ValueError(msg)767        if self.stop is not None:768            stop = self.stop769770        options_dict = kwargs.pop("options", None)771        if options_dict is None:772            # Only include parameters that are explicitly set (not None)773            options_dict = {774                k: v775                for k, v in {776                    "mirostat": self.mirostat,777                    "mirostat_eta": self.mirostat_eta,778                    "mirostat_tau": self.mirostat_tau,779                    "num_ctx": self.num_ctx,780                    "num_gpu": self.num_gpu,781                    "num_thread": self.num_thread,782                    "num_predict": self.num_predict,783                    "repeat_last_n": self.repeat_last_n,784                    "repeat_penalty": self.repeat_penalty,785                    "temperature": self.temperature,786                    "seed": self.seed,787                    "stop": self.stop if stop is None else stop,788                    "tfs_z": self.tfs_z,789                    "top_k": self.top_k,790                    "top_p": self.top_p,791                }.items()792                if v is not None793            }794795        format_param = self._resolve_format_param(796            kwargs.pop("format", self.format),797            kwargs.pop("response_format", None),798        )799800        params = {801            "messages": ollama_messages,802            "stream": kwargs.pop("stream", True),803            "model": kwargs.pop("model", self.model),804            "think": kwargs.pop("reasoning", self.reasoning),805            "format": format_param,806            "logprobs": kwargs.pop("logprobs", self.logprobs),807            "top_logprobs": kwargs.pop("top_logprobs", self.top_logprobs),808            "options": options_dict,809            "keep_alive": kwargs.pop("keep_alive", self.keep_alive),810            **kwargs,811        }812813        # Filter out 'strict' argument if present, as it is not supported by Ollama814        # but may be passed by upstream libraries (e.g. LangChain ProviderStrategy)815        if "strict" in params:816            params.pop("strict")817818        if tools := kwargs.get("tools"):819            params["tools"] = tools820821        return params822823    def _resolve_format_param(824        self,825        format_param: str | dict[str, Any] | None,826        response_format: Any | None,827    ) -> str | dict[str, Any] | None:828        """Resolve the format parameter.829830        Converts an OpenAI-style `response_format` dict to the `format`831        parameter expected by Ollama.832833        Args:834            format_param: The explicit `format` value (takes priority).835            response_format: An OpenAI-style `response_format` dict.836837        Returns:838            The resolved format value to pass to the Ollama client.839        """840        if format_param is not None:841            if response_format is not None:842                warnings.warn(843                    "Both 'format' and 'response_format' were provided. "844                    "'response_format' will be ignored in favor of 'format'.",845                    UserWarning,846                    stacklevel=2,847                )848            return format_param849850        if response_format is None:851            return None852853        return self._convert_response_format(response_format)854855    def _convert_response_format(856        self,857        response_format: Any,858    ) -> str | dict[str, Any] | None:859        """Convert an OpenAI-style `response_format` to an Ollama `format` value.860861        Args:862            response_format: The `response_format` value to convert.863864        Returns:865            The Ollama-compatible `format` value, or `None` if conversion fails.866        """867        if not isinstance(response_format, dict):868            warnings.warn(869                f"Ignored invalid 'response_format' type: {type(response_format)}. "870                "Expected a dictionary.",871                UserWarning,872                stacklevel=2,873            )874            return None875876        fmt_type = response_format.get("type")877        if fmt_type == "json_object":878            return "json"879        if fmt_type == "json_schema":880            return self._extract_json_schema(response_format)881882        warnings.warn(883            f"Ignored unrecognized 'response_format' type: {fmt_type}. "884            "Expected 'json_object' or 'json_schema'.",885            UserWarning,886            stacklevel=2,887        )888        return None889890    def _extract_json_schema(891        self,892        response_format: dict[str, Any],893    ) -> dict[str, Any] | None:894        """Extract the raw JSON schema from an OpenAI ``json_schema`` envelope.895896        Args:897            response_format: A dict with ``type: "json_schema"``.898899        Returns:900            The raw JSON schema dict, or ``None`` if extraction fails.901        """902        json_schema_block = response_format.get("json_schema")903        if not isinstance(json_schema_block, dict):904            warnings.warn(905                "response_format has type 'json_schema' but 'json_schema' "906                f"value is {type(json_schema_block)}, expected a dict "907                "containing a 'schema' key. "908                "The format parameter will not be set.",909                UserWarning,910                stacklevel=2,911            )912            return None913        schema = json_schema_block.get("schema")914        if schema is None:915            warnings.warn(916                "response_format has type 'json_schema' but no 'schema' "917                "key was found in 'json_schema'. "918                "The format parameter will not be set.",919                UserWarning,920                stacklevel=2,921            )922        return schema923924    @model_validator(mode="after")925    def _set_clients(self) -> Self:926        """Set clients to use for ollama."""927        if self.top_logprobs is not None and self.logprobs is not True:928            if self.logprobs is False:929                msg = (930                    "`top_logprobs` is set but `logprobs` is explicitly `False`. "931                    "Either set `logprobs=True` to use `top_logprobs`, or remove "932                    "`top_logprobs`."933                )934                raise ValueError(msg)935            # logprobs is None (unset)  auto-enable as convenience936            self.logprobs = True937            warnings.warn(938                "`top_logprobs` is set but `logprobs` was not explicitly enabled. "939                "Setting `logprobs=True` automatically.",940                UserWarning,941                stacklevel=2,942            )943944        client_kwargs = self.client_kwargs or {}945946        cleaned_url, auth_headers = parse_url_with_auth(self.base_url)947        merge_auth_headers(client_kwargs, auth_headers)948949        sync_client_kwargs = client_kwargs950        if self.sync_client_kwargs:951            sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}952953        async_client_kwargs = client_kwargs954        if self.async_client_kwargs:955            async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}956957        self._client = Client(host=cleaned_url, **sync_client_kwargs)958        self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)959        if self.validate_model_on_init:960            validate_model(self._client, self.model)961        return self962963    def _convert_messages_to_ollama_messages(964        self, messages: list[BaseMessage]965    ) -> Sequence[Message]:966        """Convert a BaseMessage list to list of messages for Ollama to consume.967968        Args:969            messages: List of BaseMessage to convert.970971        Returns:972            List of messages in Ollama format.973        """974        messages = list(messages)  # shallow copy to avoid mutating caller's list975        for idx, message in enumerate(messages):976            # Handle message content written in v1 format977            if (978                isinstance(message, AIMessage)979                and message.response_metadata.get("output_version") == "v1"980            ):981                # Unpack known v1 content to Ollama format for the request982                # Most types are passed through unchanged983                messages[idx] = message.model_copy(984                    update={985                        "content": _convert_from_v1_to_ollama(986                            cast("list[types.ContentBlock]", message.content),987                            message.response_metadata.get("model_provider"),988                        )989                    }990                )991992        ollama_messages: list = []993        for message in messages:994            role: str995            tool_call_id: str | None = None996            tool_calls: list[dict[str, Any]] | None = None997            if isinstance(message, HumanMessage):998                role = "user"999            elif isinstance(message, AIMessage):1000                role = "assistant"1001                tool_calls = (1002                    [1003                        _lc_tool_call_to_openai_tool_call(tool_call)1004                        for tool_call in message.tool_calls1005                    ]1006                    if message.tool_calls1007                    else None1008                )1009            elif isinstance(message, SystemMessage):1010                role = "system"1011            elif isinstance(message, ChatMessage):1012                role = message.role1013            elif isinstance(message, ToolMessage):1014                role = "tool"1015                tool_call_id = message.tool_call_id1016            else:1017                msg = "Received unsupported message type for Ollama."1018                raise TypeError(msg)10191020            content = ""1021            images = []1022            if isinstance(message.content, str):1023                content = message.content1024            else:  # List1025                for content_part in message.content:1026                    if isinstance(content_part, str):1027                        if content:1028                            content += "\n"1029                        content += content_part1030                    elif content_part.get("type") == "text":1031                        if content:1032                            content += "\n"1033                        content += content_part["text"]1034                    elif content_part.get("type") == "tool_use":1035                        continue1036                    elif content_part.get("type") == "image_url":1037                        image_url = None1038                        temp_image_url = content_part.get("image_url")1039                        if isinstance(temp_image_url, str):1040                            image_url = temp_image_url1041                        elif (1042                            isinstance(temp_image_url, dict)1043                            and "url" in temp_image_url1044                            and isinstance(temp_image_url["url"], str)1045                        ):1046                            image_url = temp_image_url["url"]1047                        else:1048                            msg = (1049                                "Only string image_url or dict with string 'url' "1050                                "inside content parts are supported."1051                            )1052                            raise ValueError(msg)10531054                        image_url_components = image_url.split(",")1055                        # Support data:image/jpeg;base64,<image> format1056                        # and base64 strings1057                        if len(image_url_components) > 1:1058                            images.append(image_url_components[1])1059                        else:1060                            images.append(image_url_components[0])1061                    elif is_data_content_block(content_part):1062                        # Handles v1 "image" type1063                        image = _get_image_from_data_content_block(content_part)1064                        images.append(image)1065                    else:1066                        msg = (1067                            "Unsupported message content type. "1068                            "Must either have type 'text' or type 'image_url' "1069                            "with a string 'image_url' field."1070                        )1071                        raise ValueError(msg)1072            # Should convert to ollama.Message once role includes tool, and tool_call_id1073            # is in Message1074            msg_: dict = {1075                "role": role,1076                "content": content,1077                "images": images,1078            }1079            if tool_calls:1080                msg_["tool_calls"] = tool_calls1081            if tool_call_id:1082                msg_["tool_call_id"] = tool_call_id1083            if isinstance(message, AIMessage):1084                thinking = message.additional_kwargs.get("reasoning_content")1085                if thinking is not None:1086                    msg_["thinking"] = thinking1087            ollama_messages.append(msg_)10881089        return ollama_messages10901091    async def _acreate_chat_stream(1092        self,1093        messages: list[BaseMessage],1094        stop: list[str] | None = None,1095        **kwargs: Any,1096    ) -> AsyncIterator[Mapping[str, Any] | str]:1097        if not self._async_client:1098            msg = (1099                "Ollama async client is not initialized. "1100                "Make sure the model was properly constructed."1101            )1102            raise RuntimeError(msg)1103        chat_params = self._chat_params(messages, stop, **kwargs)11041105        if chat_params["stream"]:1106            async for part in await self._async_client.chat(**chat_params):1107                yield part1108        else:1109            yield await self._async_client.chat(**chat_params)11101111    def _create_chat_stream(1112        self,1113        messages: list[BaseMessage],1114        stop: list[str] | None = None,1115        **kwargs: Any,1116    ) -> Iterator[Mapping[str, Any] | str]:1117        if not self._client:1118            msg = (1119                "Ollama sync client is not initialized. "1120                "Make sure the model was properly constructed."1121            )1122            raise RuntimeError(msg)1123        chat_params = self._chat_params(messages, stop, **kwargs)11241125        if chat_params["stream"]:1126            yield from self._client.chat(**chat_params)1127        else:1128            yield self._client.chat(**chat_params)11291130    def _chat_stream_with_aggregation(1131        self,1132        messages: list[BaseMessage],1133        stop: list[str] | None = None,1134        run_manager: CallbackManagerForLLMRun | None = None,1135        verbose: bool = False,  # noqa: FBT0021136        **kwargs: Any,1137    ) -> ChatGenerationChunk:1138        final_chunk = None1139        for chunk in self._iterate_over_stream(messages, stop, **kwargs):1140            if final_chunk is None:1141                final_chunk = chunk1142            else:1143                final_chunk += chunk1144            if run_manager:1145                run_manager.on_llm_new_token(1146                    chunk.text,1147                    chunk=chunk,1148                    verbose=verbose,1149                )1150        if final_chunk is None:1151            msg = "No data received from Ollama stream."1152            raise ValueError(msg)11531154        return final_chunk11551156    async def _achat_stream_with_aggregation(1157        self,1158        messages: list[BaseMessage],1159        stop: list[str] | None = None,1160        run_manager: AsyncCallbackManagerForLLMRun | None = None,1161        verbose: bool = False,  # noqa: FBT0021162        **kwargs: Any,1163    ) -> ChatGenerationChunk:1164        final_chunk = None1165        async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1166            if final_chunk is None:1167                final_chunk = chunk1168            else:1169                final_chunk += chunk1170            if run_manager:1171                await run_manager.on_llm_new_token(1172                    chunk.text,1173                    chunk=chunk,1174                    verbose=verbose,1175                )1176        if final_chunk is None:1177            msg = "No data received from Ollama stream."1178            raise ValueError(msg)11791180        return final_chunk11811182    def _get_ls_params(1183        self, stop: list[str] | None = None, **kwargs: Any1184    ) -> LangSmithParams:1185        """Get standard params for tracing."""1186        params = self._get_invocation_params(stop=stop, **kwargs)1187        ls_params = LangSmithParams(1188            ls_provider="ollama",1189            ls_model_name=params.get("model", self.model),1190            ls_model_type="chat",1191            ls_temperature=params.get("temperature", self.temperature),1192        )1193        if ls_stop := stop or params.get("stop", None) or self.stop:1194            ls_params["ls_stop"] = ls_stop1195        return ls_params11961197    def _generate(1198        self,1199        messages: list[BaseMessage],1200        stop: list[str] | None = None,1201        run_manager: CallbackManagerForLLMRun | None = None,1202        **kwargs: Any,1203    ) -> ChatResult:1204        final_chunk = self._chat_stream_with_aggregation(1205            messages, stop, run_manager, verbose=self.verbose, **kwargs1206        )1207        generation_info = final_chunk.generation_info1208        chat_generation = ChatGeneration(1209            message=AIMessage(1210                content=final_chunk.text,1211                usage_metadata=cast(1212                    "AIMessageChunk", final_chunk.message1213                ).usage_metadata,1214                tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1215                additional_kwargs=final_chunk.message.additional_kwargs,1216            ),1217            generation_info=generation_info,1218        )1219        return ChatResult(generations=[chat_generation])12201221    def _iterate_over_stream(1222        self,1223        messages: list[BaseMessage],1224        stop: list[str] | None = None,1225        **kwargs: Any,1226    ) -> Iterator[ChatGenerationChunk]:1227        reasoning = kwargs.get("reasoning", self.reasoning)1228        for stream_resp in self._create_chat_stream(messages, stop, **kwargs):1229            if not isinstance(stream_resp, str):1230                content = (1231                    stream_resp["message"]["content"]1232                    if "message" in stream_resp and "content" in stream_resp["message"]1233                    else ""1234                )12351236                # Warn and skip responses with done_reason: 'load' and empty content1237                # These indicate the model was loaded but no actual generation occurred1238                is_load_response_with_empty_content = (1239                    stream_resp.get("done") is True1240                    and stream_resp.get("done_reason") == "load"1241                    and not content.strip()1242                )12431244                if is_load_response_with_empty_content:1245                    log.warning(1246                        "Ollama returned empty response with done_reason='load'."1247                        "This typically indicates the model was loaded but no content "1248                        "was generated. Skipping this response."1249                    )1250                    continue12511252                if stream_resp.get("done") is True:1253                    generation_info = dict(stream_resp)1254                    if "model" in generation_info:1255                        generation_info["model_name"] = generation_info["model"]1256                    generation_info["model_provider"] = "ollama"1257                    _ = generation_info.pop("message", None)1258                else:1259                    chunk_logprobs = stream_resp.get("logprobs")1260                    generation_info = (1261                        {"logprobs": chunk_logprobs}1262                        if chunk_logprobs is not None1263                        else None1264                    )12651266                additional_kwargs = {}1267                if (1268                    reasoning1269                    and "message" in stream_resp1270                    and (thinking_content := stream_resp["message"].get("thinking"))1271                ):1272                    additional_kwargs["reasoning_content"] = thinking_content12731274                chunk = ChatGenerationChunk(1275                    message=AIMessageChunk(1276                        content=content,1277                        additional_kwargs=additional_kwargs,1278                        usage_metadata=_get_usage_metadata_from_generation_info(1279                            stream_resp1280                        ),1281                        tool_calls=_get_tool_calls_from_response(stream_resp),1282                    ),1283                    generation_info=generation_info,1284                )12851286                yield chunk12871288    def _stream(1289        self,1290        messages: list[BaseMessage],1291        stop: list[str] | None = None,1292        run_manager: CallbackManagerForLLMRun | None = None,1293        **kwargs: Any,1294    ) -> Iterator[ChatGenerationChunk]:1295        for chunk in self._iterate_over_stream(messages, stop, **kwargs):1296            if run_manager:1297                run_manager.on_llm_new_token(1298                    chunk.text,1299                    verbose=self.verbose,1300                )1301            yield chunk13021303    async def _aiterate_over_stream(1304        self,1305        messages: list[BaseMessage],1306        stop: list[str] | None = None,1307        **kwargs: Any,1308    ) -> AsyncIterator[ChatGenerationChunk]:1309        reasoning = kwargs.get("reasoning", self.reasoning)1310        async for stream_resp in self._acreate_chat_stream(messages, stop, **kwargs):1311            if not isinstance(stream_resp, str):1312                content = (1313                    stream_resp["message"]["content"]1314                    if "message" in stream_resp and "content" in stream_resp["message"]1315                    else ""1316                )13171318                # Warn and skip responses with done_reason: 'load' and empty content1319                # These indicate the model was loaded but no actual generation occurred1320                is_load_response_with_empty_content = (1321                    stream_resp.get("done") is True1322                    and stream_resp.get("done_reason") == "load"1323                    and not content.strip()1324                )13251326                if is_load_response_with_empty_content:1327                    log.warning(1328                        "Ollama returned empty response with done_reason='load'. "1329                        "This typically indicates the model was loaded but no content "1330                        "was generated. Skipping this response."1331                    )1332                    continue13331334                if stream_resp.get("done") is True:1335                    generation_info = dict(stream_resp)1336                    if "model" in generation_info:1337                        generation_info["model_name"] = generation_info["model"]1338                    generation_info["model_provider"] = "ollama"1339                    _ = generation_info.pop("message", None)1340                else:1341                    chunk_logprobs = stream_resp.get("logprobs")1342                    generation_info = (1343                        {"logprobs": chunk_logprobs}1344                        if chunk_logprobs is not None1345                        else None1346                    )13471348                additional_kwargs = {}1349                if (1350                    reasoning1351                    and "message" in stream_resp1352                    and (thinking_content := stream_resp["message"].get("thinking"))1353                ):1354                    additional_kwargs["reasoning_content"] = thinking_content13551356                chunk = ChatGenerationChunk(1357                    message=AIMessageChunk(1358                        content=content,1359                        additional_kwargs=additional_kwargs,1360                        usage_metadata=_get_usage_metadata_from_generation_info(1361                            stream_resp1362                        ),1363                        tool_calls=_get_tool_calls_from_response(stream_resp),1364                    ),1365                    generation_info=generation_info,1366                )13671368                yield chunk13691370    async def _astream(1371        self,1372        messages: list[BaseMessage],1373        stop: list[str] | None = None,1374        run_manager: AsyncCallbackManagerForLLMRun | None = None,1375        **kwargs: Any,1376    ) -> AsyncIterator[ChatGenerationChunk]:1377        async for chunk in self._aiterate_over_stream(messages, stop, **kwargs):1378            if run_manager:1379                await run_manager.on_llm_new_token(1380                    chunk.text,1381                    verbose=self.verbose,1382                )1383            yield chunk13841385    async def _agenerate(1386        self,1387        messages: list[BaseMessage],1388        stop: list[str] | None = None,1389        run_manager: AsyncCallbackManagerForLLMRun | None = None,1390        **kwargs: Any,1391    ) -> ChatResult:1392        final_chunk = await self._achat_stream_with_aggregation(1393            messages, stop, run_manager, verbose=self.verbose, **kwargs1394        )1395        generation_info = final_chunk.generation_info1396        chat_generation = ChatGeneration(1397            message=AIMessage(1398                content=final_chunk.text,1399                usage_metadata=cast(1400                    "AIMessageChunk", final_chunk.message1401                ).usage_metadata,1402                tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls,1403                additional_kwargs=final_chunk.message.additional_kwargs,1404            ),1405            generation_info=generation_info,1406        )1407        return ChatResult(generations=[chat_generation])14081409    @property1410    def _llm_type(self) -> str:1411        """Return type of chat model."""1412        return "chat-ollama"14131414    def bind_tools(1415        self,1416        tools: Sequence[dict[str, Any] | type | Callable | BaseTool],1417        *,1418        tool_choice: dict | str | Literal["auto", "any"] | bool | None = None,  # noqa: PYI051, ARG0021419        **kwargs: Any,1420    ) -> Runnable[LanguageModelInput, AIMessage]:1421        """Bind tool-like objects to this chat model.14221423        Assumes model is compatible with OpenAI tool-calling API.14241425        Args:1426            tools: A list of tool definitions to bind to this chat model.14271428                Supports any tool definition handled by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1429            tool_choice: If provided, which tool for model to call. **This parameter1430                is currently ignored as it is not supported by Ollama.**1431            kwargs: Any additional parameters are passed directly to1432                `self.bind(**kwargs)`.1433        """  # noqa: E5011434        formatted_tools = [convert_to_openai_tool(tool) for tool in tools]1435        return super().bind(tools=formatted_tools, **kwargs)14361437    def with_structured_output(1438        self,1439        schema: dict | type,1440        *,1441        method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",1442        include_raw: bool = False,1443        **kwargs: Any,1444    ) -> Runnable[LanguageModelInput, dict | BaseModel]:1445        r"""Model wrapper that returns outputs formatted to match the given schema.14461447        Args:1448            schema: The output schema. Can be passed in as:14491450                - An OpenAI function/tool schema.1451                - A JSON Schema,1452                - A `TypedDict` class,1453                - Or a Pydantic class.14541455                If `schema` is a Pydantic class then the model output will be a1456                Pydantic instance of that class, and the model-generated fields will be1457                validated by the Pydantic class. Otherwise the model output will be a1458                dict and will not be validated.14591460                See `langchain_core.utils.function_calling.convert_to_openai_tool` for1461                more on how to properly specify types and descriptions of schema fields1462                when specifying a Pydantic or `TypedDict` class.14631464            method: The method for steering model generation, one of:14651466                - `'json_schema'`:1467                    Uses Ollama's [structured output API](https://ollama.com/blog/structured-outputs)1468                - `'function_calling'`:1469                    Uses Ollama's tool-calling API1470                - `'json_mode'`:1471                    Specifies `format='json'`. Note that if using JSON mode then you1472                    must include instructions for formatting the output into the1473                    desired schema into the model call.14741475            include_raw:1476                If `False` then only the parsed structured output is returned.14771478                If an error occurs during model output parsing it will be raised.14791480                If `True` then both the raw model response (a `BaseMessage`) and the1481                parsed model response will be returned.14821483                If an error occurs during output parsing it will be caught and returned1484                as well.14851486                The final output is always a `dict` with keys `'raw'`, `'parsed'`, and1487                `'parsing_error'`.14881489            kwargs: Additional keyword args aren't supported.14901491        Returns:1492            A `Runnable` that takes same inputs as a1493                `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is1494                `False` and `schema` is a Pydantic class, `Runnable` outputs an instance1495                of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is1496                `False` then `Runnable` outputs a `dict`.14971498                If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:14991500                - `'raw'`: `BaseMessage`1501                - `'parsed'`: `None` if there was a parsing error, otherwise the type1502                    depends on the `schema` as described above.1503                - `'parsing_error'`: `BaseException | None`15041505        !!! warning "Behavior changed in `langchain-ollama` 0.2.2"15061507            Added support for structured output API via `format` parameter.15081509        !!! warning "Behavior changed in `langchain-ollama` 0.3.0"15101511            Updated default `method` to `'json_schema'`.15121513        ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=False`"15141515            ```python1516            from typing import Optional15171518            from langchain_ollama import ChatOllama1519            from pydantic import BaseModel, Field152015211522            class AnswerWithJustification(BaseModel):1523                '''An answer to the user question along with justification for the answer.'''15241525                answer: str1526                justification: str | None = Field(1527                    default=...,1528                    description="A justification for the answer.",1529                )153015311532            model = ChatOllama(model="llama3.1", temperature=0)1533            structured_model = model.with_structured_output(AnswerWithJustification)15341535            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")15361537            # -> AnswerWithJustification(1538            #     answer='They weigh the same',1539            #     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.'1540            # )1541            ```15421543        ??? note "Example: `schema=Pydantic` class, `method='json_schema'`, `include_raw=True`"15441545            ```python1546            from langchain_ollama import ChatOllama1547            from pydantic import BaseModel154815491550            class AnswerWithJustification(BaseModel):1551                '''An answer to the user question along with justification for the answer.'''15521553                answer: str1554                justification: str155515561557            model = ChatOllama(model="llama3.1", temperature=0)1558            structured_model = model.with_structured_output(1559                AnswerWithJustification,1560                include_raw=True,1561            )15621563            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1564            # -> {1565            #     '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'}]}),1566            #     '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.'),1567            #     'parsing_error': None1568            # }1569            ```15701571        ??? note "Example: `schema=Pydantic` class, `method='function_calling'`, `include_raw=False`"15721573            ```python1574            from typing import Optional15751576            from langchain_ollama import ChatOllama1577            from pydantic import BaseModel, Field157815791580            class AnswerWithJustification(BaseModel):1581                '''An answer to the user question along with justification for the answer.'''15821583                answer: str1584                justification: str | None = Field(1585                    default=...,1586                    description="A justification for the answer.",1587                )158815891590            model = ChatOllama(model="llama3.1", temperature=0)1591            structured_model = model.with_structured_output(1592                AnswerWithJustification,1593                method="function_calling",1594            )15951596            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")15971598            # -> AnswerWithJustification(1599            #     answer='They weigh the same',1600            #     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.'1601            # )1602            ```16031604        ??? note "Example: `schema=TypedDict` class, `method='function_calling'`, `include_raw=False`"16051606            ```python1607            from typing_extensions import Annotated, TypedDict16081609            from langchain_ollama import ChatOllama161016111612            class AnswerWithJustification(TypedDict):1613                '''An answer to the user question along with justification for the answer.'''16141615                answer: str1616                justification: Annotated[str | None, None, "A justification for the answer."]161716181619            model = ChatOllama(model="llama3.1", temperature=0)1620            structured_model = model.with_structured_output(AnswerWithJustification)16211622            structured_model.invoke("What weighs more a pound of bricks or a pound of feathers")1623            # -> {1624            #     'answer': 'They weigh the same',1625            #     '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.'1626            # }1627            ```16281629        ??? note "Example: `schema=OpenAI` function schema, `method='function_calling'`, `include_raw=False`"16301631            ```python1632            from langchain_ollama import ChatOllama16331634            oai_schema = {1635                'name': 'AnswerWithJustification',1636                'description': 'An answer to the user question along with justification for the answer.',1637                'parameters': {1638                    'type': 'object',1639                    'properties': {1640                        'answer': {'type': 'string'},1641                        'justification': {'description': 'A justification for the answer.', 'type': 'string'}1642                    },1643                    'required': ['answer']1644                }16451646                model = ChatOllama(model="llama3.1", temperature=0)1647                structured_model = model.with_structured_output(oai_schema)16481649                structured_model.invoke(1650                    "What weighs more a pound of bricks or a pound of feathers"1651                )1652                # -> {1653                #     'answer': 'They weigh the same',1654                #     '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.'1655                # }1656            ```16571658        ??? note "Example: `schema=Pydantic` class, `method='json_mode'`, `include_raw=True`"16591660            ```python1661            from langchain_ollama import ChatOllama1662            from pydantic import BaseModel166316641665            class AnswerWithJustification(BaseModel):1666                answer: str1667                justification: str166816691670            model = ChatOllama(model="llama3.1", temperature=0)1671            structured_model = model.with_structured_output(1672                AnswerWithJustification, method="json_mode", include_raw=True1673            )16741675            structured_model.invoke(1676                "Answer the following question. "1677                "Make sure to return a JSON blob with keys 'answer' and 'justification'.\\n\\n"1678                "What's heavier a pound of bricks or a pound of feathers?"1679            )1680            # -> {1681            #     '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}'),1682            #     '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.'),1683            #     'parsing_error': None1684            # }1685            ```16861687        """  # noqa: E5011688        _ = kwargs.pop("strict", None)1689        if kwargs:1690            msg = f"Received unsupported arguments {kwargs}"1691            raise ValueError(msg)1692        is_pydantic_schema = _is_pydantic_class(schema)1693        if method == "function_calling":1694            if schema is None:1695                msg = (1696                    "schema must be specified when method is not 'json_mode'. "1697                    "Received None."1698                )1699                raise ValueError(msg)1700            formatted_tool = convert_to_openai_tool(schema)1701            tool_name = formatted_tool["function"]["name"]1702            llm = self.bind_tools(1703                [schema],1704                tool_choice=tool_name,1705                ls_structured_output_format={1706                    "kwargs": {"method": method},1707                    "schema": formatted_tool,1708                },1709            )1710            if is_pydantic_schema:1711                output_parser: Runnable = PydanticToolsParser(1712                    tools=[schema],  # ty: ignore[invalid-argument-type]1713                    first_tool_only=True,1714                )1715            else:1716                output_parser = JsonOutputKeyToolsParser(1717                    key_name=tool_name, first_tool_only=True1718                )1719        elif method == "json_mode":1720            llm = self.bind(1721                format="json",1722                ls_structured_output_format={1723                    "kwargs": {"method": method},1724                    "schema": schema,1725                },1726            )1727            output_parser = (1728                PydanticOutputParser(pydantic_object=schema)  # ty: ignore[invalid-argument-type]1729                if is_pydantic_schema1730                else JsonOutputParser()1731            )1732        elif method == "json_schema":1733            if schema is None:1734                msg = (1735                    "schema must be specified when method is not 'json_mode'. "1736                    "Received None."1737                )1738                raise ValueError(msg)1739            if is_pydantic_schema:1740                schema = cast("TypeBaseModel", schema)1741                if issubclass(schema, BaseModelV1):1742                    response_format = schema.schema()1743                else:1744                    response_format = schema.model_json_schema()1745                llm = self.bind(1746                    format=response_format,1747                    ls_structured_output_format={1748                        "kwargs": {"method": method},1749                        "schema": schema,1750                    },1751                )1752                output_parser = PydanticOutputParser(pydantic_object=schema)1753            else:1754                if is_typeddict(schema):1755                    response_format = convert_to_json_schema(schema)1756                    if "required" not in response_format:1757                        response_format["required"] = list(1758                            response_format["properties"].keys()1759                        )1760                else:1761                    # is JSON schema1762                    response_format = cast("dict", schema)1763                llm = self.bind(1764                    format=response_format,1765                    ls_structured_output_format={1766                        "kwargs": {"method": method},1767                        "schema": response_format,1768                    },1769                )1770                output_parser = JsonOutputParser()1771        else:1772            msg = (1773                f"Unrecognized method argument. Expected one of 'function_calling', "1774                f"'json_schema', or 'json_mode'. Received: '{method}'"1775            )1776            raise ValueError(msg)17771778        if include_raw:1779            parser_assign = RunnablePassthrough.assign(1780                parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None1781            )1782            parser_none = RunnablePassthrough.assign(parsed=lambda _: None)1783            parser_with_fallback = parser_assign.with_fallbacks(1784                [parser_none], exception_key="parsing_error"1785            )1786            return RunnableMap(raw=llm) | parser_with_fallback1787        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.