openai_chat.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import re
  2. from collections.abc import Generator
  3. from json import dumps, loads
  4. from time import time
  5. # import monkeypatch
  6. from typing import Any, Literal, Optional, Union
  7. from openai import AzureOpenAI, OpenAI
  8. from openai._types import NOT_GIVEN, NotGiven
  9. from openai.resources.chat.completions import Completions
  10. from openai.types import Completion as CompletionMessage
  11. from openai.types.chat import (
  12. ChatCompletion,
  13. ChatCompletionChunk,
  14. ChatCompletionMessageParam,
  15. ChatCompletionMessageToolCall,
  16. ChatCompletionToolChoiceOptionParam,
  17. ChatCompletionToolParam,
  18. completion_create_params,
  19. )
  20. from openai.types.chat.chat_completion import ChatCompletion as _ChatCompletion
  21. from openai.types.chat.chat_completion import Choice as _ChatCompletionChoice
  22. from openai.types.chat.chat_completion_chunk import (
  23. Choice,
  24. ChoiceDelta,
  25. ChoiceDeltaFunctionCall,
  26. ChoiceDeltaToolCall,
  27. ChoiceDeltaToolCallFunction,
  28. )
  29. from openai.types.chat.chat_completion_message import ChatCompletionMessage, FunctionCall
  30. from openai.types.chat.chat_completion_message_tool_call import Function
  31. from openai.types.completion_usage import CompletionUsage
  32. from core.model_runtime.errors.invoke import InvokeAuthorizationError
  33. class MockChatClass:
  34. @staticmethod
  35. def generate_function_call(
  36. functions: list[completion_create_params.Function] | NotGiven = NOT_GIVEN,
  37. ) -> Optional[FunctionCall]:
  38. if not functions or len(functions) == 0:
  39. return None
  40. function: completion_create_params.Function = functions[0]
  41. function_name = function["name"]
  42. function_description = function["description"]
  43. function_parameters = function["parameters"]
  44. function_parameters_type = function_parameters["type"]
  45. if function_parameters_type != "object":
  46. return None
  47. function_parameters_properties = function_parameters["properties"]
  48. function_parameters_required = function_parameters["required"]
  49. parameters = {}
  50. for parameter_name, parameter in function_parameters_properties.items():
  51. if parameter_name not in function_parameters_required:
  52. continue
  53. parameter_type = parameter["type"]
  54. if parameter_type == "string":
  55. if "enum" in parameter:
  56. if len(parameter["enum"]) == 0:
  57. continue
  58. parameters[parameter_name] = parameter["enum"][0]
  59. else:
  60. parameters[parameter_name] = "kawaii"
  61. elif parameter_type == "integer":
  62. parameters[parameter_name] = 114514
  63. elif parameter_type == "number":
  64. parameters[parameter_name] = 1919810.0
  65. elif parameter_type == "boolean":
  66. parameters[parameter_name] = True
  67. return FunctionCall(name=function_name, arguments=dumps(parameters))
  68. @staticmethod
  69. def generate_tool_calls(tools=NOT_GIVEN) -> Optional[list[ChatCompletionMessageToolCall]]:
  70. list_tool_calls = []
  71. if not tools or len(tools) == 0:
  72. return None
  73. tool = tools[0]
  74. if "type" in tools and tools["type"] != "function":
  75. return None
  76. function = tool["function"]
  77. function_call = MockChatClass.generate_function_call(functions=[function])
  78. if function_call is None:
  79. return None
  80. list_tool_calls.append(
  81. ChatCompletionMessageToolCall(
  82. id="sakurajima-mai",
  83. function=Function(
  84. name=function_call.name,
  85. arguments=function_call.arguments,
  86. ),
  87. type="function",
  88. )
  89. )
  90. return list_tool_calls
  91. @staticmethod
  92. def mocked_openai_chat_create_sync(
  93. model: str,
  94. functions: list[completion_create_params.Function] | NotGiven = NOT_GIVEN,
  95. tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
  96. ) -> CompletionMessage:
  97. tool_calls = []
  98. function_call = MockChatClass.generate_function_call(functions=functions)
  99. if not function_call:
  100. tool_calls = MockChatClass.generate_tool_calls(tools=tools)
  101. return _ChatCompletion(
  102. id="cmpl-3QJQa5jXJ5Z5X",
  103. choices=[
  104. _ChatCompletionChoice(
  105. finish_reason="content_filter",
  106. index=0,
  107. message=ChatCompletionMessage(
  108. content="elaina", role="assistant", function_call=function_call, tool_calls=tool_calls
  109. ),
  110. )
  111. ],
  112. created=int(time()),
  113. model=model,
  114. object="chat.completion",
  115. system_fingerprint="",
  116. usage=CompletionUsage(
  117. prompt_tokens=2,
  118. completion_tokens=1,
  119. total_tokens=3,
  120. ),
  121. )
  122. @staticmethod
  123. def mocked_openai_chat_create_stream(
  124. model: str,
  125. functions: list[completion_create_params.Function] | NotGiven = NOT_GIVEN,
  126. tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
  127. ) -> Generator[ChatCompletionChunk, None, None]:
  128. tool_calls = []
  129. function_call = MockChatClass.generate_function_call(functions=functions)
  130. if not function_call:
  131. tool_calls = MockChatClass.generate_tool_calls(tools=tools)
  132. full_text = "Hello, world!\n\n```python\nprint('Hello, world!')\n```"
  133. for i in range(0, len(full_text) + 1):
  134. if i == len(full_text):
  135. yield ChatCompletionChunk(
  136. id="cmpl-3QJQa5jXJ5Z5X",
  137. choices=[
  138. Choice(
  139. delta=ChoiceDelta(
  140. content="",
  141. function_call=ChoiceDeltaFunctionCall(
  142. name=function_call.name,
  143. arguments=function_call.arguments,
  144. )
  145. if function_call
  146. else None,
  147. role="assistant",
  148. tool_calls=[
  149. ChoiceDeltaToolCall(
  150. index=0,
  151. id="misaka-mikoto",
  152. function=ChoiceDeltaToolCallFunction(
  153. name=tool_calls[0].function.name,
  154. arguments=tool_calls[0].function.arguments,
  155. ),
  156. type="function",
  157. )
  158. ]
  159. if tool_calls and len(tool_calls) > 0
  160. else None,
  161. ),
  162. finish_reason="function_call",
  163. index=0,
  164. )
  165. ],
  166. created=int(time()),
  167. model=model,
  168. object="chat.completion.chunk",
  169. system_fingerprint="",
  170. usage=CompletionUsage(
  171. prompt_tokens=2,
  172. completion_tokens=17,
  173. total_tokens=19,
  174. ),
  175. )
  176. else:
  177. yield ChatCompletionChunk(
  178. id="cmpl-3QJQa5jXJ5Z5X",
  179. choices=[
  180. Choice(
  181. delta=ChoiceDelta(
  182. content=full_text[i],
  183. role="assistant",
  184. ),
  185. finish_reason="content_filter",
  186. index=0,
  187. )
  188. ],
  189. created=int(time()),
  190. model=model,
  191. object="chat.completion.chunk",
  192. system_fingerprint="",
  193. )
  194. def chat_create(
  195. self: Completions,
  196. *,
  197. messages: list[ChatCompletionMessageParam],
  198. model: Union[
  199. str,
  200. Literal[
  201. "gpt-4-1106-preview",
  202. "gpt-4-vision-preview",
  203. "gpt-4",
  204. "gpt-4-0314",
  205. "gpt-4-0613",
  206. "gpt-4-32k",
  207. "gpt-4-32k-0314",
  208. "gpt-4-32k-0613",
  209. "gpt-3.5-turbo-1106",
  210. "gpt-3.5-turbo",
  211. "gpt-3.5-turbo-16k",
  212. "gpt-3.5-turbo-0301",
  213. "gpt-3.5-turbo-0613",
  214. "gpt-3.5-turbo-16k-0613",
  215. ],
  216. ],
  217. functions: list[completion_create_params.Function] | NotGiven = NOT_GIVEN,
  218. response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN,
  219. stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN,
  220. tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
  221. **kwargs: Any,
  222. ):
  223. openai_models = [
  224. "gpt-4-1106-preview",
  225. "gpt-4-vision-preview",
  226. "gpt-4",
  227. "gpt-4-0314",
  228. "gpt-4-0613",
  229. "gpt-4-32k",
  230. "gpt-4-32k-0314",
  231. "gpt-4-32k-0613",
  232. "gpt-3.5-turbo-1106",
  233. "gpt-3.5-turbo",
  234. "gpt-3.5-turbo-16k",
  235. "gpt-3.5-turbo-0301",
  236. "gpt-3.5-turbo-0613",
  237. "gpt-3.5-turbo-16k-0613",
  238. ]
  239. azure_openai_models = ["gpt35", "gpt-4v", "gpt-35-turbo"]
  240. if not re.match(r"^(https?):\/\/[^\s\/$.?#].[^\s]*$", str(self._client.base_url)):
  241. raise InvokeAuthorizationError("Invalid base url")
  242. if model in openai_models + azure_openai_models:
  243. if not re.match(r"sk-[a-zA-Z0-9]{24,}$", self._client.api_key) and type(self._client) == OpenAI:
  244. # sometime, provider use OpenAI compatible API will not have api key or have different api key format
  245. # so we only check if model is in openai_models
  246. raise InvokeAuthorizationError("Invalid api key")
  247. if len(self._client.api_key) < 18 and type(self._client) == AzureOpenAI:
  248. raise InvokeAuthorizationError("Invalid api key")
  249. if stream:
  250. return MockChatClass.mocked_openai_chat_create_stream(model=model, functions=functions, tools=tools)
  251. return MockChatClass.mocked_openai_chat_create_sync(model=model, functions=functions, tools=tools)