@@ -1,6 +1,6 @@ from ml_model.runners.dummy import DummyImageRunner, DummyTextRunner from ml_model.runners.falai import FalAIRunner -from ml_model.runners.openai import OpenAIGPTRunner +from ml_model.runners.openai import OpenAIGPTRunner, OpenAIResponseRunner from ml_model.runners.openrouter import OpenrouterRunner from ml_model.runners.replicate import ( ReplicateAudioRunner, @@ -11,6 +11,7 @@ from ml_model.runners.replicate import ( __all__ = [ 'OpenAIGPTRunner', + 'OpenAIResponseRunner', 'OpenrouterRunner', 'ReplicateTextRunner', 'ReplicateAudioRunner', @@ -166,3 +166,126 @@ class OpenAIGPTRunner(OpenAICompatibleRunner): raise ParameterNotValid('model') elif error['code'] == 'invalid_value': raise ParameterNotValid(error['param']) + elif error['code'] == 'invalid_type': + raise ParameterNotValid(error['param']) + elif error['code'] == 'unknown_parameter': + raise ParameterNotValid(error['param']) + + +class OpenAIResponseRunner(OpenAIGPTRunner): + @classmethod + def generate(cls, content=None, file=None, parameters={}, history=[], scrape_results=[]): + proxies = Proxy.objects.all() + payload = { + 'input': [ + *[ + { + 'role': 'user' if not message.from_model else 'assistant', + 'content': stripped_message, + } + for message in history + if message.content and (stripped_message := message.content.strip()) + ], + ], + 'stream': True, + 'instructions': 'Форматирование — обязательное требование. Выполняй строго по правилам:\\n\\n1) ' + 'Используй реальные символы новой строки. Не выводи "\\\\n" как текст — вставляй ' + 'переносы (символ новой строки).\\n2) Между абзацами ставь ОДНУ пустую строку ' + '(то есть два символа новой строки подряд: \\\\n\\\\n).\\n3) Для списков — каждый пункт на ' + 'отдельной строке; между списком и текстом — пустая строка.\\n4) ' + 'Любые блоки/куски/фрагменты кода СТРОГО ' + 'в тройных бэктиках (```) с указанием наименования языка программирования, ' + 'с пустой строкой перед и после блока/куска/фрагмента кода.' + '5) Не используй HTML.\\n6) Если формат неверный — перепиши ответ и ' + 'верни исправленный вариант.', + **parameters, + } + for result in scrape_results: + if isinstance(result, StringIO): + payload['input'].append( + {'role': 'user', 'content': [{'type': 'input_text', 'text': result.getvalue()}]} + ) + + payload['input'].append({'role': 'user', 'content': [{'type': 'input_text', 'text': content}]}) + + if file and isinstance(file, BytesIO): + mime = filetype.guess(file.read(20)).mime + file.seek(0) + payload['input'][-1]['content'].append( + { + 'type': 'input_image', + 'image_url': f'data:{mime};base64,{base64.b64encode(file.getvalue()).decode("utf-8")}' + } + ) + elif file and isinstance(file, StringIO): + file_content = file.getvalue() + if len(file_data := file.getvalue()) > 20_000: + chunks = TextSplitterTool().split_text( + text=file_data, separators=["\n\n", "\n", ".", " ", ""] + ) + for proxy in proxies: + try: + file_content = EmbeddingTool( + settings.REDIS_HOST, + settings.REDIS_PORT, + proxy.protocol, + proxy.address, + cls.AUTHORIZATION_TOKEN, + settings.MAX_THREADS + ).convert( + document_name=chunks[0].partition(f':{chr(10)}')[2].split(f'{chr(10)}')[0][:100], + chunks=chunks, + file_uid=str(uuid.uuid4()).replace('-', '_'), + user_prompt=content + ) + payload['input'].pop() + except httpx.ConnectError: + continue + except httpx.TimeoutException as exc: + logger.exception(exc) + raise Exception('Timeout happened') + payload['input'].append( + {'role': 'user', 'content': [{'type': 'input_text', 'text': file_content}]} + ) + for proxy in proxies: + with httpx.Client( + base_url=cls.BASE_URL, + headers={ + 'Authorization': f'Bearer {cls.AUTHORIZATION_TOKEN}', + 'Content-Type': 'application/json', + }, + proxy=f'{proxy.protocol}://{proxy.address}', + timeout=600, + ) as client: + try: + with client.stream('POST', '/responses', json=payload) as stream: + stream_content = stream.iter_lines() + if stream.status_code >= 400: + raw = ''.join([chunk for chunk in stream_content]) + try: + errors: dict[Literal['error'], dict[Literal['message'] | str, Any]] = ( + json.loads(raw).get('error', {}) + ) + except json.decoder.JSONDecodeError: + logger.error(raw) + errors = raw + cls.map_errors(errors) + for chunk in stream_content: + if chunk.strip() in cls.SKIP_TOKENS: + continue + if chunk.strip() in cls.END_TOKENS: + break + try: + data_str = chunk[5:].strip() + dict_ = json.loads(data_str) + if dict_.get("type") == "response.output_text.delta": + text = dict_.get("delta", "") + if text: + yield text + except json.decoder.JSONDecodeError: + continue + except httpx.ConnectError: + continue + except httpx.TimeoutException as exc: + logger.exception(exc) + raise Exception('Timeout happened')