libs/partners/fireworks/langchain_fireworks/chat_models.py PYTHON 1,584 lines View on github.com → Search inside
1"""Fireworks chat wrapper."""23from __future__ import annotations45import contextlib6import json7import logging8from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence9from operator import itemgetter10from typing import (11    Any,12    Literal,13    NoReturn,14    cast,15)1617import httpx18from fireworks import (19    APIConnectionError,20    AsyncFireworks,21    BadRequestError,22    Fireworks,23    FireworksError,24    InternalServerError,25    RateLimitError,26)27from langchain_core.callbacks import (28    AsyncCallbackManagerForLLMRun,29    CallbackManagerForLLMRun,30)31from langchain_core.exceptions import ContextOverflowError32from langchain_core.language_models import (33    LanguageModelInput,34    ModelProfile,35    ModelProfileRegistry,36)37from langchain_core.language_models.chat_models import (38    BaseChatModel,39    LangSmithParams,40    agenerate_from_stream,41    generate_from_stream,42)43from langchain_core.language_models.llms import create_base_retry_decorator44from langchain_core.messages import (45    AIMessage,46    AIMessageChunk,47    BaseMessage,48    BaseMessageChunk,49    ChatMessage,50    ChatMessageChunk,51    FunctionMessage,52    FunctionMessageChunk,53    HumanMessage,54    HumanMessageChunk,55    InvalidToolCall,56    SystemMessage,57    SystemMessageChunk,58    ToolCall,59    ToolMessage,60    ToolMessageChunk,61    is_data_content_block,62)63from langchain_core.messages.block_translators.openai import (64    convert_to_openai_data_block,65)66from langchain_core.messages.tool import (67    ToolCallChunk,68)69from langchain_core.messages.tool import (70    tool_call_chunk as create_tool_call_chunk,71)72from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser73from langchain_core.output_parsers.base import OutputParserLike74from langchain_core.output_parsers.openai_tools import (75    JsonOutputKeyToolsParser,76    PydanticToolsParser,77    make_invalid_tool_call,78    parse_tool_call,79)80from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult81from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough82from langchain_core.tools import BaseTool83from langchain_core.utils import (84    get_pydantic_field_names,85)86from langchain_core.utils.function_calling import (87    convert_to_json_schema,88    convert_to_openai_tool,89)90from langchain_core.utils.pydantic import is_basemodel_subclass91from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_from_env92from pydantic import (93    BaseModel,94    ConfigDict,95    Field,96    PrivateAttr,97    SecretStr,98    model_validator,99)100from typing_extensions import Self101102from langchain_fireworks._compat import _convert_from_v1_to_chat_completions103from langchain_fireworks.data._profiles import _PROFILES104105logger = logging.getLogger(__name__)106107108_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)109110111def _get_default_model_profile(model_name: str) -> ModelProfile:112    default = _MODEL_PROFILES.get(model_name) or {}113    return default.copy()114115116def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:117    """Convert a dictionary to a LangChain message.118119    Args:120        _dict: The dictionary.121122    Returns:123        The LangChain message.124125    """126    role = _dict.get("role")127    if role == "user":128        return HumanMessage(content=_dict.get("content", ""))129    if role == "assistant":130        # Fix for azure131        # Also Fireworks returns None for tool invocations132        content = _dict.get("content", "") or ""133        additional_kwargs: dict = {}134        if reasoning_content := _dict.get("reasoning_content"):135            additional_kwargs["reasoning_content"] = reasoning_content136137        if function_call := _dict.get("function_call"):138            additional_kwargs["function_call"] = dict(function_call)139140        tool_calls = []141        invalid_tool_calls = []142        if raw_tool_calls := _dict.get("tool_calls"):143            additional_kwargs["tool_calls"] = raw_tool_calls144            for raw_tool_call in raw_tool_calls:145                try:146                    tool_calls.append(parse_tool_call(raw_tool_call, return_id=True))147                except Exception as e:148                    invalid_tool_calls.append(149                        dict(make_invalid_tool_call(raw_tool_call, str(e)))150                    )151        return AIMessage(152            content=content,153            additional_kwargs=additional_kwargs,154            tool_calls=tool_calls,155            invalid_tool_calls=invalid_tool_calls,156        )157    if role == "system":158        return SystemMessage(content=_dict.get("content", ""))159    if role == "function":160        return FunctionMessage(161            content=_dict.get("content", ""), name=_dict.get("name", "")162        )163    if role == "tool":164        additional_kwargs = {}165        if "name" in _dict:166            additional_kwargs["name"] = _dict["name"]167        return ToolMessage(168            content=_dict.get("content", ""),169            tool_call_id=_dict.get("tool_call_id", ""),170            additional_kwargs=additional_kwargs,171        )172    return ChatMessage(content=_dict.get("content", ""), role=role or "")173174175def _allowed_content_part_keys() -> frozenset[str]:176    """Allowlist of wire-valid keys on a Fireworks content part.177178    Derived at import time from the stainless-generated TypedDict so the179    allowlist tracks the upstream OpenAPI spec as `fireworks-ai` is bumped:180    new fields widen the allowlist for free, removed/renamed fields shrink it181    in lockstep. If the SDK reshuffles its module layout the import falls back182    to a conservative hand-coded set and emits a warning, and the layout test183    (`test_fireworks_sdk_request_layout_stable`) fails to surface the drift.184    """185    try:186        from typing import get_type_hints187188        from fireworks.types.shared_params.chat_message import (189            ContentUnionMember1,190        )191192        return frozenset(get_type_hints(ContentUnionMember1))193    except ImportError:194        logger.warning(195            "Could not import `fireworks.types.shared_params.chat_message."196            "ContentUnionMember1`; falling back to a conservative content-part "197            "key allowlist. Bump `fireworks-ai` or update "198            "`_allowed_content_part_keys` if the SDK has moved this type.",199        )200        return frozenset({"type", "text", "image_url", "video_url"})201202203_ALLOWED_CONTENT_PART_KEYS: frozenset[str] = _allowed_content_part_keys()204205206def _sanitize_chat_completions_content(content: Any) -> Any:207    """Strip non-wire keys from content blocks before serializing to Fireworks.208209    Fireworks's chat completions endpoint rejects unknown fields on message210    content parts with `Extra inputs are not permitted, field: 'messages[N]211    .content.list[ChatMessageContent][i].<key>'`. This surfaces when a212    conversation accumulates AIMessages from a different provider (e.g.213    Anthropic's v1 streaming-reassembly `index` marker on text blocks, or the214    LangChain-internal `caller` key on `tool_use` blocks) and that history is215    later forwarded to a Fireworks-hosted model.216217    For list content:218        - each block dict is filtered down to keys in219            `_ALLOWED_CONTENT_PART_KEYS` (sourced from the SDK TypedDict, so it220            stays in sync with the upstream spec).221        - if the result is a list of exactly one block that, post-strip, is222            `{"type": "text", "text": <str>}` and nothing else, it is coerced to223            a plain string. Fireworks's `content` union lists `str` first224            (`Input should be a valid string, field: 'messages[N].content.str'`),225            and the stricter shape avoids the union-validation noise on the226            server side.227    Non-list content (strings, None) passes through unchanged.228    """229    if not isinstance(content, list):230        return content231    sanitized: list[Any] = []232    for block in content:233        if isinstance(block, dict):234            sanitized.append(235                {k: v for k, v in block.items() if k in _ALLOWED_CONTENT_PART_KEYS}236            )237        else:238            sanitized.append(block)239    if (240        len(sanitized) == 1241        and isinstance(sanitized[0], dict)242        and set(sanitized[0]) == {"type", "text"}243        and sanitized[0]["type"] == "text"244        and isinstance(sanitized[0]["text"], str)245    ):246        return sanitized[0]["text"]247    return sanitized248249250def _format_message_content(content: Any) -> Any:251    """Format message content for the Fireworks chat completions wire format.252253    Adapted from `langchain_openai.chat_models.base._format_message_content`,254    scoped to the chat completions API: drops content block types the wire255    format does not carry, translates canonical v0/v1 multimodal data blocks256    via `convert_to_openai_data_block(block, api="chat/completions")`, and257    converts legacy Anthropic-shape image blocks (`{"type": "image",258    "source": {...}}`) to OpenAI `image_url` blocks. String and non-list259    content are returned unchanged.260261    Args:262        content: The message content. Strings and non-list values are263            returned as-is; lists are walked block by block.264265    Returns:266        The formatted content, ready to be placed on the chat completions267        wire. List inputs return a new list with translations applied; other268        inputs are returned unchanged.269    """270    if not isinstance(content, list):271        return content272    formatted: list[Any] = []273    for block in content:274        if isinstance(block, dict) and "type" in block:275            btype = block["type"]276            if btype in (277                "tool_use",278                "thinking",279                "reasoning_content",280                "function_call",281                "code_interpreter_call",282            ):283                continue284            if is_data_content_block(block):285                formatted.append(286                    convert_to_openai_data_block(block, api="chat/completions")287                )288                continue289            if (290                btype == "image"291                and (source := block.get("source"))292                and isinstance(source, dict)293            ):294                if (295                    source.get("type") == "base64"296                    and (media_type := source.get("media_type"))297                    and (data := source.get("data"))298                ):299                    formatted.append(300                        {301                            "type": "image_url",302                            "image_url": {"url": f"data:{media_type};base64,{data}"},303                        }304                    )305                    continue306                if source.get("type") == "url" and (url := source.get("url")):307                    formatted.append({"type": "image_url", "image_url": {"url": url}})308                    continue309                continue310        formatted.append(block)311    return formatted312313314def _convert_message_to_dict(message: BaseMessage) -> dict:315    """Convert a LangChain message to a dictionary.316317    Args:318        message: The LangChain message.319320    Returns:321        The dictionary.322323    """324    message_dict: dict[str, Any]325    if isinstance(message, ChatMessage):326        message_dict = {327            "role": message.role,328            "content": _sanitize_chat_completions_content(329                _format_message_content(message.content)330            ),331        }332    elif isinstance(message, HumanMessage):333        message_dict = {334            "role": "user",335            "content": _sanitize_chat_completions_content(336                _format_message_content(message.content)337            ),338        }339    elif isinstance(message, AIMessage):340        # Translate v1 content341        if message.response_metadata.get("output_version") == "v1":342            message = _convert_from_v1_to_chat_completions(message)343        message_dict = {344            "role": "assistant",345            "content": _sanitize_chat_completions_content(346                _format_message_content(message.content)347            ),348        }349        if "function_call" in message.additional_kwargs:350            message_dict["function_call"] = message.additional_kwargs["function_call"]351            # If function call only, content is None not empty string352            if message_dict["content"] == "":353                message_dict["content"] = None354        if message.tool_calls or message.invalid_tool_calls:355            message_dict["tool_calls"] = [356                _lc_tool_call_to_fireworks_tool_call(tc) for tc in message.tool_calls357            ] + [358                _lc_invalid_tool_call_to_fireworks_tool_call(tc)359                for tc in message.invalid_tool_calls360            ]361        elif "tool_calls" in message.additional_kwargs:362            message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]363        # If tool calls only, content is None not empty string364        if "tool_calls" in message_dict and message_dict["content"] == "":365            message_dict["content"] = None366        else:367            pass368    elif isinstance(message, SystemMessage):369        message_dict = {370            "role": "system",371            "content": _sanitize_chat_completions_content(372                _format_message_content(message.content)373            ),374        }375    elif isinstance(message, FunctionMessage):376        message_dict = {377            "role": "function",378            "content": message.content,379            "name": message.name,380        }381    elif isinstance(message, ToolMessage):382        message_dict = {383            "role": "tool",384            "content": _sanitize_chat_completions_content(385                _format_message_content(message.content)386            ),387            "tool_call_id": message.tool_call_id,388        }389    else:390        msg = f"Got unknown type {message}"391        raise TypeError(msg)392    if "name" in message.additional_kwargs:393        message_dict["name"] = message.additional_kwargs["name"]394    return message_dict395396397def _usage_to_metadata(usage: Mapping[str, Any]) -> dict[str, int]:398    input_tokens = usage.get("prompt_tokens", 0)399    output_tokens = usage.get("completion_tokens", 0)400    return {401        "input_tokens": input_tokens,402        "output_tokens": output_tokens,403        "total_tokens": usage.get("total_tokens", input_tokens + output_tokens),404    }405406407def _convert_chunk_to_message_chunk(408    chunk: Mapping[str, Any], default_class: type[BaseMessageChunk]409) -> BaseMessageChunk:410    choices = chunk.get("choices") or []411    response_metadata: dict[str, Any] = {"model_provider": "fireworks"}412    if service_tier := chunk.get("service_tier"):413        response_metadata["service_tier"] = service_tier414    if not choices:415        # Final chunk emitted when `stream_options.include_usage=True`:416        # `choices` is empty and the chunk carries only `usage`.417        usage = chunk.get("usage")418        if not usage:419            logger.debug(420                "Received stream chunk with no choices and no usage: %s", chunk421            )422        usage_metadata = _usage_to_metadata(usage) if usage else None423        return AIMessageChunk(424            content="",425            usage_metadata=usage_metadata,  # type: ignore[arg-type]426            response_metadata=response_metadata,427        )428    choice = choices[0]429    _dict = choice["delta"]430    role = cast(str, _dict.get("role"))431    content = cast(str, _dict.get("content") or "")432    additional_kwargs: dict = {}433    tool_call_chunks: list[ToolCallChunk] = []434    if _dict.get("function_call"):435        function_call = dict(_dict["function_call"])436        if "name" in function_call and function_call["name"] is None:437            function_call["name"] = ""438        additional_kwargs["function_call"] = function_call439    if raw_tool_calls := _dict.get("tool_calls"):440        additional_kwargs["tool_calls"] = raw_tool_calls441        for rtc in raw_tool_calls:442            with contextlib.suppress(KeyError):443                tool_call_chunks.append(444                    create_tool_call_chunk(445                        name=rtc["function"].get("name"),446                        args=rtc["function"].get("arguments"),447                        id=rtc.get("id"),448                        index=rtc.get("index"),449                    )450                )451    if role == "user" or default_class == HumanMessageChunk:452        return HumanMessageChunk(content=content)453    if role == "assistant" or default_class == AIMessageChunk:454        usage = chunk.get("usage")455        usage_metadata = _usage_to_metadata(usage) if usage else None456        return AIMessageChunk(457            content=content,458            additional_kwargs=additional_kwargs,459            tool_call_chunks=tool_call_chunks,460            usage_metadata=usage_metadata,  # type: ignore[arg-type]461            response_metadata=response_metadata,462        )463    if role == "system" or default_class == SystemMessageChunk:464        return SystemMessageChunk(content=content)465    if role == "function" or default_class == FunctionMessageChunk:466        return FunctionMessageChunk(content=content, name=_dict["name"])467    if role == "tool" or default_class == ToolMessageChunk:468        return ToolMessageChunk(content=content, tool_call_id=_dict["tool_call_id"])469    if role or default_class == ChatMessageChunk:470        return ChatMessageChunk(content=content, role=role)471    return default_class(content=content)  # type: ignore[call-arg]472473474class _RetryableHTTPStatusError(FireworksError):475    """Internal marker for 5xx `httpx.HTTPStatusError` responses.476477    The 1.x SDK wraps every status response into a typed `APIStatusError`478    subclass, so this path is defense-in-depth: it only fires when a raw479    `httpx.HTTPStatusError` escapes the SDK (e.g., a custom `http_client` or480    monkey-patched transport raises one directly). Promoting it here keeps the481    retryable set expressible as a list of classes for482    `create_base_retry_decorator`.483    """484485486_RETRYABLE_ERRORS: tuple[type[BaseException], ...] = (487    APIConnectionError,488    InternalServerError,489    RateLimitError,490    httpx.TimeoutException,491    httpx.TransportError,492    _RetryableHTTPStatusError,493)494495496def _promote_http_status_error(exc: httpx.HTTPStatusError) -> NoReturn:497    """Re-raise 5xx `httpx.HTTPStatusError` as a retryable marker."""498    if exc.response.status_code >= 500:499        msg = f"Retryable {exc.response.status_code} from Fireworks: {exc}"500        raise _RetryableHTTPStatusError(msg) from exc501    raise exc502503504class FireworksContextOverflowError(BadRequestError, ContextOverflowError):505    """`BadRequestError` raised when input exceeds Fireworks's context limit."""506507508def _handle_fireworks_invalid_request(e: BadRequestError) -> NoReturn:509    """Promote prompt-too-long errors to `FireworksContextOverflowError`."""510    if "prompt is too long" in str(e):511        raise FireworksContextOverflowError(512            str(e), response=e.response, body=e.body513        ) from e514    raise e515516517def _raise_empty_stream() -> NoReturn:518    """Raise a descriptive error when the SDK returns a zero-chunk stream."""519    msg = "Received empty stream from Fireworks"520    raise FireworksError(msg)521522523def _create_retry_decorator(524    llm: ChatFireworks,525    run_manager: AsyncCallbackManagerForLLMRun | CallbackManagerForLLMRun | None = None,526) -> Callable[[Any], Any]:527    """Return a tenacity retry decorator for Fireworks SDK calls.528529    Retries live here rather than in the SDK so each attempt is visible to the530    LangChain `run_manager.on_retry` callback. The SDK's own retry layer is531    suppressed via `max_retries=0` on the client; see `validate_environment`.532    """533    # `max_retries` counts retries *after* the initial attempt (default lives on534    # the `ChatFireworks.max_retries` field). `create_base_retry_decorator`535    # forwards its `max_retries` to `stop_after_attempt`, which counts total536    # attempts  so offset by 1. `None` and `0` both mean "single attempt, no537    # retries".538    attempts = (llm.max_retries + 1) if llm.max_retries else 1539    return create_base_retry_decorator(540        error_types=list(_RETRYABLE_ERRORS),541        max_retries=attempts,542        run_manager=run_manager,543    )544545546def _prepare_sdk_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:547    """Move fields the 1.x SDK does not model into `extra_body`.548549    The Stainless-generated `chat.completions.create` signature has a fixed set550    of typed parameters. Fireworks accepts additional fields on the wire (notably551    `stream_options.include_usage`) that the SDK schema does not declare. The552    SDK exposes `extra_body` precisely for this  merge anything that looks553    extra-body-shaped into it so it lands in the JSON request body.554555    If a caller supplies both `extra_body={"stream_options": ...}` and a556    top-level `stream_options=...`, the value already in `extra_body` wins557    (callers using `extra_body` are presumed to want explicit control); the558    discarded top-level value is logged.559    """560    extra_body = dict(kwargs.pop("extra_body", None) or {})561    top_level_stream_options = kwargs.pop("stream_options", None)562    if top_level_stream_options is not None:563        if "stream_options" in extra_body:564            logger.warning(565                "Both `extra_body['stream_options']` and a top-level "566                "`stream_options` were supplied; using `extra_body`'s value "567                "and discarding the top-level value.",568            )569        else:570            extra_body["stream_options"] = top_level_stream_options571    if extra_body:572        kwargs["extra_body"] = extra_body573    return kwargs574575576def _completion_with_retry(577    llm: ChatFireworks,578    run_manager: CallbackManagerForLLMRun | None = None,579    **kwargs: Any,580) -> Any:581    """Retry the sync completion call, including stream setup."""582    retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)583    kwargs = _prepare_sdk_kwargs(kwargs)584585    @retry_decorator586    def _call() -> Any:587        try:588            result = llm.client.create(**kwargs)589        except httpx.HTTPStatusError as e:590            _promote_http_status_error(e)591        if kwargs.get("stream"):592            # The streaming generator is lazy  advance once so the HTTP593            # connection and any transport error happen inside the retry594            # boundary. `_prepend_chunk` then re-yields the consumed chunk595            # ahead of the rest so callers still see every event.596            try:597                iterator = iter(result)598                first = next(iterator)599            except StopIteration:600                _raise_empty_stream()601            except httpx.HTTPStatusError as e:602                _promote_http_status_error(e)603            return _prepend_chunk(first, iterator)604        return result605606    return _call()607608609async def _acompletion_with_retry(610    llm: ChatFireworks,611    run_manager: AsyncCallbackManagerForLLMRun | None = None,612    **kwargs: Any,613) -> Any:614    """Retry the async completion call, including stream setup."""615    retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)616    kwargs = _prepare_sdk_kwargs(kwargs)617618    @retry_decorator619    async def _call() -> Any:620        if kwargs.get("stream"):621            try:622                # 1.x async `create()` is a coroutine that resolves to an623                # `AsyncStream` when `stream=True`. Await it, then advance the624                # async iterator once inside the retry boundary so transport625                # errors surface here rather than at first downstream consumer.626                result = await llm.async_client.create(**kwargs)627                agen = result.__aiter__()628                first = await agen.__anext__()629            except StopAsyncIteration:630                _raise_empty_stream()631            except httpx.HTTPStatusError as e:632                _promote_http_status_error(e)633            return _aprepend_chunk(first, agen)634        try:635            return await llm.async_client.create(**kwargs)636        except httpx.HTTPStatusError as e:637            _promote_http_status_error(e)638639    return await _call()640641642def _prepend_chunk(first: Any, rest: Iterator[Any]) -> Iterator[Any]:643    yield first644    yield from rest645646647async def _aprepend_chunk(first: Any, rest: AsyncIterator[Any]) -> AsyncIterator[Any]:648    yield first649    async for item in rest:650        yield item651652653class ChatFireworks(BaseChatModel):654    """`Fireworks` Chat large language models API.655656    To use, you should have the657    environment variable `FIREWORKS_API_KEY` set with your API key.658659    Any parameters that are valid to be passed to the fireworks.create call660    can be passed in, even if not explicitly saved on this class.661662    Example:663        ```python664        from langchain_fireworks.chat_models import ChatFireworks665666        fireworks = ChatFireworks(model_name="accounts/fireworks/models/gpt-oss-120b")667        ```668    """669670    @property671    def lc_secrets(self) -> dict[str, str]:672        return {"fireworks_api_key": "FIREWORKS_API_KEY"}673674    @classmethod675    def get_lc_namespace(cls) -> list[str]:676        """Get the namespace of the LangChain object.677678        Returns:679            `["langchain", "chat_models", "fireworks"]`680        """681        return ["langchain", "chat_models", "fireworks"]682683    @property684    def lc_attributes(self) -> dict[str, Any]:685        attributes: dict[str, Any] = {}686        if self.fireworks_api_base:687            attributes["fireworks_api_base"] = self.fireworks_api_base688689        return attributes690691    @classmethod692    def is_lc_serializable(cls) -> bool:693        """Return whether this model can be serialized by LangChain."""694        return True695696    client: Any = Field(default=None, exclude=True)697    """Internal `fireworks.Fireworks().chat.completions` resource.698699    Constructed with `max_retries=0` so retries are owned by700    `_create_retry_decorator` (which surfaces each attempt to the LangChain701    `run_manager`). Callers reaching for this directly should set their own702    retry layer.703    """704705    async_client: Any = Field(default=None, exclude=True)706    """Internal `fireworks.AsyncFireworks().chat.completions` resource.707708    Constructed with `max_retries=0`; see `client`.709    """710711    _sdk_client: Any = PrivateAttr(default=None)712    """Owning `fireworks.Fireworks` instance, retained so `close()` can call713    into the underlying HTTPX client. The 1.x SDK does not expose lifecycle714    methods on the `chat.completions` resource itself.715    """716717    _async_sdk_client: Any = PrivateAttr(default=None)718    """Owning `fireworks.AsyncFireworks` instance; see `_sdk_client`."""719720    model_name: str = Field(alias="model")721    """Model name to use."""722723    @property724    def model(self) -> str:725        """Same as model_name."""726        return self.model_name727728    temperature: float | None = None729    """What sampling temperature to use."""730731    stop: str | list[str] | None = Field(default=None, alias="stop_sequences")732    """Default stop sequences."""733734    model_kwargs: dict[str, Any] = Field(default_factory=dict)735    """Holds any model parameters valid for `create` call not explicitly specified."""736737    fireworks_api_key: SecretStr = Field(738        alias="api_key",739        default_factory=secret_from_env(740            "FIREWORKS_API_KEY",741            error_message=(742                "You must specify an api key. "743                "You can pass it an argument as `api_key=...` or "744                "set the environment variable `FIREWORKS_API_KEY`."745            ),746        ),747    )748    """Fireworks API key.749750    Automatically read from env variable `FIREWORKS_API_KEY` if not provided.751    """752753    fireworks_api_base: str | None = Field(754        alias="base_url", default_factory=from_env("FIREWORKS_API_BASE", default=None)755    )756    """Base URL path for API requests, leave blank if not using a proxy or service757    emulator.758    """759760    request_timeout: float | tuple[float, float] | Any | None = Field(761        default=None, alias="timeout"762    )763    """Timeout for requests to Fireworks completion API. Can be `float`,764    `httpx.Timeout` or `None`.765    """766767    streaming: bool = False768    """Whether to stream the results or not."""769770    stream_usage: bool = True771    """Whether to include usage metadata in streaming output.772773    If `True`, a final empty-content chunk carrying `usage_metadata` is emitted774    during the stream. Set to `False` if the upstream model/proxy rejects775    `stream_options`, or pass `stream_options` explicitly via `model_kwargs` or776    a runtime kwarg to override.777778    !!! version-added "Added in `langchain-fireworks` 1.2.0"779780    !!! warning "Behavior changed in `langchain-fireworks` 1.2.0"781782        Streaming now opts into `stream_options.include_usage` by default, and783        the final empty-`choices` chunk is surfaced as an `AIMessageChunk` with784        `usage_metadata` instead of being silently dropped.785    """786787    n: int = 1788    """Number of chat completions to generate for each prompt."""789790    max_tokens: int | None = None791    """Maximum number of tokens to generate."""792793    max_retries: int | None = 2794    """Maximum number of retries after the initial attempt when generating.795796    Retries use exponential backoff and trigger on transient errors:797    `RateLimitError`, `APIConnectionError` (including its `APITimeoutError`798    subclass), 5xx responses (including those that surface as799    `httpx.HTTPStatusError` rather than typed SDK errors), and underlying800    transport errors (`httpx.TimeoutException`, `httpx.TransportError`).801    A value of `None` or `0` disables retries.802    """803804    service_tier: str | None = None805    """Service tier for the request.806807    Forwarded as the `service_tier` field on the Fireworks chat completions808    request when set. Pass `'priority'` to opt into Fireworks' priority tier;809    leave as `None` to use the default tier.810811    To use Fireworks' fast mode instead, select a fast-routed `model`; fast mode812    is not controlled by this field. See Fireworks'813    [serverless product docs](https://docs.fireworks.ai/guides/serverless-products)814    for the current list of fast routers and tiers.815816    !!! version-added "Added in `langchain-fireworks` 1.3.0"817    """818819    model_config = ConfigDict(820        populate_by_name=True,821    )822823    @model_validator(mode="before")824    @classmethod825    def build_extra(cls, values: dict[str, Any]) -> Any:826        """Build extra kwargs from additional params that were passed in."""827        all_required_field_names = get_pydantic_field_names(cls)828        return _build_model_kwargs(values, all_required_field_names)829830    @model_validator(mode="after")831    def validate_environment(self) -> Self:832        """Validate that api key and python package exists in environment."""833        if self.n < 1:834            msg = "n must be at least 1."835            raise ValueError(msg)836        if self.n > 1 and self.streaming:837            msg = "n must be 1 when streaming."838            raise ValueError(msg)839840        api_key = self.fireworks_api_key.get_secret_value()841        base_url = self.fireworks_api_base842        # 0.x accepted a `(connect, read)` tuple. 1.x's SDK only accepts a843        # float, `httpx.Timeout`, or `None`  normalize so existing user code844        # keeps working.845        if isinstance(self.request_timeout, tuple):846            connect, read = self.request_timeout847            timeout: Any = httpx.Timeout(read, connect=connect)848        else:849            timeout = self.request_timeout850        # `langchain-fireworks` owns retry/backoff via `_create_retry_decorator`851        # so the LangChain `run_manager` sees each attempt. Suppress the852        # SDK's built-in retry layer to avoid double-retrying.853        if not self.client:854            self._sdk_client = Fireworks(855                api_key=api_key,856                base_url=base_url,857                timeout=timeout,858                max_retries=0,859            )860            self.client = self._sdk_client.chat.completions861        if not self.async_client:862            self._async_sdk_client = AsyncFireworks(863                api_key=api_key,864                base_url=base_url,865                timeout=timeout,866                max_retries=0,867            )868            self.async_client = self._async_sdk_client.chat.completions869        return self870871    def close(self) -> None:872        """Close the underlying sync HTTP client.873874        After calling, sync invocations on this model will raise. Async875        invocations remain available until `aclose()` is also called. Safe to876        call multiple times.877        """878        if self._sdk_client is not None:879            self._sdk_client.close()880881    async def aclose(self) -> None:882        """Close the underlying async HTTP client.883884        Releases the aiohttp-backed connector that the 1.x SDK uses by885        default. Without this, transient `ChatFireworks` instances can leak886        an `Unclosed connector` warning at GC if the event loop has already887        stopped. Safe to call multiple times.888        """889        if self._async_sdk_client is not None:890            await self._async_sdk_client.close()891892    def _resolve_model_profile(self) -> ModelProfile | None:893        return _get_default_model_profile(self.model_name) or None894895    @property896    def _default_params(self) -> dict[str, Any]:897        """Get the default parameters for calling Fireworks API."""898        params = {899            "model": self.model_name,900            "stream": self.streaming,901            "n": self.n,902            "stop": self.stop,903            **self.model_kwargs,904        }905        if self.temperature is not None:906            params["temperature"] = self.temperature907        if self.max_tokens is not None:908            params["max_tokens"] = self.max_tokens909        if self.service_tier is not None:910            params["service_tier"] = self.service_tier911        return params912913    def _get_ls_params(914        self, stop: list[str] | None = None, **kwargs: Any915    ) -> LangSmithParams:916        """Get standard params for tracing."""917        params = self._get_invocation_params(stop=stop, **kwargs)918        ls_params = LangSmithParams(919            ls_provider="fireworks",920            ls_model_name=params.get("model", self.model_name),921            ls_model_type="chat",922            ls_temperature=params.get("temperature", self.temperature),923        )924        if ls_max_tokens := params.get("max_tokens", self.max_tokens):925            ls_params["ls_max_tokens"] = ls_max_tokens926        if ls_stop := stop or params.get("stop", None):927            ls_params["ls_stop"] = ls_stop928        return ls_params929930    def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict:931        overall_token_usage: dict = {}932        system_fingerprint = None933        for output in llm_outputs:934            if output is None:935                # Happens in streaming936                continue937            token_usage = output["token_usage"]938            if token_usage is not None:939                for k, v in token_usage.items():940                    if k in overall_token_usage:941                        overall_token_usage[k] += v942                    else:943                        overall_token_usage[k] = v944            if system_fingerprint is None:945                system_fingerprint = output.get("system_fingerprint")946        combined = {"token_usage": overall_token_usage, "model_name": self.model_name}947        if system_fingerprint:948            combined["system_fingerprint"] = system_fingerprint949        return combined950951    def _stream(952        self,953        messages: list[BaseMessage],954        stop: list[str] | None = None,955        run_manager: CallbackManagerForLLMRun | None = None,956        **kwargs: Any,957    ) -> Iterator[ChatGenerationChunk]:958        message_dicts, params = self._create_message_dicts(messages, stop)959        params = {**params, **kwargs, "stream": True}960        if self.stream_usage and "stream_options" not in params:961            params["stream_options"] = {"include_usage": True}962963        default_chunk_class: type[BaseMessageChunk] = AIMessageChunk964        try:965            stream = _completion_with_retry(966                self, run_manager=run_manager, messages=message_dicts, **params967            )968        except BadRequestError as e:969            _handle_fireworks_invalid_request(e)970        for chunk in stream:971            if not isinstance(chunk, dict):972                chunk = chunk.model_dump()973            message_chunk = _convert_chunk_to_message_chunk(chunk, default_chunk_class)974            generation_info: dict[str, Any] = {}975            logprobs = None976            if choices := chunk.get("choices"):977                choice = choices[0]978                if finish_reason := choice.get("finish_reason"):979                    generation_info["finish_reason"] = finish_reason980                    generation_info["model_name"] = self.model_name981                logprobs = choice.get("logprobs")982                if logprobs:983                    generation_info["logprobs"] = logprobs984            default_chunk_class = message_chunk.__class__985            generation_chunk = ChatGenerationChunk(986                message=message_chunk, generation_info=generation_info or None987            )988            if run_manager:989                run_manager.on_llm_new_token(990                    generation_chunk.text, chunk=generation_chunk, logprobs=logprobs991                )992            yield generation_chunk993994    def _generate(995        self,996        messages: list[BaseMessage],997        stop: list[str] | None = None,998        run_manager: CallbackManagerForLLMRun | None = None,999        stream: bool | None = None,  # noqa: FBT0011000        **kwargs: Any,1001    ) -> ChatResult:1002        should_stream = stream if stream is not None else self.streaming1003        if should_stream:1004            stream_iter = self._stream(1005                messages, stop=stop, run_manager=run_manager, **kwargs1006            )1007            return generate_from_stream(stream_iter)1008        message_dicts, params = self._create_message_dicts(messages, stop)1009        params = {1010            **params,1011            **({"stream": stream} if stream is not None else {}),1012            **kwargs,1013        }1014        try:1015            response = _completion_with_retry(1016                self, run_manager=run_manager, messages=message_dicts, **params1017            )1018        except BadRequestError as e:1019            _handle_fireworks_invalid_request(e)1020        return self._create_chat_result(response)10211022    def _create_message_dicts(1023        self, messages: list[BaseMessage], stop: list[str] | None1024    ) -> tuple[list[dict[str, Any]], dict[str, Any]]:1025        params = self._default_params1026        if stop is not None:1027            params["stop"] = stop1028        message_dicts = [_convert_message_to_dict(m) for m in messages]1029        return message_dicts, params10301031    def _create_chat_result(self, response: dict | BaseModel) -> ChatResult:1032        generations = []1033        if not isinstance(response, dict):1034            response = response.model_dump()1035        token_usage = response.get("usage", {})1036        service_tier = response.get("service_tier")1037        for res in response["choices"]:1038            message = _convert_dict_to_message(res["message"])1039            if isinstance(message, AIMessage):1040                if token_usage:1041                    message.usage_metadata = {1042                        "input_tokens": token_usage.get("prompt_tokens", 0),1043                        "output_tokens": token_usage.get("completion_tokens", 0),1044                        "total_tokens": token_usage.get("total_tokens", 0),1045                    }1046                    message.response_metadata["model_provider"] = "fireworks"1047                    message.response_metadata["model_name"] = self.model_name1048                if service_tier:1049                    message.response_metadata["service_tier"] = service_tier1050            generation_info = {"finish_reason": res.get("finish_reason")}1051            if "logprobs" in res:1052                generation_info["logprobs"] = res["logprobs"]1053            gen = ChatGeneration(1054                message=message,1055                generation_info=generation_info,1056            )1057            generations.append(gen)1058        llm_output = {1059            "token_usage": token_usage,1060            "system_fingerprint": response.get("system_fingerprint", ""),1061        }1062        if service_tier:1063            llm_output["service_tier"] = service_tier1064        return ChatResult(generations=generations, llm_output=llm_output)10651066    async def _astream(1067        self,1068        messages: list[BaseMessage],1069        stop: list[str] | None = None,1070        run_manager: AsyncCallbackManagerForLLMRun | None = None,1071        **kwargs: Any,1072    ) -> AsyncIterator[ChatGenerationChunk]:1073        message_dicts, params = self._create_message_dicts(messages, stop)1074        params = {**params, **kwargs, "stream": True}1075        if self.stream_usage and "stream_options" not in params:1076            params["stream_options"] = {"include_usage": True}10771078        default_chunk_class: type[BaseMessageChunk] = AIMessageChunk1079        try:1080            stream = await _acompletion_with_retry(1081                self, run_manager=run_manager, messages=message_dicts, **params1082            )1083        except BadRequestError as e:1084            _handle_fireworks_invalid_request(e)1085        async for chunk in stream:1086            if not isinstance(chunk, dict):1087                chunk = chunk.model_dump()1088            message_chunk = _convert_chunk_to_message_chunk(chunk, default_chunk_class)1089            generation_info: dict[str, Any] = {}1090            logprobs = None1091            if choices := chunk.get("choices"):1092                choice = choices[0]1093                if finish_reason := choice.get("finish_reason"):1094                    generation_info["finish_reason"] = finish_reason1095                    generation_info["model_name"] = self.model_name1096                logprobs = choice.get("logprobs")1097                if logprobs:1098                    generation_info["logprobs"] = logprobs1099            default_chunk_class = message_chunk.__class__1100            generation_chunk = ChatGenerationChunk(1101                message=message_chunk, generation_info=generation_info or None1102            )1103            if run_manager:1104                await run_manager.on_llm_new_token(1105                    token=generation_chunk.text,1106                    chunk=generation_chunk,1107                    logprobs=logprobs,1108                )1109            yield generation_chunk11101111    async def _agenerate(1112        self,1113        messages: list[BaseMessage],1114        stop: list[str] | None = None,1115        run_manager: AsyncCallbackManagerForLLMRun | None = None,1116        stream: bool | None = None,  # noqa: FBT0011117        **kwargs: Any,1118    ) -> ChatResult:1119        should_stream = stream if stream is not None else self.streaming1120        if should_stream:1121            stream_iter = self._astream(1122                messages, stop=stop, run_manager=run_manager, **kwargs1123            )1124            return await agenerate_from_stream(stream_iter)11251126        message_dicts, params = self._create_message_dicts(messages, stop)1127        params = {1128            **params,1129            **({"stream": stream} if stream is not None else {}),1130            **kwargs,1131        }1132        try:1133            response = await _acompletion_with_retry(1134                self, run_manager=run_manager, messages=message_dicts, **params1135            )1136        except BadRequestError as e:1137            _handle_fireworks_invalid_request(e)1138        return self._create_chat_result(response)11391140    @property1141    def _identifying_params(self) -> dict[str, Any]:1142        """Get the identifying parameters."""1143        return {"model_name": self.model_name, **self._default_params}11441145    def _get_invocation_params(1146        self, stop: list[str] | None = None, **kwargs: Any1147    ) -> dict[str, Any]:1148        """Get the parameters used to invoke the model."""1149        return {1150            "model": self.model_name,1151            **super()._get_invocation_params(stop=stop),1152            **self._default_params,1153            **kwargs,1154        }11551156    @property1157    def _llm_type(self) -> str:1158        """Return type of chat model."""1159        return "fireworks-chat"11601161    def bind_tools(1162        self,1163        tools: Sequence[dict[str, Any] | type[BaseModel] | Callable | BaseTool],1164        *,1165        tool_choice: dict | str | bool | None = None,1166        **kwargs: Any,1167    ) -> Runnable[LanguageModelInput, AIMessage]:1168        """Bind tool-like objects to this chat model.11691170        Assumes model is compatible with Fireworks tool-calling API.11711172        Args:1173            tools: A list of tool definitions to bind to this chat model.11741175                Supports any tool definition handled by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1176            tool_choice: Which tool to require the model to call.1177                Must be the name of the single provided function,1178                `'auto'` to automatically determine which function to call1179                with the option to not call any function, `'any'` to enforce that some1180                function is called, or a dict of the form:1181                `{"type": "function", "function": {"name": <<tool_name>>}}`.1182            **kwargs: Any additional parameters to pass to1183                `langchain_fireworks.chat_models.ChatFireworks.bind`1184        """  # noqa: E5011185        strict = kwargs.pop("strict", None)1186        formatted_tools = [1187            convert_to_openai_tool(tool, strict=strict) for tool in tools1188        ]1189        if tool_choice is not None and tool_choice:1190            if isinstance(tool_choice, str) and (1191                tool_choice not in ("auto", "any", "none")1192            ):1193                tool_choice = {"type": "function", "function": {"name": tool_choice}}1194            if isinstance(tool_choice, bool):1195                if len(tools) > 1:1196                    msg = (1197                        "tool_choice can only be True when there is one tool. Received "1198                        f"{len(tools)} tools."1199                    )1200                    raise ValueError(msg)1201                tool_name = formatted_tools[0]["function"]["name"]1202                tool_choice = {1203                    "type": "function",1204                    "function": {"name": tool_name},1205                }12061207            kwargs["tool_choice"] = tool_choice1208        return super().bind(tools=formatted_tools, **kwargs)12091210    def with_structured_output(1211        self,1212        schema: dict | type[BaseModel] | None = None,1213        *,1214        method: Literal[1215            "function_calling", "json_mode", "json_schema"1216        ] = "function_calling",1217        include_raw: bool = False,1218        **kwargs: Any,1219    ) -> Runnable[LanguageModelInput, dict | BaseModel]:1220        """Model wrapper that returns outputs formatted to match the given schema.12211222        Args:1223            schema: The output schema. Can be passed in as:12241225                - An OpenAI function/tool schema,1226                - A JSON Schema,1227                - A `TypedDict` class,1228                - Or a Pydantic class.12291230                If `schema` is a Pydantic class then the model output will be a1231                Pydantic instance of that class, and the model-generated fields will be1232                validated by the Pydantic class. Otherwise the model output will be a1233                dict and will not be validated.12341235                See `langchain_core.utils.function_calling.convert_to_openai_tool` for1236                more on how to properly specify types and descriptions of schema fields1237                when specifying a Pydantic or `TypedDict` class.12381239            method: The method for steering model generation, one of:12401241                - `'function_calling'`:1242                    Uses Fireworks's [tool-calling features](https://docs.fireworks.ai/guides/function-calling).1243                - `'json_schema'`:1244                    Uses Fireworks's [structured output feature](https://docs.fireworks.ai/structured-responses/structured-response-formatting).1245                - `'json_mode'`:1246                    Uses Fireworks's [JSON mode feature](https://docs.fireworks.ai/structured-responses/structured-response-formatting).12471248                !!! warning "Behavior changed in `langchain-fireworks` 0.2.8"12491250                    Added support for `'json_schema'`.12511252            include_raw:1253                If `False` then only the parsed structured output is returned.12541255                If an error occurs during model output parsing it will be raised.12561257                If `True` then both the raw model response (a `BaseMessage`) and the1258                parsed model response will be returned.12591260                If an error occurs during output parsing it will be caught and returned1261                as well.12621263                The final output is always a `dict` with keys `'raw'`, `'parsed'`, and1264                `'parsing_error'`.12651266            kwargs:1267                Any additional parameters to pass to the `langchain.runnable.Runnable`1268                constructor.12691270        Returns:1271            A `Runnable` that takes same inputs as a1272                `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is1273                `False` and `schema` is a Pydantic class, `Runnable` outputs an instance1274                of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is1275                `False` then `Runnable` outputs a `dict`.12761277                If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:12781279                - `'raw'`: `BaseMessage`1280                - `'parsed'`: `None` if there was a parsing error, otherwise the type1281                    depends on the `schema` as described above.1282                - `'parsing_error'`: `BaseException | None`12831284        Example: schema=Pydantic class, method="function_calling", include_raw=False:12851286        ```python1287        from typing import Optional12881289        from langchain_fireworks import ChatFireworks1290        from pydantic import BaseModel, Field129112921293        class AnswerWithJustification(BaseModel):1294            '''An answer to the user question along with justification for the answer.'''12951296            answer: str1297            # If we provide default values and/or descriptions for fields, these will be passed1298            # to the model. This is an important part of improving a model's ability to1299            # correctly return structured outputs.1300            justification: str | None = Field(1301                default=None, description="A justification for the answer."1302            )130313041305        model = ChatFireworks(1306            model="accounts/fireworks/models/gpt-oss-120b",1307            temperature=0,1308        )1309        structured_model = model.with_structured_output(AnswerWithJustification)13101311        structured_model.invoke(1312            "What weighs more a pound of bricks or a pound of feathers"1313        )13141315        # -> AnswerWithJustification(1316        #     answer='They weigh the same',1317        #     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.'1318        # )1319        ```13201321        Example: schema=Pydantic class, method="function_calling", include_raw=True:13221323        ```python1324        from langchain_fireworks import ChatFireworks1325        from pydantic import BaseModel132613271328        class AnswerWithJustification(BaseModel):1329            '''An answer to the user question along with justification for the answer.'''13301331            answer: str1332            justification: str133313341335        model = ChatFireworks(1336            model="accounts/fireworks/models/gpt-oss-120b",1337            temperature=0,1338        )1339        structured_model = model.with_structured_output(1340            AnswerWithJustification, include_raw=True1341        )13421343        structured_model.invoke(1344            "What weighs more a pound of bricks or a pound of feathers"1345        )1346        # -> {1347        #     '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'}]}),1348        #     '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.'),1349        #     'parsing_error': None1350        # }1351        ```13521353        Example: schema=TypedDict class, method="function_calling", include_raw=False:13541355        ```python1356        from typing_extensions import Annotated, TypedDict13571358        from langchain_fireworks import ChatFireworks135913601361        class AnswerWithJustification(TypedDict):1362            '''An answer to the user question along with justification for the answer.'''13631364            answer: str1365            justification: Annotated[1366                str | None, None, "A justification for the answer."1367            ]136813691370        model = ChatFireworks(1371            model="accounts/fireworks/models/gpt-oss-120b",1372            temperature=0,1373        )1374        structured_model = model.with_structured_output(AnswerWithJustification)13751376        structured_model.invoke(1377            "What weighs more a pound of bricks or a pound of feathers"1378        )1379        # -> {1380        #     'answer': 'They weigh the same',1381        #     '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.'1382        # }1383        ```13841385        Example: schema=OpenAI function schema, method="function_calling", include_raw=False:13861387        ```python1388        from langchain_fireworks import ChatFireworks13891390        oai_schema = {1391            "name": "AnswerWithJustification",1392            "description": "An answer to the user question along with justification for the answer.",1393            "parameters": {1394                "type": "object",1395                "properties": {1396                    "answer": {"type": "string"},1397                    "justification": {1398                        "description": "A justification for the answer.",1399                        "type": "string",1400                    },1401                },1402                "required": ["answer"],1403            },1404        }14051406        model = ChatFireworks(1407            model="accounts/fireworks/models/gpt-oss-120b",1408            temperature=0,1409        )1410        structured_model = model.with_structured_output(oai_schema)14111412        structured_model.invoke(1413            "What weighs more a pound of bricks or a pound of feathers"1414        )1415        # -> {1416        #     'answer': 'They weigh the same',1417        #     '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.'1418        # }1419        ```14201421        Example: schema=Pydantic class, method="json_mode", include_raw=True:14221423        ```python1424        from langchain_fireworks import ChatFireworks1425        from pydantic import BaseModel142614271428        class AnswerWithJustification(BaseModel):1429            answer: str1430            justification: str143114321433        model = ChatFireworks(1434            model="accounts/fireworks/models/gpt-oss-120b", temperature=01435        )1436        structured_model = model.with_structured_output(1437            AnswerWithJustification, method="json_mode", include_raw=True1438        )14391440        structured_model.invoke(1441            "Answer the following question. "1442            "Make sure to return a JSON blob with keys 'answer' and 'justification'. "1443            "What's heavier a pound of bricks or a pound of feathers?"1444        )1445        # -> {1446        #     'raw': AIMessage(content='{"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."}'),1447        #     '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.'),1448        #     'parsing_error': None1449        # }1450        ```14511452        Example: schema=None, method="json_mode", include_raw=True:14531454        ```python1455        structured_model = model.with_structured_output(1456            method="json_mode", include_raw=True1457        )14581459        structured_model.invoke(1460            "Answer the following question. "1461            "Make sure to return a JSON blob with keys 'answer' and 'justification'. "1462            "What's heavier a pound of bricks or a pound of feathers?"1463        )1464        # -> {1465        #     'raw': AIMessage(content='{"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."}'),1466        #     'parsed': {1467        #         'answer': 'They are both the same weight.',1468        #         '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.'1469        #     },1470        #     'parsing_error': None1471        # }1472        ```14731474        """  # noqa: E5011475        _ = kwargs.pop("strict", None)1476        if kwargs:1477            msg = f"Received unsupported arguments {kwargs}"1478            raise ValueError(msg)1479        is_pydantic_schema = _is_pydantic_class(schema)1480        if method == "function_calling":1481            if schema is None:1482                msg = (1483                    "schema must be specified when method is 'function_calling'. "1484                    "Received None."1485                )1486                raise ValueError(msg)1487            formatted_tool = convert_to_openai_tool(schema)1488            tool_name = formatted_tool["function"]["name"]1489            llm = self.bind_tools(1490                [schema],1491                tool_choice=tool_name,1492                ls_structured_output_format={1493                    "kwargs": {"method": "function_calling"},1494                    "schema": formatted_tool,1495                },1496            )1497            if is_pydantic_schema:1498                output_parser: OutputParserLike = PydanticToolsParser(1499                    tools=[schema],  # type: ignore[list-item]1500                    first_tool_only=True,  # type: ignore[list-item]1501                )1502            else:1503                output_parser = JsonOutputKeyToolsParser(1504                    key_name=tool_name, first_tool_only=True1505                )1506        elif method == "json_schema":1507            if schema is None:1508                msg = (1509                    "schema must be specified when method is 'json_schema'. "1510                    "Received None."1511                )1512                raise ValueError(msg)1513            formatted_schema = convert_to_json_schema(schema)1514            llm = self.bind(1515                response_format={"type": "json_object", "schema": formatted_schema},1516                ls_structured_output_format={1517                    "kwargs": {"method": "json_schema"},1518                    "schema": schema,1519                },1520            )1521            output_parser = (1522                PydanticOutputParser(pydantic_object=schema)  # type: ignore[arg-type]1523                if is_pydantic_schema1524                else JsonOutputParser()1525            )1526        elif method == "json_mode":1527            llm = self.bind(1528                response_format={"type": "json_object"},1529                ls_structured_output_format={1530                    "kwargs": {"method": "json_mode"},1531                    "schema": schema,1532                },1533            )1534            output_parser = (1535                PydanticOutputParser(pydantic_object=schema)  # type: ignore[type-var, arg-type]1536                if is_pydantic_schema1537                else JsonOutputParser()1538            )1539        else:1540            msg = (1541                f"Unrecognized method argument. Expected one of 'function_calling' or "1542                f"'json_mode'. Received: '{method}'"1543            )1544            raise ValueError(msg)15451546        if include_raw:1547            parser_assign = RunnablePassthrough.assign(1548                parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None1549            )1550            parser_none = RunnablePassthrough.assign(parsed=lambda _: None)1551            parser_with_fallback = parser_assign.with_fallbacks(1552                [parser_none], exception_key="parsing_error"1553            )1554            return RunnableMap(raw=llm) | parser_with_fallback1555        return llm | output_parser155615571558def _is_pydantic_class(obj: Any) -> bool:1559    return isinstance(obj, type) and is_basemodel_subclass(obj)156015611562def _lc_tool_call_to_fireworks_tool_call(tool_call: ToolCall) -> dict:1563    return {1564        "type": "function",1565        "id": tool_call["id"],1566        "function": {1567            "name": tool_call["name"],1568            "arguments": json.dumps(tool_call["args"], ensure_ascii=False),1569        },1570    }157115721573def _lc_invalid_tool_call_to_fireworks_tool_call(1574    invalid_tool_call: InvalidToolCall,1575) -> dict:1576    return {1577        "type": "function",1578        "id": invalid_tool_call["id"],1579        "function": {1580            "name": invalid_tool_call["name"],1581            "arguments": invalid_tool_call["args"],1582        },1583    }

Code quality findings 25

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(sanitized[0], dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(sanitized[0]["text"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and "type" in block:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(source, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, ChatMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif 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, FunctionMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(message, ToolMessage):
Ensure functions have docstrings for documentation
missing-docstring
def lc_secrets(self) -> dict[str, str]:
Ensure functions have docstrings for documentation
missing-docstring
def lc_attributes(self) -> dict[str, Any]:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.request_timeout, tuple):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(chunk, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(response, dict):
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(chunk, dict):
Ensure functions have docstrings for documentation
missing-docstring
def bind_tools(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_choice, str) and (
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_choice, bool):
Ensure functions have docstrings for documentation
missing-docstring
def with_structured_output(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
return isinstance(obj, type) and is_basemodel_subclass(obj)

Get this view in your editor

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