langchain-openai 0.2.0.dev2__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (17) hide show
  1. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/PKG-INFO +2 -2
  2. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/chat_models/azure.py +33 -331
  3. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/chat_models/base.py +142 -54
  4. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/embeddings/base.py +13 -9
  5. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/llms/base.py +2 -5
  6. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/pyproject.toml +3 -2
  7. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/LICENSE +0 -0
  8. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/README.md +0 -0
  9. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/__init__.py +0 -0
  10. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/chat_models/__init__.py +0 -0
  11. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/embeddings/__init__.py +0 -0
  12. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/embeddings/azure.py +0 -0
  13. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/llms/__init__.py +0 -0
  14. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/llms/azure.py +0 -0
  15. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/output_parsers/__init__.py +0 -0
  16. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/output_parsers/tools.py +0 -0
  17. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.2}/langchain_openai/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langchain-openai
3
- Version: 0.2.0.dev2
3
+ Version: 0.2.2
4
4
  Summary: An integration package connecting OpenAI and LangChain
5
5
  Home-page: https://github.com/langchain-ai/langchain
6
6
  License: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: langchain-core (>=0.3.0.dev1,<0.4.0)
14
+ Requires-Dist: langchain-core (>=0.3.9,<0.4.0)
15
15
  Requires-Dist: openai (>=1.40.0,<2.0.0)
16
16
  Requires-Dist: tiktoken (>=0.7,<1)
17
17
  Project-URL: Repository, https://github.com/langchain-ai/langchain
@@ -4,37 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import os
7
- from operator import itemgetter
8
- from typing import (
9
- Any,
10
- Callable,
11
- Dict,
12
- List,
13
- Literal,
14
- Optional,
15
- Sequence,
16
- Type,
17
- TypedDict,
18
- TypeVar,
19
- Union,
20
- overload,
21
- )
7
+ from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, TypeVar, Union
22
8
 
23
9
  import openai
24
- from langchain_core.language_models import LanguageModelInput
25
10
  from langchain_core.language_models.chat_models import LangSmithParams
26
11
  from langchain_core.messages import BaseMessage
27
- from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
28
- from langchain_core.output_parsers.base import OutputParserLike
29
- from langchain_core.output_parsers.openai_tools import (
30
- JsonOutputKeyToolsParser,
31
- PydanticToolsParser,
32
- )
33
12
  from langchain_core.outputs import ChatResult
34
- from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough
35
- from langchain_core.tools import BaseTool
36
13
  from langchain_core.utils import from_env, secret_from_env
37
- from langchain_core.utils.function_calling import convert_to_openai_tool
38
14
  from langchain_core.utils.pydantic import is_basemodel_subclass
39
15
  from pydantic import BaseModel, Field, SecretStr, model_validator
40
16
  from typing_extensions import Self
@@ -538,6 +514,7 @@ class AzureChatOpenAI(BaseChatOpenAI):
538
514
  default_factory=from_env("OPENAI_API_TYPE", default="azure")
539
515
  )
540
516
  """Legacy, for openai<1.0.0 support."""
517
+
541
518
  validate_base_url: bool = True
542
519
  """If legacy arg openai_api_base is passed in, try to infer if it is a base_url or
543
520
  azure_endpoint and update client params accordingly.
@@ -550,6 +527,28 @@ class AzureChatOpenAI(BaseChatOpenAI):
550
527
  Used for tracing and token counting. Does NOT affect completion.
551
528
  """
552
529
 
530
+ disabled_params: Optional[Dict[str, Any]] = Field(default=None)
531
+ """Parameters of the OpenAI client or chat.completions endpoint that should be
532
+ disabled for the given model.
533
+
534
+ Should be specified as ``{"param": None | ['val1', 'val2']}`` where the key is the
535
+ parameter and the value is either None, meaning that parameter should never be
536
+ used, or it's a list of disabled values for the parameter.
537
+
538
+ For example, older models may not support the 'parallel_tool_calls' parameter at
539
+ all, in which case ``disabled_params={"parallel_tool_calls: None}`` can ben passed
540
+ in.
541
+
542
+ If a parameter is disabled then it will not be used by default in any methods, e.g.
543
+ in
544
+ :meth:`~langchain_openai.chat_models.azure.AzureChatOpenAI.with_structured_output`.
545
+ However this does not prevent a user from directly passed in the parameter during
546
+ invocation.
547
+
548
+ By default, unless ``model_name="gpt-4o"`` is specified, then
549
+ 'parallel_tools_calls' will be disabled.
550
+ """
551
+
553
552
  @classmethod
554
553
  def get_lc_namespace(cls) -> List[str]:
555
554
  """Get the namespace of the langchain object."""
@@ -574,6 +573,13 @@ class AzureChatOpenAI(BaseChatOpenAI):
574
573
  if self.n > 1 and self.streaming:
575
574
  raise ValueError("n must be 1 when streaming.")
576
575
 
576
+ if self.disabled_params is None:
577
+ # As of 09-17-2024 'parallel_tool_calls' param is only supported for gpt-4o.
578
+ if self.model_name and self.model_name == "gpt-4o":
579
+ pass
580
+ else:
581
+ self.disabled_params = {"parallel_tool_calls": None}
582
+
577
583
  # Check OPENAI_ORGANIZATION for backwards compatibility.
578
584
  self.openai_organization = (
579
585
  self.openai_organization
@@ -634,311 +640,6 @@ class AzureChatOpenAI(BaseChatOpenAI):
634
640
  self.async_client = self.root_async_client.chat.completions
635
641
  return self
636
642
 
637
- def bind_tools(
638
- self,
639
- tools: Sequence[Union[Dict[str, Any], Type, Callable, BaseTool]],
640
- *,
641
- tool_choice: Optional[
642
- Union[dict, str, Literal["auto", "none", "required", "any"], bool]
643
- ] = None,
644
- **kwargs: Any,
645
- ) -> Runnable[LanguageModelInput, BaseMessage]:
646
- # As of 05/2024 Azure OpenAI doesn't support tool_choice="required".
647
- # TODO: Update this condition once tool_choice="required" is supported.
648
- if tool_choice in ("any", "required", True):
649
- if len(tools) > 1:
650
- raise ValueError(
651
- f"Azure OpenAI does not currently support {tool_choice=}. Should "
652
- f"be one of 'auto', 'none', or the name of the tool to call."
653
- )
654
- else:
655
- tool_choice = convert_to_openai_tool(tools[0])["function"]["name"]
656
- return super().bind_tools(tools, tool_choice=tool_choice, **kwargs)
657
-
658
- # TODO: Fix typing.
659
- @overload # type: ignore[override]
660
- def with_structured_output(
661
- self,
662
- schema: Optional[_DictOrPydanticClass] = None,
663
- *,
664
- method: Literal["function_calling", "json_mode"] = "function_calling",
665
- include_raw: Literal[True] = True,
666
- **kwargs: Any,
667
- ) -> Runnable[LanguageModelInput, _AllReturnType]: ...
668
-
669
- @overload
670
- def with_structured_output(
671
- self,
672
- schema: Optional[_DictOrPydanticClass] = None,
673
- *,
674
- method: Literal["function_calling", "json_mode"] = "function_calling",
675
- include_raw: Literal[False] = False,
676
- **kwargs: Any,
677
- ) -> Runnable[LanguageModelInput, _DictOrPydantic]: ...
678
-
679
- def with_structured_output(
680
- self,
681
- schema: Optional[_DictOrPydanticClass] = None,
682
- *,
683
- method: Literal["function_calling", "json_mode"] = "function_calling",
684
- include_raw: bool = False,
685
- **kwargs: Any,
686
- ) -> Runnable[LanguageModelInput, _DictOrPydantic]:
687
- """Model wrapper that returns outputs formatted to match the given schema.
688
-
689
- Args:
690
- schema:
691
- The output schema. Can be passed in as:
692
- - an OpenAI function/tool schema,
693
- - a JSON Schema,
694
- - a TypedDict class,
695
- - or a Pydantic class.
696
- If ``schema`` is a Pydantic class then the model output will be a
697
- Pydantic instance of that class, and the model-generated fields will be
698
- validated by the Pydantic class. Otherwise the model output will be a
699
- dict and will not be validated. See :meth:`langchain_core.utils.function_calling.convert_to_openai_tool`
700
- for more on how to properly specify types and descriptions of
701
- schema fields when specifying a Pydantic or TypedDict class.
702
- method:
703
- The method for steering model generation, either "function_calling"
704
- or "json_mode". If "function_calling" then the schema will be converted
705
- to an OpenAI function and the returned model will make use of the
706
- function-calling API. If "json_mode" then OpenAI's JSON mode will be
707
- used. Note that if using "json_mode" then you must include instructions
708
- for formatting the output into the desired schema into the model call.
709
- include_raw:
710
- If False then only the parsed structured output is returned. If
711
- an error occurs during model output parsing it will be raised. If True
712
- then both the raw model response (a BaseMessage) and the parsed model
713
- response will be returned. If an error occurs during output parsing it
714
- will be caught and returned as well. The final output is always a dict
715
- with keys "raw", "parsed", and "parsing_error".
716
-
717
- Returns:
718
- A Runnable that takes same inputs as a :class:`langchain_core.language_models.chat.BaseChatModel`.
719
-
720
- If ``include_raw`` is False and ``schema`` is a Pydantic class, Runnable outputs
721
- an instance of ``schema`` (i.e., a Pydantic object).
722
-
723
- Otherwise, if ``include_raw`` is False then Runnable outputs a dict.
724
-
725
- If ``include_raw`` is True, then Runnable outputs a dict with keys:
726
- - ``"raw"``: BaseMessage
727
- - ``"parsed"``: None if there was a parsing error, otherwise the type depends on the ``schema`` as described above.
728
- - ``"parsing_error"``: Optional[BaseException]
729
-
730
- Example: schema=Pydantic class, method="function_calling", include_raw=False:
731
- .. code-block:: python
732
-
733
- from typing import Optional
734
-
735
- from langchain_openai import AzureChatOpenAI
736
- from pydantic import BaseModel, Field
737
-
738
-
739
- class AnswerWithJustification(BaseModel):
740
- '''An answer to the user question along with justification for the answer.'''
741
-
742
- answer: str
743
- # If we provide default values and/or descriptions for fields, these will be passed
744
- # to the model. This is an important part of improving a model's ability to
745
- # correctly return structured outputs.
746
- justification: Optional[str] = Field(
747
- default=None, description="A justification for the answer."
748
- )
749
-
750
-
751
- llm = AzureChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
752
- structured_llm = llm.with_structured_output(AnswerWithJustification)
753
-
754
- structured_llm.invoke(
755
- "What weighs more a pound of bricks or a pound of feathers"
756
- )
757
-
758
- # -> AnswerWithJustification(
759
- # answer='They weigh the same',
760
- # 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.'
761
- # )
762
-
763
- Example: schema=Pydantic class, method="function_calling", include_raw=True:
764
- .. code-block:: python
765
-
766
- from langchain_openai import AzureChatOpenAI
767
- from pydantic import BaseModel
768
-
769
-
770
- class AnswerWithJustification(BaseModel):
771
- '''An answer to the user question along with justification for the answer.'''
772
-
773
- answer: str
774
- justification: str
775
-
776
-
777
- llm = AzureChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
778
- structured_llm = llm.with_structured_output(
779
- AnswerWithJustification, include_raw=True
780
- )
781
-
782
- structured_llm.invoke(
783
- "What weighs more a pound of bricks or a pound of feathers"
784
- )
785
- # -> {
786
- # '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'}]}),
787
- # '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.'),
788
- # 'parsing_error': None
789
- # }
790
-
791
- Example: schema=TypedDict class, method="function_calling", include_raw=False:
792
- .. code-block:: python
793
-
794
- # IMPORTANT: If you are using Python <=3.8, you need to import Annotated
795
- # from typing_extensions, not from typing.
796
- from typing_extensions import Annotated, TypedDict
797
-
798
- from langchain_openai import AzureChatOpenAI
799
-
800
-
801
- class AnswerWithJustification(TypedDict):
802
- '''An answer to the user question along with justification for the answer.'''
803
-
804
- answer: str
805
- justification: Annotated[
806
- Optional[str], None, "A justification for the answer."
807
- ]
808
-
809
-
810
- llm = AzureChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
811
- structured_llm = llm.with_structured_output(AnswerWithJustification)
812
-
813
- structured_llm.invoke(
814
- "What weighs more a pound of bricks or a pound of feathers"
815
- )
816
- # -> {
817
- # 'answer': 'They weigh the same',
818
- # '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.'
819
- # }
820
-
821
- Example: schema=OpenAI function schema, method="function_calling", include_raw=False:
822
- .. code-block:: python
823
-
824
- from langchain_openai import AzureChatOpenAI
825
-
826
- oai_schema = {
827
- 'name': 'AnswerWithJustification',
828
- 'description': 'An answer to the user question along with justification for the answer.',
829
- 'parameters': {
830
- 'type': 'object',
831
- 'properties': {
832
- 'answer': {'type': 'string'},
833
- 'justification': {'description': 'A justification for the answer.', 'type': 'string'}
834
- },
835
- 'required': ['answer']
836
- }
837
- }
838
-
839
- llm = AzureChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
840
- structured_llm = llm.with_structured_output(oai_schema)
841
-
842
- structured_llm.invoke(
843
- "What weighs more a pound of bricks or a pound of feathers"
844
- )
845
- # -> {
846
- # 'answer': 'They weigh the same',
847
- # '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.'
848
- # }
849
-
850
- Example: schema=Pydantic class, method="json_mode", include_raw=True:
851
- .. code-block::
852
-
853
- from langchain_openai import AzureChatOpenAI
854
- from pydantic import BaseModel
855
-
856
- class AnswerWithJustification(BaseModel):
857
- answer: str
858
- justification: str
859
-
860
- llm = AzureChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
861
- structured_llm = llm.with_structured_output(
862
- AnswerWithJustification,
863
- method="json_mode",
864
- include_raw=True
865
- )
866
-
867
- structured_llm.invoke(
868
- "Answer the following question. "
869
- "Make sure to return a JSON blob with keys 'answer' and 'justification'.\n\n"
870
- "What's heavier a pound of bricks or a pound of feathers?"
871
- )
872
- # -> {
873
- # 'raw': AIMessage(content='{\n "answer": "They are both the same weight.",\n "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \n}'),
874
- # '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.'),
875
- # 'parsing_error': None
876
- # }
877
-
878
- Example: schema=None, method="json_mode", include_raw=True:
879
- .. code-block::
880
-
881
- structured_llm = llm.with_structured_output(method="json_mode", include_raw=True)
882
-
883
- structured_llm.invoke(
884
- "Answer the following question. "
885
- "Make sure to return a JSON blob with keys 'answer' and 'justification'.\n\n"
886
- "What's heavier a pound of bricks or a pound of feathers?"
887
- )
888
- # -> {
889
- # 'raw': AIMessage(content='{\n "answer": "They are both the same weight.",\n "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \n}'),
890
- # 'parsed': {
891
- # 'answer': 'They are both the same weight.',
892
- # '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.'
893
- # },
894
- # 'parsing_error': None
895
- # }
896
- """ # noqa: E501
897
- if kwargs:
898
- raise ValueError(f"Received unsupported arguments {kwargs}")
899
- is_pydantic_schema = _is_pydantic_class(schema)
900
- if method == "function_calling":
901
- if schema is None:
902
- raise ValueError(
903
- "schema must be specified when method is 'function_calling'. "
904
- "Received None."
905
- )
906
- tool_name = convert_to_openai_tool(schema)["function"]["name"]
907
- llm = self.bind_tools([schema], tool_choice=tool_name)
908
- if is_pydantic_schema:
909
- output_parser: OutputParserLike = PydanticToolsParser(
910
- tools=[schema], # type: ignore[list-item]
911
- first_tool_only=True, # type: ignore[list-item]
912
- )
913
- else:
914
- output_parser = JsonOutputKeyToolsParser(
915
- key_name=tool_name, first_tool_only=True
916
- )
917
- elif method == "json_mode":
918
- llm = self.bind(response_format={"type": "json_object"})
919
- output_parser = (
920
- PydanticOutputParser(pydantic_object=schema) # type: ignore[arg-type]
921
- if is_pydantic_schema
922
- else JsonOutputParser()
923
- )
924
- else:
925
- raise ValueError(
926
- f"Unrecognized method argument. Expected one of 'function_calling' or "
927
- f"'json_mode'. Received: '{method}'"
928
- )
929
-
930
- if include_raw:
931
- parser_assign = RunnablePassthrough.assign(
932
- parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None
933
- )
934
- parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
935
- parser_with_fallback = parser_assign.with_fallbacks(
936
- [parser_none], exception_key="parsing_error"
937
- )
938
- return RunnableMap(raw=llm) | parser_with_fallback
939
- else:
940
- return llm | output_parser
941
-
942
643
  @property
943
644
  def _identifying_params(self) -> Dict[str, Any]:
944
645
  """Get the identifying parameters."""
@@ -980,6 +681,8 @@ class AzureChatOpenAI(BaseChatOpenAI):
980
681
  response: Union[dict, openai.BaseModel],
981
682
  generation_info: Optional[Dict] = None,
982
683
  ) -> ChatResult:
684
+ chat_result = super()._create_chat_result(response, generation_info)
685
+
983
686
  if not isinstance(response, dict):
984
687
  response = response.model_dump()
985
688
  for res in response["choices"]:
@@ -988,7 +691,6 @@ class AzureChatOpenAI(BaseChatOpenAI):
988
691
  "Azure has not provided the response due to a content filter "
989
692
  "being triggered"
990
693
  )
991
- chat_result = super()._create_chat_result(response, generation_info)
992
694
 
993
695
  if "model" in response:
994
696
  model = response["model"]
@@ -33,6 +33,7 @@ from urllib.parse import urlparse
33
33
 
34
34
  import openai
35
35
  import tiktoken
36
+ from langchain_core._api.deprecation import deprecated
36
37
  from langchain_core.callbacks import (
37
38
  AsyncCallbackManagerForLLMRun,
38
39
  CallbackManagerForLLMRun,
@@ -62,10 +63,13 @@ from langchain_core.messages import (
62
63
  ToolMessage,
63
64
  ToolMessageChunk,
64
65
  )
65
- from langchain_core.messages.ai import UsageMetadata
66
+ from langchain_core.messages.ai import (
67
+ InputTokenDetails,
68
+ OutputTokenDetails,
69
+ UsageMetadata,
70
+ )
66
71
  from langchain_core.messages.tool import tool_call_chunk
67
72
  from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
68
- from langchain_core.output_parsers.base import OutputParserLike
69
73
  from langchain_core.output_parsers.openai_tools import (
70
74
  JsonOutputKeyToolsParser,
71
75
  PydanticToolsParser,
@@ -86,7 +90,7 @@ from langchain_core.utils.pydantic import (
86
90
  TypeBaseModel,
87
91
  is_basemodel_subclass,
88
92
  )
89
- from langchain_core.utils.utils import build_extra_kwargs, from_env, secret_from_env
93
+ from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_from_env
90
94
  from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
91
95
  from typing_extensions import Self
92
96
 
@@ -286,16 +290,10 @@ def _convert_chunk_to_generation_chunk(
286
290
  ) -> Optional[ChatGenerationChunk]:
287
291
  token_usage = chunk.get("usage")
288
292
  choices = chunk.get("choices", [])
293
+
289
294
  usage_metadata: Optional[UsageMetadata] = (
290
- UsageMetadata(
291
- input_tokens=token_usage.get("prompt_tokens", 0),
292
- output_tokens=token_usage.get("completion_tokens", 0),
293
- total_tokens=token_usage.get("total_tokens", 0),
294
- )
295
- if token_usage
296
- else None
295
+ _create_usage_metadata(token_usage) if token_usage else None
297
296
  )
298
-
299
297
  if len(choices) == 0:
300
298
  # logprobs is implicitly None
301
299
  generation_chunk = ChatGenerationChunk(
@@ -332,6 +330,33 @@ def _convert_chunk_to_generation_chunk(
332
330
  return generation_chunk
333
331
 
334
332
 
333
+ def _update_token_usage(
334
+ overall_token_usage: Union[int, dict], new_usage: Union[int, dict]
335
+ ) -> Union[int, dict]:
336
+ # Token usage is either ints or dictionaries
337
+ # `reasoning_tokens` is nested inside `completion_tokens_details`
338
+ if isinstance(new_usage, int):
339
+ if not isinstance(overall_token_usage, int):
340
+ raise ValueError(
341
+ f"Got different types for token usage: "
342
+ f"{type(new_usage)} and {type(overall_token_usage)}"
343
+ )
344
+ return new_usage + overall_token_usage
345
+ elif isinstance(new_usage, dict):
346
+ if not isinstance(overall_token_usage, dict):
347
+ raise ValueError(
348
+ f"Got different types for token usage: "
349
+ f"{type(new_usage)} and {type(overall_token_usage)}"
350
+ )
351
+ return {
352
+ k: _update_token_usage(overall_token_usage.get(k, 0), v)
353
+ for k, v in new_usage.items()
354
+ }
355
+ else:
356
+ warnings.warn(f"Unexpected type for token usage: {type(new_usage)}")
357
+ return new_usage
358
+
359
+
335
360
  class _FunctionCall(TypedDict):
336
361
  name: str
337
362
 
@@ -413,11 +438,11 @@ class BaseChatOpenAI(BaseChatModel):
413
438
  default_query: Union[Mapping[str, object], None] = None
414
439
  # Configure a custom httpx client. See the
415
440
  # [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
416
- http_client: Union[Any, None] = None
441
+ http_client: Union[Any, None] = Field(default=None, exclude=True)
417
442
  """Optional httpx.Client. Only used for sync invocations. Must specify
418
443
  http_async_client as well if you'd like a custom client for async invocations.
419
444
  """
420
- http_async_client: Union[Any, None] = None
445
+ http_async_client: Union[Any, None] = Field(default=None, exclude=True)
421
446
  """Optional httpx.AsyncClient. Only used for async invocations. Must specify
422
447
  http_client as well if you'd like a custom client for sync invocations."""
423
448
  stop: Optional[Union[List[str], str]] = Field(default=None, alias="stop_sequences")
@@ -427,6 +452,23 @@ class BaseChatOpenAI(BaseChatModel):
427
452
  making requests to OpenAI compatible APIs, such as vLLM."""
428
453
  include_response_headers: bool = False
429
454
  """Whether to include response headers in the output message response_metadata."""
455
+ disabled_params: Optional[Dict[str, Any]] = Field(default=None)
456
+ """Parameters of the OpenAI client or chat.completions endpoint that should be
457
+ disabled for the given model.
458
+
459
+ Should be specified as ``{"param": None | ['val1', 'val2']}`` where the key is the
460
+ parameter and the value is either None, meaning that parameter should never be
461
+ used, or it's a list of disabled values for the parameter.
462
+
463
+ For example, older models may not support the 'parallel_tool_calls' parameter at
464
+ all, in which case ``disabled_params={"parallel_tool_calls: None}`` can ben passed
465
+ in.
466
+
467
+ If a parameter is disabled then it will not be used by default in any methods, e.g.
468
+ in :meth:`~langchain_openai.chat_models.base.ChatOpenAI.with_structured_output`.
469
+ However this does not prevent a user from directly passed in the parameter during
470
+ invocation.
471
+ """
430
472
 
431
473
  model_config = ConfigDict(populate_by_name=True)
432
474
 
@@ -435,10 +477,7 @@ class BaseChatOpenAI(BaseChatModel):
435
477
  def build_extra(cls, values: Dict[str, Any]) -> Any:
436
478
  """Build extra kwargs from additional params that were passed in."""
437
479
  all_required_field_names = get_pydantic_field_names(cls)
438
- extra = values.get("model_kwargs", {})
439
- values["model_kwargs"] = build_extra_kwargs(
440
- extra, values, all_required_field_names
441
- )
480
+ values = _build_model_kwargs(values, all_required_field_names)
442
481
  return values
443
482
 
444
483
  @model_validator(mode="after")
@@ -545,7 +584,9 @@ class BaseChatOpenAI(BaseChatModel):
545
584
  if token_usage is not None:
546
585
  for k, v in token_usage.items():
547
586
  if k in overall_token_usage:
548
- overall_token_usage[k] += v
587
+ overall_token_usage[k] = _update_token_usage(
588
+ overall_token_usage[k], v
589
+ )
549
590
  else:
550
591
  overall_token_usage[k] = v
551
592
  if system_fingerprint is None:
@@ -675,15 +716,11 @@ class BaseChatOpenAI(BaseChatModel):
675
716
  if response_dict.get("error"):
676
717
  raise ValueError(response_dict.get("error"))
677
718
 
678
- token_usage = response_dict.get("usage", {})
719
+ token_usage = response_dict.get("usage")
679
720
  for res in response_dict["choices"]:
680
721
  message = _convert_dict_to_message(res["message"])
681
722
  if token_usage and isinstance(message, AIMessage):
682
- message.usage_metadata = {
683
- "input_tokens": token_usage.get("prompt_tokens", 0),
684
- "output_tokens": token_usage.get("completion_tokens", 0),
685
- "total_tokens": token_usage.get("total_tokens", 0),
686
- }
723
+ message.usage_metadata = _create_usage_metadata(token_usage)
687
724
  generation_info = generation_info or {}
688
725
  generation_info["finish_reason"] = (
689
726
  res.get("finish_reason")
@@ -741,7 +778,7 @@ class BaseChatOpenAI(BaseChatModel):
741
778
  )
742
779
  return
743
780
  if self.include_response_headers:
744
- raw_response = self.async_client.with_raw_response.create(**payload)
781
+ raw_response = await self.async_client.with_raw_response.create(**payload)
745
782
  response = raw_response.parse()
746
783
  base_generation_info = {"headers": dict(raw_response.headers)}
747
784
  else:
@@ -938,6 +975,11 @@ class BaseChatOpenAI(BaseChatModel):
938
975
  num_tokens += 3
939
976
  return num_tokens
940
977
 
978
+ @deprecated(
979
+ since="0.2.1",
980
+ alternative="langchain_openai.chat_models.base.ChatOpenAI.bind_tools",
981
+ removal="0.3.0",
982
+ )
941
983
  def bind_functions(
942
984
  self,
943
985
  functions: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
@@ -1005,22 +1047,18 @@ class BaseChatOpenAI(BaseChatModel):
1005
1047
 
1006
1048
  Assumes model is compatible with OpenAI tool-calling API.
1007
1049
 
1008
- .. versionchanged:: 0.1.21
1009
-
1010
- Support for ``strict`` argument added.
1011
-
1012
1050
  Args:
1013
1051
  tools: A list of tool definitions to bind to this chat model.
1014
1052
  Supports any tool definition handled by
1015
1053
  :meth:`langchain_core.utils.function_calling.convert_to_openai_tool`.
1016
- tool_choice: Which tool to require the model to call.
1017
- Options are:
1018
- - str of the form ``"<<tool_name>>"``: calls <<tool_name>> tool.
1019
- - ``"auto"``: automatically selects a tool (including no tool).
1020
- - ``"none"``: does not call a tool.
1021
- - ``"any"`` or ``"required"`` or ``True``: force at least one tool to be called.
1022
- - dict of the form ``{"type": "function", "function": {"name": <<tool_name>>}}``: calls <<tool_name>> tool.
1023
- - ``False`` or ``None``: no effect, default OpenAI behavior.
1054
+ tool_choice: Which tool to require the model to call. Options are:
1055
+
1056
+ - str of the form ``"<<tool_name>>"``: calls <<tool_name>> tool.
1057
+ - ``"auto"``: automatically selects a tool (including no tool).
1058
+ - ``"none"``: does not call a tool.
1059
+ - ``"any"`` or ``"required"`` or ``True``: force at least one tool to be called.
1060
+ - dict of the form ``{"type": "function", "function": {"name": <<tool_name>>}}``: calls <<tool_name>> tool.
1061
+ - ``False`` or ``None``: no effect, default OpenAI behavior.
1024
1062
  strict: If True, model output is guaranteed to exactly match the JSON Schema
1025
1063
  provided in the tool definition. If True, the input schema will be
1026
1064
  validated according to
@@ -1028,11 +1066,13 @@ class BaseChatOpenAI(BaseChatModel):
1028
1066
  If False, input schema will not be validated and model output will not
1029
1067
  be validated.
1030
1068
  If None, ``strict`` argument will not be passed to the model.
1069
+ kwargs: Any additional parameters are passed directly to
1070
+ :meth:`~langchain_openai.chat_models.base.ChatOpenAI.bind`.
1031
1071
 
1032
- .. versionadded:: 0.1.21
1072
+ .. versionchanged:: 0.1.21
1073
+
1074
+ Support for ``strict`` argument added.
1033
1075
 
1034
- kwargs: Any additional parameters are passed directly to
1035
- ``self.bind(**kwargs)``.
1036
1076
  """ # noqa: E501
1037
1077
 
1038
1078
  formatted_tools = [
@@ -1168,12 +1208,12 @@ class BaseChatOpenAI(BaseChatModel):
1168
1208
  Support for ``strict`` argument added.
1169
1209
  Support for ``method`` = "json_schema" added.
1170
1210
 
1171
- .. note:: Planned breaking changes in version `0.2.0`
1211
+ .. note:: Planned breaking changes in version `0.3.0`
1172
1212
 
1173
1213
  - ``method`` default will be changed to "json_schema" from
1174
1214
  "function_calling".
1175
1215
  - ``strict`` will default to True when ``method`` is
1176
- "function_calling" as of version `0.2.0`.
1216
+ "function_calling" as of version `0.3.0`.
1177
1217
 
1178
1218
 
1179
1219
  .. dropdown:: Example: schema=Pydantic class, method="function_calling", include_raw=False, strict=True
@@ -1369,14 +1409,13 @@ class BaseChatOpenAI(BaseChatModel):
1369
1409
  "Received None."
1370
1410
  )
1371
1411
  tool_name = convert_to_openai_tool(schema)["function"]["name"]
1372
- llm = self.bind_tools(
1373
- [schema],
1374
- tool_choice=tool_name,
1375
- parallel_tool_calls=False,
1376
- strict=strict,
1412
+ bind_kwargs = self._filter_disabled_params(
1413
+ tool_choice=tool_name, parallel_tool_calls=False, strict=strict
1377
1414
  )
1415
+
1416
+ llm = self.bind_tools([schema], **bind_kwargs)
1378
1417
  if is_pydantic_schema:
1379
- output_parser: OutputParserLike = PydanticToolsParser(
1418
+ output_parser: Runnable = PydanticToolsParser(
1380
1419
  tools=[schema], # type: ignore[list-item]
1381
1420
  first_tool_only=True, # type: ignore[list-item]
1382
1421
  )
@@ -1400,11 +1439,12 @@ class BaseChatOpenAI(BaseChatModel):
1400
1439
  strict = strict if strict is not None else True
1401
1440
  response_format = _convert_to_openai_response_format(schema, strict=strict)
1402
1441
  llm = self.bind(response_format=response_format)
1403
- output_parser = (
1404
- cast(Runnable, _oai_structured_outputs_parser)
1405
- if is_pydantic_schema
1406
- else JsonOutputParser()
1407
- )
1442
+ if is_pydantic_schema:
1443
+ output_parser = _oai_structured_outputs_parser.with_types(
1444
+ output_type=cast(type, schema)
1445
+ )
1446
+ else:
1447
+ output_parser = JsonOutputParser()
1408
1448
  else:
1409
1449
  raise ValueError(
1410
1450
  f"Unrecognized method argument. Expected one of 'function_calling' or "
@@ -1423,6 +1463,21 @@ class BaseChatOpenAI(BaseChatModel):
1423
1463
  else:
1424
1464
  return llm | output_parser
1425
1465
 
1466
+ def _filter_disabled_params(self, **kwargs: Any) -> Dict[str, Any]:
1467
+ if not self.disabled_params:
1468
+ return kwargs
1469
+ filtered = {}
1470
+ for k, v in kwargs.items():
1471
+ # Skip param
1472
+ if k in self.disabled_params and (
1473
+ self.disabled_params[k] is None or v in self.disabled_params[k]
1474
+ ):
1475
+ continue
1476
+ # Keep param
1477
+ else:
1478
+ filtered[k] = v
1479
+ return filtered
1480
+
1426
1481
 
1427
1482
  class ChatOpenAI(BaseChatOpenAI):
1428
1483
  """OpenAI chat model integration.
@@ -2081,7 +2136,7 @@ def _oai_structured_outputs_parser(ai_msg: AIMessage) -> PydanticBaseModel:
2081
2136
  else:
2082
2137
  raise ValueError(
2083
2138
  "Structured Output response does not have a 'parsed' field nor a 'refusal' "
2084
- "field."
2139
+ "field. Received message:\n\n{ai_msg}"
2085
2140
  )
2086
2141
 
2087
2142
 
@@ -2096,3 +2151,36 @@ class OpenAIRefusalError(Exception):
2096
2151
 
2097
2152
  .. versionadded:: 0.1.21
2098
2153
  """
2154
+
2155
+
2156
+ def _create_usage_metadata(oai_token_usage: dict) -> UsageMetadata:
2157
+ input_tokens = oai_token_usage.get("prompt_tokens", 0)
2158
+ output_tokens = oai_token_usage.get("completion_tokens", 0)
2159
+ total_tokens = oai_token_usage.get("total_tokens", input_tokens + output_tokens)
2160
+ input_token_details: dict = {
2161
+ "audio": (oai_token_usage.get("prompt_tokens_details") or {}).get(
2162
+ "audio_tokens"
2163
+ ),
2164
+ "cache_read": (oai_token_usage.get("prompt_tokens_details") or {}).get(
2165
+ "cached_tokens"
2166
+ ),
2167
+ }
2168
+ output_token_details: dict = {
2169
+ "audio": (oai_token_usage.get("completion_tokens_details") or {}).get(
2170
+ "audio_tokens"
2171
+ ),
2172
+ "reasoning": (oai_token_usage.get("completion_tokens_details") or {}).get(
2173
+ "reasoning_tokens"
2174
+ ),
2175
+ }
2176
+ return UsageMetadata(
2177
+ input_tokens=input_tokens,
2178
+ output_tokens=output_tokens,
2179
+ total_tokens=total_tokens,
2180
+ input_token_details=InputTokenDetails(
2181
+ **{k: v for k, v in input_token_details.items() if v is not None}
2182
+ ),
2183
+ output_token_details=OutputTokenDetails(
2184
+ **{k: v for k, v in output_token_details.items() if v is not None}
2185
+ ),
2186
+ )
@@ -254,14 +254,14 @@ class OpenAIEmbeddings(BaseModel, Embeddings):
254
254
  retry_max_seconds: int = 20
255
255
  """Max number of seconds to wait between retries"""
256
256
  http_client: Union[Any, None] = None
257
- """Optional httpx.Client. Only used for sync invocations. Must specify
257
+ """Optional httpx.Client. Only used for sync invocations. Must specify
258
258
  http_async_client as well if you'd like a custom client for async invocations.
259
259
  """
260
260
  http_async_client: Union[Any, None] = None
261
- """Optional httpx.AsyncClient. Only used for async invocations. Must specify
261
+ """Optional httpx.AsyncClient. Only used for async invocations. Must specify
262
262
  http_client as well if you'd like a custom client for sync invocations."""
263
263
  check_embedding_ctx_length: bool = True
264
- """Whether to check the token length of inputs and automatically split inputs
264
+ """Whether to check the token length of inputs and automatically split inputs
265
265
  longer than embedding_ctx_length."""
266
266
 
267
267
  model_config = ConfigDict(
@@ -558,7 +558,7 @@ class OpenAIEmbeddings(BaseModel, Embeddings):
558
558
  return [e if e is not None else await empty_embedding() for e in embeddings]
559
559
 
560
560
  def embed_documents(
561
- self, texts: List[str], chunk_size: Optional[int] = 0
561
+ self, texts: List[str], chunk_size: int | None = None
562
562
  ) -> List[List[float]]:
563
563
  """Call out to OpenAI's embedding endpoint for embedding search docs.
564
564
 
@@ -570,10 +570,13 @@ class OpenAIEmbeddings(BaseModel, Embeddings):
570
570
  Returns:
571
571
  List of embeddings, one for each text.
572
572
  """
573
+ chunk_size_ = chunk_size or self.chunk_size
573
574
  if not self.check_embedding_ctx_length:
574
575
  embeddings: List[List[float]] = []
575
- for text in texts:
576
- response = self.client.create(input=text, **self._invocation_params)
576
+ for i in range(0, len(texts), self.chunk_size):
577
+ response = self.client.create(
578
+ input=texts[i : i + chunk_size_], **self._invocation_params
579
+ )
577
580
  if not isinstance(response, dict):
578
581
  response = response.dict()
579
582
  embeddings.extend(r["embedding"] for r in response["data"])
@@ -585,7 +588,7 @@ class OpenAIEmbeddings(BaseModel, Embeddings):
585
588
  return self._get_len_safe_embeddings(texts, engine=engine)
586
589
 
587
590
  async def aembed_documents(
588
- self, texts: List[str], chunk_size: Optional[int] = 0
591
+ self, texts: List[str], chunk_size: int | None = None
589
592
  ) -> List[List[float]]:
590
593
  """Call out to OpenAI's embedding endpoint async for embedding search docs.
591
594
 
@@ -597,11 +600,12 @@ class OpenAIEmbeddings(BaseModel, Embeddings):
597
600
  Returns:
598
601
  List of embeddings, one for each text.
599
602
  """
603
+ chunk_size_ = chunk_size or self.chunk_size
600
604
  if not self.check_embedding_ctx_length:
601
605
  embeddings: List[List[float]] = []
602
- for text in texts:
606
+ for i in range(0, len(texts), chunk_size_):
603
607
  response = await self.async_client.create(
604
- input=text, **self._invocation_params
608
+ input=texts[i : i + chunk_size_], **self._invocation_params
605
609
  )
606
610
  if not isinstance(response, dict):
607
611
  response = response.dict()
@@ -27,7 +27,7 @@ from langchain_core.callbacks import (
27
27
  from langchain_core.language_models.llms import BaseLLM
28
28
  from langchain_core.outputs import Generation, GenerationChunk, LLMResult
29
29
  from langchain_core.utils import get_pydantic_field_names
30
- from langchain_core.utils.utils import build_extra_kwargs, from_env, secret_from_env
30
+ from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_from_env
31
31
  from pydantic import ConfigDict, Field, SecretStr, model_validator
32
32
  from typing_extensions import Self
33
33
 
@@ -160,10 +160,7 @@ class BaseOpenAI(BaseLLM):
160
160
  def build_extra(cls, values: Dict[str, Any]) -> Any:
161
161
  """Build extra kwargs from additional params that were passed in."""
162
162
  all_required_field_names = get_pydantic_field_names(cls)
163
- extra = values.get("model_kwargs", {})
164
- values["model_kwargs"] = build_extra_kwargs(
165
- extra, values, all_required_field_names
166
- )
163
+ values = _build_model_kwargs(values, all_required_field_names)
167
164
  return values
168
165
 
169
166
  @model_validator(mode="after")
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "langchain-openai"
7
- version = "0.2.0.dev2"
7
+ version = "0.2.2"
8
8
  description = "An integration package connecting OpenAI and LangChain"
9
9
  authors = []
10
10
  readme = "README.md"
@@ -23,7 +23,7 @@ ignore_missing_imports = true
23
23
 
24
24
  [tool.poetry.dependencies]
25
25
  python = ">=3.9,<4.0"
26
- langchain-core = { version = "^0.3.0.dev1", allow-prereleases = true }
26
+ langchain-core = "^0.3.9"
27
27
  openai = "^1.40.0"
28
28
  tiktoken = ">=0.7,<1"
29
29
 
@@ -41,6 +41,7 @@ omit = [ "tests/*",]
41
41
  addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5 --cov=langchain_openai"
42
42
  markers = [ "requires: mark tests as requiring a specific library", "asyncio: mark tests as requiring asyncio", "compile: mark placeholder test used to compile integration tests without running them", "scheduled: mark tests to run in scheduled testing",]
43
43
  asyncio_mode = "auto"
44
+ filterwarnings = [ "ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",]
44
45
 
45
46
  [tool.poetry.group.test]
46
47
  optional = true