from__future__importannotationsimportloggingimportsysfromtypingimport(AbstractSet,Any,AsyncIterator,Collection,Dict,Iterator,List,Literal,Mapping,Optional,Set,Tuple,Union,)importopenaiimporttiktokenfromlangchain_core.callbacksimport(AsyncCallbackManagerForLLMRun,CallbackManagerForLLMRun,)fromlangchain_core.language_models.llmsimportBaseLLMfromlangchain_core.outputsimportGeneration,GenerationChunk,LLMResultfromlangchain_core.utilsimportget_pydantic_field_namesfromlangchain_core.utils.utilsimport_build_model_kwargs,from_env,secret_from_envfrompydanticimportConfigDict,Field,SecretStr,model_validatorfromtyping_extensionsimportSelflogger=logging.getLogger(__name__)def_update_token_usage(keys:Set[str],response:Dict[str,Any],token_usage:Dict[str,Any])->None:"""Update token usage."""_keys_to_use=keys.intersection(response["usage"])for_keyin_keys_to_use:if_keynotintoken_usage:token_usage[_key]=response["usage"][_key]else:token_usage[_key]+=response["usage"][_key]def_stream_response_to_generation_chunk(stream_response:Dict[str,Any],)->GenerationChunk:"""Convert a stream response to a generation chunk."""ifnotstream_response["choices"]:returnGenerationChunk(text="")returnGenerationChunk(text=stream_response["choices"][0]["text"],generation_info=dict(finish_reason=stream_response["choices"][0].get("finish_reason",None),logprobs=stream_response["choices"][0].get("logprobs",None),),)
[docs]classBaseOpenAI(BaseLLM):"""Base OpenAI large language model class."""client:Any=Field(default=None,exclude=True)#: :meta private:async_client:Any=Field(default=None,exclude=True)#: :meta private:model_name:str=Field(default="gpt-3.5-turbo-instruct",alias="model")"""Model name to use."""temperature:float=0.7"""What sampling temperature to use."""max_tokens:int=256"""The maximum number of tokens to generate in the completion. -1 returns as many tokens as possible given the prompt and the models maximal context size."""top_p:float=1"""Total probability mass of tokens to consider at each step."""frequency_penalty:float=0"""Penalizes repeated tokens according to frequency."""presence_penalty:float=0"""Penalizes repeated tokens."""n:int=1"""How many completions to generate for each prompt."""best_of:int=1"""Generates best_of completions server-side and returns the "best"."""model_kwargs:Dict[str,Any]=Field(default_factory=dict)"""Holds any model parameters valid for `create` call not explicitly specified."""openai_api_key:Optional[SecretStr]=Field(alias="api_key",default_factory=secret_from_env("OPENAI_API_KEY",default=None))"""Automatically inferred from env var `OPENAI_API_KEY` if not provided."""openai_api_base:Optional[str]=Field(alias="base_url",default_factory=from_env("OPENAI_API_BASE",default=None))"""Base URL path for API requests, leave blank if not using a proxy or service emulator."""openai_organization:Optional[str]=Field(alias="organization",default_factory=from_env(["OPENAI_ORG_ID","OPENAI_ORGANIZATION"],default=None),)"""Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""# to support explicit proxy for OpenAIopenai_proxy:Optional[str]=Field(default_factory=from_env("OPENAI_PROXY",default=None))batch_size:int=20"""Batch size to use when passing multiple documents to generate."""request_timeout:Union[float,Tuple[float,float],Any,None]=Field(default=None,alias="timeout")"""Timeout for requests to OpenAI completion API. Can be float, httpx.Timeout or None."""logit_bias:Optional[Dict[str,float]]=None"""Adjust the probability of specific tokens being generated."""max_retries:int=2"""Maximum number of retries to make when generating."""seed:Optional[int]=None"""Seed for generation"""logprobs:Optional[int]=None"""Include the log probabilities on the logprobs most likely output tokens, as well the chosen tokens."""streaming:bool=False"""Whether to stream the results or not."""allowed_special:Union[Literal["all"],AbstractSet[str]]=set()"""Set of special tokens that are allowedใ"""disallowed_special:Union[Literal["all"],Collection[str]]="all""""Set of special tokens that are not allowedใ"""tiktoken_model_name:Optional[str]=None"""The model name to pass to tiktoken when using this class. Tiktoken is used to count the number of tokens in documents to constrain them to be under a certain limit. By default, when set to None, this will be the same as the embedding model name. However, there are some cases where you may want to use this Embedding class with a model name not supported by tiktoken. This can include when using Azure embeddings or when using one of the many model providers that expose an OpenAI-like API but with different models. In those cases, in order to avoid erroring when tiktoken is called, you can specify a model name to use here."""default_headers:Union[Mapping[str,str],None]=Nonedefault_query:Union[Mapping[str,object],None]=None# Configure a custom httpx client. See the# [httpx documentation](https://www.python-httpx.org/api/#client) for more details.http_client:Union[Any,None]=None"""Optional httpx.Client. Only used for sync invocations. Must specify http_async_client as well if you'd like a custom client for async invocations. """http_async_client:Union[Any,None]=None"""Optional httpx.AsyncClient. Only used for async invocations. Must specify http_client as well if you'd like a custom client for sync invocations."""extra_body:Optional[Mapping[str,Any]]=None"""Optional additional JSON properties to include in the request parameters when making requests to OpenAI compatible APIs, such as vLLM."""model_config=ConfigDict(populate_by_name=True)@model_validator(mode="before")@classmethoddefbuild_extra(cls,values:Dict[str,Any])->Any:"""Build extra kwargs from additional params that were passed in."""all_required_field_names=get_pydantic_field_names(cls)values=_build_model_kwargs(values,all_required_field_names)returnvalues@model_validator(mode="after")defvalidate_environment(self)->Self:"""Validate that api key and python package exists in environment."""ifself.n<1:raiseValueError("n must be at least 1.")ifself.streamingandself.n>1:raiseValueError("Cannot stream results when n > 1.")ifself.streamingandself.best_of>1:raiseValueError("Cannot stream results when best_of > 1.")client_params:dict={"api_key":(self.openai_api_key.get_secret_value()ifself.openai_api_keyelseNone),"organization":self.openai_organization,"base_url":self.openai_api_base,"timeout":self.request_timeout,"max_retries":self.max_retries,"default_headers":self.default_headers,"default_query":self.default_query,}ifnotself.client:sync_specific={"http_client":self.http_client}self.client=openai.OpenAI(**client_params,**sync_specific).completions# type: ignore[arg-type]ifnotself.async_client:async_specific={"http_client":self.http_async_client}self.async_client=openai.AsyncOpenAI(**client_params,**async_specific,# type: ignore[arg-type]).completionsreturnself@propertydef_default_params(self)->Dict[str,Any]:"""Get the default parameters for calling OpenAI API."""normal_params:Dict[str,Any]={"temperature":self.temperature,"top_p":self.top_p,"frequency_penalty":self.frequency_penalty,"presence_penalty":self.presence_penalty,"n":self.n,"seed":self.seed,"logprobs":self.logprobs,}ifself.logit_biasisnotNone:normal_params["logit_bias"]=self.logit_biasifself.max_tokensisnotNone:normal_params["max_tokens"]=self.max_tokensifself.extra_bodyisnotNone:normal_params["extra_body"]=self.extra_body# Azure gpt-35-turbo doesn't support best_of# don't specify best_of if it is 1ifself.best_of>1:normal_params["best_of"]=self.best_ofreturn{**normal_params,**self.model_kwargs}def_stream(self,prompt:str,stop:Optional[List[str]]=None,run_manager:Optional[CallbackManagerForLLMRun]=None,**kwargs:Any,)->Iterator[GenerationChunk]:params={**self._invocation_params,**kwargs,"stream":True}self.get_sub_prompts(params,[prompt],stop)# this mutates paramsforstream_respinself.client.create(prompt=prompt,**params):ifnotisinstance(stream_resp,dict):stream_resp=stream_resp.model_dump()chunk=_stream_response_to_generation_chunk(stream_resp)ifrun_manager:run_manager.on_llm_new_token(chunk.text,chunk=chunk,verbose=self.verbose,logprobs=(chunk.generation_info["logprobs"]ifchunk.generation_infoelseNone),)yieldchunkasyncdef_astream(self,prompt:str,stop:Optional[List[str]]=None,run_manager:Optional[AsyncCallbackManagerForLLMRun]=None,**kwargs:Any,)->AsyncIterator[GenerationChunk]:params={**self._invocation_params,**kwargs,"stream":True}self.get_sub_prompts(params,[prompt],stop)# this mutates paramsasyncforstream_respinawaitself.async_client.create(prompt=prompt,**params):ifnotisinstance(stream_resp,dict):stream_resp=stream_resp.model_dump()chunk=_stream_response_to_generation_chunk(stream_resp)ifrun_manager:awaitrun_manager.on_llm_new_token(chunk.text,chunk=chunk,verbose=self.verbose,logprobs=(chunk.generation_info["logprobs"]ifchunk.generation_infoelseNone),)yieldchunkdef_generate(self,prompts:List[str],stop:Optional[List[str]]=None,run_manager:Optional[CallbackManagerForLLMRun]=None,**kwargs:Any,)->LLMResult:"""Call out to OpenAI's endpoint with k unique prompts. Args: prompts: The prompts to pass into the model. stop: Optional list of stop words to use when generating. Returns: The full LLM output. Example: .. code-block:: python response = openai.generate(["Tell me a joke."]) """# TODO: write a unit test for thisparams=self._invocation_paramsparams={**params,**kwargs}sub_prompts=self.get_sub_prompts(params,prompts,stop)choices=[]token_usage:Dict[str,int]={}# Get the token usage from the response.# Includes prompt, completion, and total tokens used._keys={"completion_tokens","prompt_tokens","total_tokens"}system_fingerprint:Optional[str]=Nonefor_promptsinsub_prompts:ifself.streaming:iflen(_prompts)>1:raiseValueError("Cannot stream results with multiple prompts.")generation:Optional[GenerationChunk]=Noneforchunkinself._stream(_prompts[0],stop,run_manager,**kwargs):ifgenerationisNone:generation=chunkelse:generation+=chunkassertgenerationisnotNonechoices.append({"text":generation.text,"finish_reason":(generation.generation_info.get("finish_reason")ifgeneration.generation_infoelseNone),"logprobs":(generation.generation_info.get("logprobs")ifgeneration.generation_infoelseNone),})else:response=self.client.create(prompt=_prompts,**params)ifnotisinstance(response,dict):# V1 client returns the response in an PyDantic object instead of# dict. For the transition period, we deep convert it to dict.response=response.model_dump()# Sometimes the AI Model calling will get error, we should raise it.# Otherwise, the next code 'choices.extend(response["choices"])'# will throw a "TypeError: 'NoneType' object is not iterable" error# to mask the true error. Because 'response["choices"]' is None.ifresponse.get("error"):raiseValueError(response.get("error"))choices.extend(response["choices"])_update_token_usage(_keys,response,token_usage)ifnotsystem_fingerprint:system_fingerprint=response.get("system_fingerprint")returnself.create_llm_result(choices,prompts,params,token_usage,system_fingerprint=system_fingerprint)asyncdef_agenerate(self,prompts:List[str],stop:Optional[List[str]]=None,run_manager:Optional[AsyncCallbackManagerForLLMRun]=None,**kwargs:Any,)->LLMResult:"""Call out to OpenAI's endpoint async with k unique prompts."""params=self._invocation_paramsparams={**params,**kwargs}sub_prompts=self.get_sub_prompts(params,prompts,stop)choices=[]token_usage:Dict[str,int]={}# Get the token usage from the response.# Includes prompt, completion, and total tokens used._keys={"completion_tokens","prompt_tokens","total_tokens"}system_fingerprint:Optional[str]=Nonefor_promptsinsub_prompts:ifself.streaming:iflen(_prompts)>1:raiseValueError("Cannot stream results with multiple prompts.")generation:Optional[GenerationChunk]=Noneasyncforchunkinself._astream(_prompts[0],stop,run_manager,**kwargs):ifgenerationisNone:generation=chunkelse:generation+=chunkassertgenerationisnotNonechoices.append({"text":generation.text,"finish_reason":(generation.generation_info.get("finish_reason")ifgeneration.generation_infoelseNone),"logprobs":(generation.generation_info.get("logprobs")ifgeneration.generation_infoelseNone),})else:response=awaitself.async_client.create(prompt=_prompts,**params)ifnotisinstance(response,dict):response=response.model_dump()choices.extend(response["choices"])_update_token_usage(_keys,response,token_usage)returnself.create_llm_result(choices,prompts,params,token_usage,system_fingerprint=system_fingerprint)
[docs]defget_sub_prompts(self,params:Dict[str,Any],prompts:List[str],stop:Optional[List[str]]=None,)->List[List[str]]:"""Get the sub prompts for llm call."""ifstopisnotNone:params["stop"]=stopifparams["max_tokens"]==-1:iflen(prompts)!=1:raiseValueError("max_tokens set to -1 not supported for multiple inputs.")params["max_tokens"]=self.max_tokens_for_prompt(prompts[0])sub_prompts=[prompts[i:i+self.batch_size]foriinrange(0,len(prompts),self.batch_size)]returnsub_prompts
[docs]defcreate_llm_result(self,choices:Any,prompts:List[str],params:Dict[str,Any],token_usage:Dict[str,int],*,system_fingerprint:Optional[str]=None,)->LLMResult:"""Create the LLMResult from the choices and prompts."""generations=[]n=params.get("n",self.n)fori,_inenumerate(prompts):sub_choices=choices[i*n:(i+1)*n]generations.append([Generation(text=choice["text"],generation_info=dict(finish_reason=choice.get("finish_reason"),logprobs=choice.get("logprobs"),),)forchoiceinsub_choices])llm_output={"token_usage":token_usage,"model_name":self.model_name}ifsystem_fingerprint:llm_output["system_fingerprint"]=system_fingerprintreturnLLMResult(generations=generations,llm_output=llm_output)
@propertydef_invocation_params(self)->Dict[str,Any]:"""Get the parameters used to invoke the model."""returnself._default_params@propertydef_identifying_params(self)->Mapping[str,Any]:"""Get the identifying parameters."""return{**{"model_name":self.model_name},**self._default_params}@propertydef_llm_type(self)->str:"""Return type of llm."""return"openai"
[docs]defget_token_ids(self,text:str)->List[int]:"""Get the token IDs using the tiktoken package."""ifself.custom_get_token_idsisnotNone:returnself.custom_get_token_ids(text)# tiktoken NOT supported for Python < 3.8ifsys.version_info[1]<8:returnsuper().get_num_tokens(text)model_name=self.tiktoken_model_nameorself.model_nametry:enc=tiktoken.encoding_for_model(model_name)exceptKeyError:enc=tiktoken.get_encoding("cl100k_base")returnenc.encode(text,allowed_special=self.allowed_special,disallowed_special=self.disallowed_special,)
[docs]@staticmethoddefmodelname_to_contextsize(modelname:str)->int:"""Calculate the maximum number of tokens possible to generate for a model. Args: modelname: The modelname we want to know the context size for. Returns: The maximum context size Example: .. code-block:: python max_tokens = openai.modelname_to_contextsize("gpt-3.5-turbo-instruct") """model_token_mapping={"gpt-4o-mini":128_000,"gpt-4o":128_000,"gpt-4o-2024-05-13":128_000,"gpt-4":8192,"gpt-4-0314":8192,"gpt-4-0613":8192,"gpt-4-32k":32768,"gpt-4-32k-0314":32768,"gpt-4-32k-0613":32768,"gpt-3.5-turbo":4096,"gpt-3.5-turbo-0301":4096,"gpt-3.5-turbo-0613":4096,"gpt-3.5-turbo-16k":16385,"gpt-3.5-turbo-16k-0613":16385,"gpt-3.5-turbo-instruct":4096,"text-ada-001":2049,"ada":2049,"text-babbage-001":2040,"babbage":2049,"text-curie-001":2049,"curie":2049,"davinci":2049,"text-davinci-003":4097,"text-davinci-002":4097,"code-davinci-002":8001,"code-davinci-001":8001,"code-cushman-002":2048,"code-cushman-001":2048,}# handling finetuned modelsif"ft-"inmodelname:modelname=modelname.split(":")[0]context_size=model_token_mapping.get(modelname,None)ifcontext_sizeisNone:raiseValueError(f"Unknown model: {modelname}. Please provide a valid OpenAI model name.""Known models are: "+", ".join(model_token_mapping.keys()))returncontext_size
@propertydefmax_context_size(self)->int:"""Get max context size for this model."""returnself.modelname_to_contextsize(self.model_name)
[docs]defmax_tokens_for_prompt(self,prompt:str)->int:"""Calculate the maximum number of tokens possible to generate for a prompt. Args: prompt: The prompt to pass into the model. Returns: The maximum number of tokens to generate for a prompt. Example: .. code-block:: python max_tokens = openai.max_tokens_for_prompt("Tell me a joke.") """num_tokens=self.get_num_tokens(prompt)returnself.max_context_size-num_tokens
[docs]classOpenAI(BaseOpenAI):"""OpenAI completion model integration. Setup: Install ``langchain-openai`` and set environment variable ``OPENAI_API_KEY``. .. code-block:: bash pip install -U langchain-openai export OPENAI_API_KEY="your-api-key" Key init args โ completion params: model: str Name of OpenAI model to use. temperature: float Sampling temperature. max_tokens: Optional[int] Max number of tokens to generate. logprobs: Optional[bool] Whether to return logprobs. stream_options: Dict Configure streaming outputs, like whether to return token usage when streaming (``{"include_usage": True}``). Key init args โ client params: timeout: Union[float, Tuple[float, float], Any, None] Timeout for requests. max_retries: int Max number of retries. api_key: Optional[str] OpenAI API key. If not passed in will be read from env var OPENAI_API_KEY. base_url: Optional[str] Base URL for API requests. Only specify if using a proxy or service emulator. organization: Optional[str] OpenAI organization ID. If not passed in will be read from env var OPENAI_ORG_ID. See full list of supported init args and their descriptions in the params section. Instantiate: .. code-block:: python from langchain_openai import OpenAI llm = OpenAI( model="gpt-3.5-turbo-instruct", temperature=0, max_retries=2, # api_key="...", # base_url="...", # organization="...", # other params... ) Invoke: .. code-block:: python input_text = "The meaning of life is " llm.invoke(input_text) .. code-block:: none "a philosophical question that has been debated by thinkers and scholars for centuries." Stream: .. code-block:: python for chunk in llm.stream(input_text): print(chunk, end="|") .. code-block:: none a| philosophical| question| that| has| been| debated| by| thinkers| and| scholars| for| centuries|. .. code-block:: python "".join(llm.stream(input_text)) .. code-block:: none "a philosophical question that has been debated by thinkers and scholars for centuries." Async: .. code-block:: python await llm.ainvoke(input_text) # stream: # async for chunk in (await llm.astream(input_text)): # print(chunk) # batch: # await llm.abatch([input_text]) .. code-block:: none "a philosophical question that has been debated by thinkers and scholars for centuries." """# noqa: E501@classmethoddefget_lc_namespace(cls)->List[str]:"""Get the namespace of the langchain object."""return["langchain","llms","openai"]@classmethoddefis_lc_serializable(cls)->bool:"""Return whether this model can be serialized by Langchain."""returnTrue@propertydef_invocation_params(self)->Dict[str,Any]:return{**{"model":self.model_name},**super()._invocation_params}@propertydeflc_secrets(self)->Dict[str,str]:return{"openai_api_key":"OPENAI_API_KEY"}@propertydeflc_attributes(self)->Dict[str,Any]:attributes:Dict[str,Any]={}ifself.openai_api_base:attributes["openai_api_base"]=self.openai_api_baseifself.openai_organization:attributes["openai_organization"]=self.openai_organizationifself.openai_proxy:attributes["openai_proxy"]=self.openai_proxyreturnattributes