langchain-openai 0.2.0.dev2__tar.gz → 0.2.1__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.1}/PKG-INFO +2 -2
  2. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/chat_models/azure.py +33 -331
  3. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/chat_models/base.py +94 -30
  4. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/embeddings/base.py +13 -9
  5. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/pyproject.toml +3 -2
  6. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/LICENSE +0 -0
  7. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/README.md +0 -0
  8. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/__init__.py +0 -0
  9. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/chat_models/__init__.py +0 -0
  10. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/embeddings/__init__.py +0 -0
  11. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/embeddings/azure.py +0 -0
  12. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/llms/__init__.py +0 -0
  13. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/llms/azure.py +0 -0
  14. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/llms/base.py +0 -0
  15. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/output_parsers/__init__.py +0 -0
  16. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/langchain_openai/output_parsers/tools.py +0 -0
  17. {langchain_openai-0.2.0.dev2 → langchain_openai-0.2.1}/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.1
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,<0.4)
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,
@@ -65,7 +66,6 @@ from langchain_core.messages import (
65
66
  from langchain_core.messages.ai import UsageMetadata
66
67
  from langchain_core.messages.tool import tool_call_chunk
67
68
  from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
68
- from langchain_core.output_parsers.base import OutputParserLike
69
69
  from langchain_core.output_parsers.openai_tools import (
70
70
  JsonOutputKeyToolsParser,
71
71
  PydanticToolsParser,
@@ -332,6 +332,33 @@ def _convert_chunk_to_generation_chunk(
332
332
  return generation_chunk
333
333
 
334
334
 
335
+ def _update_token_usage(
336
+ overall_token_usage: Union[int, dict], new_usage: Union[int, dict]
337
+ ) -> Union[int, dict]:
338
+ # Token usage is either ints or dictionaries
339
+ # `reasoning_tokens` is nested inside `completion_tokens_details`
340
+ if isinstance(new_usage, int):
341
+ if not isinstance(overall_token_usage, int):
342
+ raise ValueError(
343
+ f"Got different types for token usage: "
344
+ f"{type(new_usage)} and {type(overall_token_usage)}"
345
+ )
346
+ return new_usage + overall_token_usage
347
+ elif isinstance(new_usage, dict):
348
+ if not isinstance(overall_token_usage, dict):
349
+ raise ValueError(
350
+ f"Got different types for token usage: "
351
+ f"{type(new_usage)} and {type(overall_token_usage)}"
352
+ )
353
+ return {
354
+ k: _update_token_usage(overall_token_usage.get(k, 0), v)
355
+ for k, v in new_usage.items()
356
+ }
357
+ else:
358
+ warnings.warn(f"Unexpected type for token usage: {type(new_usage)}")
359
+ return new_usage
360
+
361
+
335
362
  class _FunctionCall(TypedDict):
336
363
  name: str
337
364
 
@@ -427,6 +454,23 @@ class BaseChatOpenAI(BaseChatModel):
427
454
  making requests to OpenAI compatible APIs, such as vLLM."""
428
455
  include_response_headers: bool = False
429
456
  """Whether to include response headers in the output message response_metadata."""
457
+ disabled_params: Optional[Dict[str, Any]] = Field(default=None)
458
+ """Parameters of the OpenAI client or chat.completions endpoint that should be
459
+ disabled for the given model.
460
+
461
+ Should be specified as ``{"param": None | ['val1', 'val2']}`` where the key is the
462
+ parameter and the value is either None, meaning that parameter should never be
463
+ used, or it's a list of disabled values for the parameter.
464
+
465
+ For example, older models may not support the 'parallel_tool_calls' parameter at
466
+ all, in which case ``disabled_params={"parallel_tool_calls: None}`` can ben passed
467
+ in.
468
+
469
+ If a parameter is disabled then it will not be used by default in any methods, e.g.
470
+ in :meth:`~langchain_openai.chat_models.base.ChatOpenAI.with_structured_output`.
471
+ However this does not prevent a user from directly passed in the parameter during
472
+ invocation.
473
+ """
430
474
 
431
475
  model_config = ConfigDict(populate_by_name=True)
432
476
 
@@ -545,7 +589,9 @@ class BaseChatOpenAI(BaseChatModel):
545
589
  if token_usage is not None:
546
590
  for k, v in token_usage.items():
547
591
  if k in overall_token_usage:
548
- overall_token_usage[k] += v
592
+ overall_token_usage[k] = _update_token_usage(
593
+ overall_token_usage[k], v
594
+ )
549
595
  else:
550
596
  overall_token_usage[k] = v
551
597
  if system_fingerprint is None:
@@ -741,7 +787,7 @@ class BaseChatOpenAI(BaseChatModel):
741
787
  )
742
788
  return
743
789
  if self.include_response_headers:
744
- raw_response = self.async_client.with_raw_response.create(**payload)
790
+ raw_response = await self.async_client.with_raw_response.create(**payload)
745
791
  response = raw_response.parse()
746
792
  base_generation_info = {"headers": dict(raw_response.headers)}
747
793
  else:
@@ -938,6 +984,11 @@ class BaseChatOpenAI(BaseChatModel):
938
984
  num_tokens += 3
939
985
  return num_tokens
940
986
 
987
+ @deprecated(
988
+ since="0.2.1",
989
+ alternative="langchain_openai.chat_models.base.ChatOpenAI.bind_tools",
990
+ removal="0.3.0",
991
+ )
941
992
  def bind_functions(
942
993
  self,
943
994
  functions: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
@@ -1005,22 +1056,18 @@ class BaseChatOpenAI(BaseChatModel):
1005
1056
 
1006
1057
  Assumes model is compatible with OpenAI tool-calling API.
1007
1058
 
1008
- .. versionchanged:: 0.1.21
1009
-
1010
- Support for ``strict`` argument added.
1011
-
1012
1059
  Args:
1013
1060
  tools: A list of tool definitions to bind to this chat model.
1014
1061
  Supports any tool definition handled by
1015
1062
  :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.
1063
+ tool_choice: Which tool to require the model to call. Options are:
1064
+
1065
+ - str of the form ``"<<tool_name>>"``: calls <<tool_name>> tool.
1066
+ - ``"auto"``: automatically selects a tool (including no tool).
1067
+ - ``"none"``: does not call a tool.
1068
+ - ``"any"`` or ``"required"`` or ``True``: force at least one tool to be called.
1069
+ - dict of the form ``{"type": "function", "function": {"name": <<tool_name>>}}``: calls <<tool_name>> tool.
1070
+ - ``False`` or ``None``: no effect, default OpenAI behavior.
1024
1071
  strict: If True, model output is guaranteed to exactly match the JSON Schema
1025
1072
  provided in the tool definition. If True, the input schema will be
1026
1073
  validated according to
@@ -1028,11 +1075,13 @@ class BaseChatOpenAI(BaseChatModel):
1028
1075
  If False, input schema will not be validated and model output will not
1029
1076
  be validated.
1030
1077
  If None, ``strict`` argument will not be passed to the model.
1078
+ kwargs: Any additional parameters are passed directly to
1079
+ :meth:`~langchain_openai.chat_models.base.ChatOpenAI.bind`.
1031
1080
 
1032
- .. versionadded:: 0.1.21
1081
+ .. versionchanged:: 0.1.21
1082
+
1083
+ Support for ``strict`` argument added.
1033
1084
 
1034
- kwargs: Any additional parameters are passed directly to
1035
- ``self.bind(**kwargs)``.
1036
1085
  """ # noqa: E501
1037
1086
 
1038
1087
  formatted_tools = [
@@ -1369,14 +1418,13 @@ class BaseChatOpenAI(BaseChatModel):
1369
1418
  "Received None."
1370
1419
  )
1371
1420
  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,
1421
+ bind_kwargs = self._filter_disabled_params(
1422
+ tool_choice=tool_name, parallel_tool_calls=False, strict=strict
1377
1423
  )
1424
+
1425
+ llm = self.bind_tools([schema], **bind_kwargs)
1378
1426
  if is_pydantic_schema:
1379
- output_parser: OutputParserLike = PydanticToolsParser(
1427
+ output_parser: Runnable = PydanticToolsParser(
1380
1428
  tools=[schema], # type: ignore[list-item]
1381
1429
  first_tool_only=True, # type: ignore[list-item]
1382
1430
  )
@@ -1400,11 +1448,12 @@ class BaseChatOpenAI(BaseChatModel):
1400
1448
  strict = strict if strict is not None else True
1401
1449
  response_format = _convert_to_openai_response_format(schema, strict=strict)
1402
1450
  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
- )
1451
+ if is_pydantic_schema:
1452
+ output_parser = _oai_structured_outputs_parser.with_types(
1453
+ output_type=cast(type, schema)
1454
+ )
1455
+ else:
1456
+ output_parser = JsonOutputParser()
1408
1457
  else:
1409
1458
  raise ValueError(
1410
1459
  f"Unrecognized method argument. Expected one of 'function_calling' or "
@@ -1423,6 +1472,21 @@ class BaseChatOpenAI(BaseChatModel):
1423
1472
  else:
1424
1473
  return llm | output_parser
1425
1474
 
1475
+ def _filter_disabled_params(self, **kwargs: Any) -> Dict[str, Any]:
1476
+ if not self.disabled_params:
1477
+ return kwargs
1478
+ filtered = {}
1479
+ for k, v in kwargs.items():
1480
+ # Skip param
1481
+ if k in self.disabled_params and (
1482
+ self.disabled_params[k] is None or v in self.disabled_params[k]
1483
+ ):
1484
+ continue
1485
+ # Keep param
1486
+ else:
1487
+ filtered[k] = v
1488
+ return filtered
1489
+
1426
1490
 
1427
1491
  class ChatOpenAI(BaseChatOpenAI):
1428
1492
  """OpenAI chat model integration.
@@ -2081,7 +2145,7 @@ def _oai_structured_outputs_parser(ai_msg: AIMessage) -> PydanticBaseModel:
2081
2145
  else:
2082
2146
  raise ValueError(
2083
2147
  "Structured Output response does not have a 'parsed' field nor a 'refusal' "
2084
- "field."
2148
+ "field. Received message:\n\n{ai_msg}"
2085
2149
  )
2086
2150
 
2087
2151
 
@@ -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()
@@ -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.1"
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"
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