Overuse may indicate design issues; consider polymorphism
if not isinstance(content, list):
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 }
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.