openai_chat.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import re
  2. from collections.abc import Generator
  3. from json import dumps, loads
  4. from time import sleep, time
  5. # import monkeypatch
  6. from typing import Any, Literal, Optional, Union
  7. import openai.types.chat.completion_create_params as completion_create_params
  8. from openai import AzureOpenAI, OpenAI
  9. from openai._types import NOT_GIVEN, NotGiven
  10. from openai.resources.chat.completions import Completions
  11. from openai.types import Completion as CompletionMessage
  12. from openai.types.chat import (
  13. ChatCompletion,
  14. ChatCompletionChunk,
  15. ChatCompletionMessageParam,
  16. ChatCompletionMessageToolCall,
  17. ChatCompletionToolChoiceOptionParam,
  18. ChatCompletionToolParam,
  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(
  70. tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
  71. ) -> Optional[list[ChatCompletionMessageToolCall]]:
  72. list_tool_calls = []
  73. if not tools or len(tools) == 0:
  74. return None
  75. tool: ChatCompletionToolParam = tools[0]
  76. if tools['type'] != 'function':
  77. return None
  78. function = tool['function']
  79. function_call = MockChatClass.generate_function_call(functions=[function])
  80. if function_call is None:
  81. return None
  82. list_tool_calls.append(ChatCompletionMessageToolCall(
  83. id='sakurajima-mai',
  84. function=Function(
  85. name=function_call.name,
  86. arguments=function_call.arguments,
  87. ),
  88. type='function'
  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. sleep(1)
  102. return _ChatCompletion(
  103. id='cmpl-3QJQa5jXJ5Z5X',
  104. choices=[
  105. _ChatCompletionChoice(
  106. finish_reason='content_filter',
  107. index=0,
  108. message=ChatCompletionMessage(
  109. content='elaina',
  110. role='assistant',
  111. function_call=function_call,
  112. tool_calls=tool_calls
  113. )
  114. )
  115. ],
  116. created=int(time()),
  117. model=model,
  118. object='chat.completion',
  119. system_fingerprint='',
  120. usage=CompletionUsage(
  121. prompt_tokens=2,
  122. completion_tokens=1,
  123. total_tokens=3,
  124. )
  125. )
  126. @staticmethod
  127. def mocked_openai_chat_create_stream(
  128. model: str,
  129. functions: list[completion_create_params.Function] | NotGiven = NOT_GIVEN,
  130. tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
  131. ) -> Generator[ChatCompletionChunk, None, None]:
  132. tool_calls = []
  133. function_call = MockChatClass.generate_function_call(functions=functions)
  134. if not function_call:
  135. tool_calls = MockChatClass.generate_tool_calls(tools=tools)
  136. full_text = "Hello, world!\n\n```python\nprint('Hello, world!')\n```"
  137. for i in range(0, len(full_text) + 1):
  138. sleep(0.1)
  139. if i == len(full_text):
  140. yield ChatCompletionChunk(
  141. id='cmpl-3QJQa5jXJ5Z5X',
  142. choices=[
  143. Choice(
  144. delta=ChoiceDelta(
  145. content='',
  146. function_call=ChoiceDeltaFunctionCall(
  147. name=function_call.name,
  148. arguments=function_call.arguments,
  149. ) if function_call else None,
  150. role='assistant',
  151. tool_calls=[
  152. ChoiceDeltaToolCall(
  153. index=0,
  154. id='misaka-mikoto',
  155. function=ChoiceDeltaToolCallFunction(
  156. name=tool_calls[0].function.name,
  157. arguments=tool_calls[0].function.arguments,
  158. ),
  159. type='function'
  160. )
  161. ] if tool_calls and len(tool_calls) > 0 else None
  162. ),
  163. finish_reason='function_call',
  164. index=0,
  165. )
  166. ],
  167. created=int(time()),
  168. model=model,
  169. object='chat.completion.chunk',
  170. system_fingerprint='',
  171. usage=CompletionUsage(
  172. prompt_tokens=2,
  173. completion_tokens=17,
  174. total_tokens=19,
  175. ),
  176. )
  177. else:
  178. yield ChatCompletionChunk(
  179. id='cmpl-3QJQa5jXJ5Z5X',
  180. choices=[
  181. Choice(
  182. delta=ChoiceDelta(
  183. content=full_text[i],
  184. role='assistant',
  185. ),
  186. finish_reason='content_filter',
  187. index=0,
  188. )
  189. ],
  190. created=int(time()),
  191. model=model,
  192. object='chat.completion.chunk',
  193. system_fingerprint='',
  194. )
  195. def chat_create(self: Completions, *,
  196. messages: list[ChatCompletionMessageParam],
  197. model: Union[str,Literal[
  198. "gpt-4-1106-preview", "gpt-4-vision-preview", "gpt-4", "gpt-4-0314", "gpt-4-0613",
  199. "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613",
  200. "gpt-3.5-turbo-1106", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0301",
  201. "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613"],
  202. ],
  203. functions: list[completion_create_params.Function] | NotGiven = NOT_GIVEN,
  204. response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN,
  205. stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN,
  206. tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
  207. **kwargs: Any,
  208. ):
  209. openai_models = [
  210. "gpt-4-1106-preview", "gpt-4-vision-preview", "gpt-4", "gpt-4-0314", "gpt-4-0613",
  211. "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613",
  212. "gpt-3.5-turbo-1106", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0301",
  213. "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613",
  214. ]
  215. azure_openai_models = [
  216. "gpt35", "gpt-4v", "gpt-35-turbo"
  217. ]
  218. if not re.match(r'^(https?):\/\/[^\s\/$.?#].[^\s]*$', self._client.base_url.__str__()):
  219. raise InvokeAuthorizationError('Invalid base url')
  220. if model in openai_models + azure_openai_models:
  221. if not re.match(r'sk-[a-zA-Z0-9]{24,}$', self._client.api_key) and type(self._client) == OpenAI:
  222. # sometime, provider use OpenAI compatible API will not have api key or have different api key format
  223. # so we only check if model is in openai_models
  224. raise InvokeAuthorizationError('Invalid api key')
  225. if len(self._client.api_key) < 18 and type(self._client) == AzureOpenAI:
  226. raise InvokeAuthorizationError('Invalid api key')
  227. if stream:
  228. return MockChatClass.mocked_openai_chat_create_stream(model=model, functions=functions, tools=tools)
  229. return MockChatClass.mocked_openai_chat_create_sync(model=model, functions=functions, tools=tools)