@@ -51,6 +51,7 @@ from ml_model.services.prunaai import Prunaai from ml_model.services.pruna_v import Pruna_V from ml_model.services.qwen import Qwen from ml_model.services.qwen_235B import Qwen_235B +from ml_model.services.qwen_3_5 import Qwen_3_5 from ml_model.services.qwen_3_max_thinking import Qwen_3_Max_Thinking from ml_model.services.raifgpt import Raifgpt from ml_model.services.ray import Ray @@ -8,6 +8,7 @@ import filetype from PIL import Image from messages.models import Message +from ml_model.exceptions import FileExtensionNotSupported from ml_model.services.EmbeddingService import EmbeddingService from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService @@ -20,23 +21,31 @@ from tools.public_api.models import APIStore class Gemini_3_1(SimpleService): TOKENS_COST = { - 'input': Decimal('600'), - 'output': Decimal('3600'), - 'highest_prices': {'input': Decimal('1200'), 'output': Decimal('5400')}, + 'gemini-3.1-pro-preview:online': { + 'input': Decimal('600'), + 'output': Decimal('3600'), + 'highest_prices': {'input': Decimal('1200'), 'output': Decimal('5400')}, + }, + 'gemini-3.1-flash-lite-preview:online': { + 'input': Decimal('75'), + 'output': Decimal('450'), + 'highest_prices': {'input': Decimal('75'), 'output': Decimal('450')}, + } } TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} - def calculate_price(self, input_tokens: int, output_tokens: int, embedding_tokens: int) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int, embedding_tokens: int) -> Decimal: + price_map = self.TOKENS_COST[version] if input_tokens >= 200_000 or output_tokens >= 200_000: price = ( - input_tokens * self.TOKENS_COST['highest_prices']['input'] / 1_000_000 - + output_tokens * self.TOKENS_COST['highest_prices']['output'] / 1_000_000 + input_tokens * price_map['highest_prices']['input'] / 1_000_000 + + output_tokens * price_map['highest_prices']['output'] / 1_000_000 ) else: price = ( - input_tokens * self.TOKENS_COST['input'] / 1_000_000 - + output_tokens * self.TOKENS_COST['output'] / 1_000_000 + input_tokens * price_map['input'] / 1_000_000 + + output_tokens * price_map['output'] / 1_000_000 ) if embedding_tokens > 0: price += self.TOOLS_TOKEN_COSTS['text-embedding-3-small']['output'] * embedding_tokens @@ -56,6 +65,7 @@ class Gemini_3_1(SimpleService): return msgs def make(self, input_message: Message, save: bool = True) -> list[Message]: + version = input_message.info.get('version', 'gemini-3.1-pro-preview:online') callback_data = { 'provider': {'order': ['Google AI Studio']}, **input_message.info, @@ -103,7 +113,7 @@ class Gemini_3_1(SimpleService): f'Используй системный промпт. Содержание файла: ' f'{chunks}. Вопрос: {input_message.content}' ) - else: + elif file_extension in ('jpg', 'jpeg', 'png', 'webp'): kind = filetype.guess(file_bytes[:20]) mime = kind.mime if kind else 'application/octet-stream' normalized_image = Image.open(input_message.file) @@ -116,11 +126,14 @@ class Gemini_3_1(SimpleService): {'type': 'text', 'text': input_message.content}, {'type': 'image_url', 'image_url': {'url': image_url}}, ] + else: + raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG', 'WEBP']) start_time = time.time() - result = openrouter_run('google/gemini-3.1-pro-preview:online', messages, callback_data, 'Gemini 3.1 Pro') + result = openrouter_run(f'google/{version}', messages, callback_data, 'Gemini 3.1') process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, + version=version, input_tokens=result[1], output_tokens=result[2], embedding_tokens=embedding_tokens, @@ -0,0 +1,166 @@ +import base64 +import time +from datetime import timedelta +from decimal import Decimal +from io import BytesIO + +import filetype +from PIL import Image + +from messages.models import Message +from ml_model.exceptions import FileExtensionNotSupported +from ml_model.services.EmbeddingService import EmbeddingService +from ml_model.services.FileService import FileProcessingService +from ml_model.services.base import SimpleService +from ml_model.tasks import openrouter_run +from poller.models import Proxy +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + + +class Qwen_3_5(SimpleService): + TOKENS_COST = { + 'qwen3.5-9b': {'input': Decimal('30'), 'output': Decimal('45')}, + 'qwen3.5-flash-02-23': {'input': Decimal('30'), 'output': Decimal('120')}, + 'qwen3.5-35b-a3b': {'input': Decimal('48.75'), 'output': Decimal('390')}, + 'qwen3.5-27b': {'input': Decimal('58.5'), 'output': Decimal('468')}, + 'qwen3.5-122b-a10b': {'input': Decimal('78'), 'output': Decimal('624')}, + 'qwen3.5-397b-a17b': {'input': Decimal('117'), 'output': Decimal('702')}, + } + + PROVIDERS = { + 'qwen3.5-9b': 'together', + 'qwen3.5-flash-02-23': 'alibaba', + 'qwen3.5-35b-a3b': 'alibaba', + 'qwen3.5-27b': 'alibaba', + 'qwen3.5-122b-a10b': 'alibaba', + 'qwen3.5-397b-a17b': 'alibaba', + } + + TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} + + def calculate_price( + self, version: str, input_tokens: int, output_tokens: int, embedding_tokens: int + ) -> Decimal: + price = ( + input_tokens * self.TOKENS_COST[version]['input'] / 1_000_000 + + output_tokens * self.TOKENS_COST[version]['output'] / 1_000_000 + + Decimal('2') + ) + if embedding_tokens > 0: + price += self.TOOLS_TOKEN_COSTS['text-embedding-3-small']['output'] * embedding_tokens + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, content: str, time: timedelta, save: bool = True) -> list[Message]: + msgs = [ + Message( + content=content, + content_object=self.store, + elapsed_time=time, + ) + ] + if save: + return Message.objects.bulk_create(msgs) + return msgs + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + start_time = time.time() + version = input_message.info.get('version', 'qwen3.5-9b') + callback_data = {'provider': {'order': [self.PROVIDERS[version]]}, **input_message.info} + messages = self.get_chat_history() + messages.append({'role': 'user', 'content': input_message.content}) + embedding_tokens = 0 + if input_message.file: + file_service = FileProcessingService + file_bytes = input_message.file.read() + kind = filetype.guess(file_bytes[:20]) + raw_file_extension = kind.extension + file_extension = file_service.get_file_extension(raw_file_extension, file_bytes) + if file_extension in ('pdf', 'doc', 'docx', 'xlsx'): + text = file_service.get_file_data(file_extension, file_bytes) + chunks = EmbeddingService.split_text_to_chunks(text) + if len(text) > 20_000: + for proxy in Proxy.objects.all(): + document_name = chunks[0].partition(f':{chr(10)}')[2].split(f'{chr(10)}')[0][:100] + embedding_tokens, file_data = EmbeddingService.get_large_file_data( + self.store.messages.first().pk, + chunks, + proxy, + input_message.content, + model='text-embedding-3-small', + index_name='ml_model-index-1536', + ) + messages[-1]['content'] = EmbeddingService.make_embeddings_prompt( + document_name=document_name, + section_texts=file_data, + question=input_message.content, + ) + else: + messages[-1]['content'] = ( + f'Используй системный промпт. Содержание файла: ' + f'{chunks}. Вопрос: {input_message.content}' + ) + elif file_extension in ('jpg', 'jpeg', 'png', 'webp'): + kind = filetype.guess(file_bytes[:20]) + mime = kind.mime if kind else 'application/octet-stream' + normalized_image = Image.open(input_message.file) + format = 'jpeg' if kind.extension == 'jpg' else kind.extension + buf = BytesIO() + normalized_image.save(buf, format=format) + image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + buf.close() + messages[-1]['content'] = [ + {'type': 'text', 'text': input_message.content}, + {'type': 'image_url', 'image_url': {'url': image_url}}, + ] + else: + raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG', 'WEBP']) + model_slug = f'qwen/{version}:online' if self.PROVIDERS[version] == 'alibaba' else f'qwen/{version}' + result = openrouter_run(model_slug, messages, callback_data, 'Qwen 3.5') + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + version=version, + input_tokens=result[1], + output_tokens=result[2], + embedding_tokens=embedding_tokens, + ) + msgs = self.save_results(result[0], process_time) + return msgs + + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: + if isinstance(self.store, Chat): + air_messages = list( + reversed( + Message.objects.filter( + chats_chats_messages=self.store, is_deleted=False, is_sent=True + ).order_by('-created_at')[1 : message_limit + 1] + ) + ) + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] + ) + ) + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({'role': 'assistant', 'content': content}) + else: + memory.append({'role': 'user', 'content': content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + character_length -= len(memory.pop(0)['content']) + + return memory \ No newline at end of file @@ -123,7 +123,9 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name ) reasoning = re.sub(r'Вывод:|Основная мысль:|Рассуждение:|\*\*', '', reasoning) answer = reasoning - if any(m in data['model'] for m in ('google/gemini', 'x-ai/grok-4.1-fast')): + if any(m in data['model'] for m in ('google/gemini', 'x-ai/grok-4.1-fast')) or re.match( + r'^qwen/qwen3\.5-.*$', data['model'] + ): answer = content elif reasoning and content: # TODO: переделать рендеринг сообщения на Jinja 2