@@ -1283,6 +1283,10 @@ msgstr "" "Случилась ошибка во время генерации. Она может возникать из-за того, что " "NSFW-контент запрещен. Попробуйте снова" +#: tools/media/apis.py:168 +msgid "Temporary issues with the service, we are already working on a solution." +msgstr "Временные неполадки с сервисом, мы уже работаем над их решением." + #: tools/chats/apis.py:244 msgid "The message has already been deleted" msgstr "Сообщение уже было удалено" @@ -14,6 +14,7 @@ from ml_model.services.fluxkrea import Fluxkrea from ml_model.services.fluxlorafast import Fluxlorafast from ml_model.services.fluxproultra import Fluxproultra from ml_model.services.gemini import Gemini +from ml_model.services.gemini_3_1 import Gemini_3_1 from ml_model.services.gemma import Gemma from ml_model.services.geminiimage import Geminiimage from ml_model.services.gptimage import Gptimage @@ -39,6 +40,7 @@ from ml_model.services.minimaxmusic_lite import Minimaxmusic_Lite from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen from ml_model.services.nanobanana import Nanobanana +from ml_model.services.nanobanana_2 import Nanobanana_2 from ml_model.services.perplexity import Perplexity from ml_model.services.pulid import Pulid from ml_model.services.photon import Photon @@ -50,12 +50,6 @@ class Gemini(SimpleService): 'input': Decimal('30'), 'output': Decimal('120'), }, - 'gemini-3-pro-preview': { - 'input': Decimal('600'), - 'output': Decimal('3600'), - 'input_imgs': Decimal('0'), - 'highest_prices': {'input': Decimal('1200'), 'output': Decimal('5400')}, - }, 'gemini-3-flash-preview': { 'input': Decimal('150'), 'output': Decimal('900'), @@ -69,7 +63,7 @@ class Gemini(SimpleService): self, version: str, input_tokens: int, output_tokens: int, image: FieldFile, embedding_tokens: int ) -> Decimal: price_map = self.TOKENS_COST[version.split('/')[1]] - if version.split('/')[1] in ('gemini-2.5-pro', 'gemini-3-pro-preview') and input_tokens > 200_000: + if version.split('/')[1] == 'gemini-2.5-pro' and input_tokens > 200_000: price = ( input_tokens * price_map['highest_prices']['input'] / 1_000_000 + output_tokens * price_map['highest_prices']['output'] / 1_000_000 @@ -134,10 +128,7 @@ class Gemini(SimpleService): 'Priority: analytical depth, internal consistency, and correctness over speed.', }, ) - callback_data.update({ - "reasoning": {"effort": "high"}, - "temperature": 0.2 - }) + callback_data.update({'reasoning': {'effort': 'high'}, 'temperature': 0.2}) file = input_message.file image = None embedding_tokens = 0 @@ -152,9 +143,7 @@ class Gemini(SimpleService): 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] - ) + 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 ) @@ -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.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 Gemini_3_1(SimpleService): + TOKENS_COST = { + 'input': Decimal('600'), + 'output': Decimal('3600'), + 'highest_prices': {'input': Decimal('1200'), 'output': Decimal('5400')}, + } + + 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: + 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 + ) + else: + price = ( + input_tokens * self.TOKENS_COST['input'] / 1_000_000 + + output_tokens * self.TOKENS_COST['output'] / 1_000_000 + ) + if embedding_tokens > 0: + price += self.TOOLS_TOKEN_COSTS['text-embedding-3-small']['output'] * embedding_tokens + price += Decimal('2') + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, content: str, t: timedelta, save: bool = True) -> list[Message]: + msgs = [ + Message( + content=content, + content_object=self.store, + elapsed_time=t, + ) + ] + if save: + return Message.objects.bulk_create(msgs) + return msgs + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + callback_data = { + 'provider': {'order': ['Google AI Studio']}, + **input_message.info, + } + messages = self.get_chat_history() + messages.insert( + 0, + { + 'role': 'system', + 'content': ( + "Always respond in the same language as the user's last message, " + 'unless the user explicitly asks you to answer in a different language.' + ), + }, + ) + 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}' + ) + else: + 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}}, + ] + start_time = time.time() + result = openrouter_run('google/gemini-3.1-pro-preview:online', messages, callback_data, 'Gemini 3.1 Pro') + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + 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] + ) + ) + else: + air_messages = [] + 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 @@ -59,4 +59,4 @@ class Imagen(SimpleService): raise RequestBlocked elif exc.prediction.error == 'No image content found in response': raise ImageContentNotFound - raise GenerationException + raise GenerationException from exc @@ -0,0 +1,72 @@ +import time +from datetime import timedelta +from decimal import Decimal +from io import BytesIO +from typing import Optional, Any + +import requests +from django.core.files import File +from replicate.exceptions import ModelError + +from messages.models import Message +from ml_model.exceptions import ImageContentNotFound, GenerationException, RequestBlocked +from ml_model.services.base import SimpleService +from ml_model.tasks import replicate_run + + +class Nanobanana_2(SimpleService): + TOKENS_COST = { + '1K': Decimal('20.1'), + '2K': Decimal('30.3'), + '4K': Decimal('45.3'), + } + + def calculate_price(self, resolution: Optional[str]) -> Decimal: + return self.TOKENS_COST[resolution] + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST[info['resolution']] + + def save_results( + self, + prompt: str, + image_url: str, + time: timedelta, + save: bool = True, + ) -> list[Message]: + message = Message( + content_object=self.store, + elapsed_time=time, + content=prompt, + file=File(BytesIO(requests.get(image_url).content), '.png'), + ) + if save: + return Message.objects.bulk_create([message]) + return [message] + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + start_time = time.time() + resolution = input_message.info.get('resolution', '2K') + callback_data = dict( + { + 'prompt': self.translate_prompt(input_message.content), + **input_message.info, + } + ) + if input_message.file: + callback_data.update( + {'image_input': [input_message.file.url], 'aspect_ratio': 'match_input_image'} + ) + try: + image = replicate_run('google/nano-banana-2', callback_data) + except ModelError as exc: + if exc.prediction.error == 'No image content found in response': + raise ImageContentNotFound + elif any(error in str(exc) for error in ('E005', 'E006', 'sexual')): + raise RequestBlocked + raise GenerationException from exc + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model, resolution=resolution) + msgs = self.save_results(input_message.content, image, process_time, save) + return msgs @@ -65,7 +65,7 @@ class Photon(SimpleService): raise RequestBlocked elif exc.prediction.error == 'No image content found in response': raise ImageContentNotFound - raise GenerationException + raise GenerationException from exc process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, process_time, images, save) @@ -159,6 +159,17 @@ class MediaAPIView(APIView): logger.exception(exc) input_message.is_sent = False input_message.save() + if any( + phrase in str(exc) for phrase in ('Insufficient credit', 'Request was throttled') + ): + return Response( + { + 'detail': _( + 'Temporary issues with the service, we are already working on a solution.' + ) + }, + status=HTTP_402_PAYMENT_REQUIRED, + ) if isinstance(exc, InsufficientBalance): return Response({'detail': f'{exc}'}, status=HTTP_402_PAYMENT_REQUIRED) if isinstance(