@@ -59,6 +59,7 @@ from ml_model.services.pulid import Pulid from ml_model.services.qwen import Qwen from ml_model.services.qwen_235B import Qwen_235B from ml_model.services.qwen_3_6 import Qwen_3_6 +from ml_model.services.qwen_3_7 import Qwen_3_7 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 @@ -79,3 +80,4 @@ from ml_model.services.vicuna import Vicuna from ml_model.services.wan import Wan from ml_model.services.wan_lite import Wan_Lite from ml_model.services.whisper import Whisper +from ml_model.services.image_test_model import Image_Test_Model @@ -0,0 +1,90 @@ +import time +from datetime import timedelta +from decimal import Decimal +from io import BytesIO +from typing import Any +import filetype + +import requests +from django.core.files import File + +from messages.models import Message +from ml_model.exceptions import InvalidParameterError +from ml_model.models import ModelCategory, ModelInput, ModelParameter +from ml_model.services.base import SimpleService +from payments.exceptions.insufficient_balance import InsufficientBalance +from payments.selectors.payment_plan_selector import PaymentPlanSelector + + + +class Image_Test_Model(SimpleService): + title = 'Image Test Model' + description = 'Тестовая нейросеть для генерации изображений' + category = ModelCategory(title='Изображения', slug='images') + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] + + parameters = [ModelParameter( + name='Кастомный URL изображения', + key='ciu', + type=ModelParameter.TypeChoices.STR, + )] + + TOKENS_COST = Decimal('3') + PLACEHOLDER_URL='https://i.pinimg.com/736x/8b/e0/61/8be06158da3986fb4c47497b5660bb29.jpg' + + def calculate_price(self) -> Decimal: + return self.TOKENS_COST + + @classmethod + def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + return cls.TOKENS_COST + + def save_results(self, content: str, t: timedelta, image_bytes: bytes, image_ext: str = '.png', save: bool = True) -> list[Message]: + msg = Message( + content=content, + content_object=self.store, + elapsed_time=t, + file=File(BytesIO(image_bytes), image_ext), + ) + if save: + return Message.objects.bulk_create([msg]) + return [msg] + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: + raise InsufficientBalance(balance, self.TOKENS_COST) + + start_time = time.time() + ciu = input_message.info.get('ciu', None) + + + if ciu: + image_bytes, image_ext = self._fetch_image(ciu) + else: + image_bytes, image_ext = self._fetch_image(self.PLACEHOLDER_URL) + + 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, image_bytes, image_ext, save) + return msgs + + + + def _fetch_image(self, url: str): + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + except requests.RequestException: + raise InvalidParameterError('Invalid image URL') + + kind = filetype.guess(response.content[:20]) + + if kind is None: + raise InvalidParameterError('Invalid image URL') + + if not kind.mime.startswith('image/'): + raise InvalidParameterError('Image format not supported') + + return response.content, f'.{kind.extension}' + \ No newline at end of file @@ -0,0 +1,183 @@ +import time +from datetime import timedelta +from decimal import Decimal +from pathlib import Path + +import filetype + +from messages.models import Message +from ml_model.exceptions import CorruptedFileError, FileExtensionNotSupported +from ml_model.services.EmbeddingService import EmbeddingService +from ml_model.services.FileService import FileProcessingService +from ml_model.exceptions import ModelVersionNotAvailable +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_7(SimpleService): + COEFFICIENT = Decimal('300.0') + + TOKENS_COST = { + 'qwen3.7-max': {'input': Decimal('750'), 'output': Decimal('2250')}, + 'qwen3.7-plus': {'input': Decimal('120'), 'output': Decimal('480')}, + } + + MAX_OUTPUT_TOKENS = 30_000 + + TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} + + SYSTEM = """ + You are a reliable and practical AI assistant. + + Provide accurate, direct, and useful answers. + Focus on the user's actual goal and solve the task with minimal unnecessary explanation. + + Rules: + - Follow the user's instructions exactly. + - Start with the answer, not with introductions or disclaimers. + - Be concise when the question is simple. + - Be detailed when the task is complex or requires analysis. + - Ask clarifying questions only when necessary. + - Do not make up facts, data, sources, or capabilities. + - If information is uncertain, state the uncertainty clearly. + - Prefer actionable recommendations over theory. + - Structure long answers with headings, lists, and examples. + - Preserve important details and constraints from the conversation. + - When writing code, prioritize correctness, readability, and maintainability. + - Adapt the level of detail, terminology, and tone to the user's apparent expertise. + + Your goal is to maximize usefulness, clarity, and task completion. + """ + + def calculate_price(self, version: str, cost: float, embedding_tokens: int) -> Decimal: + price = Decimal(cost) * self.COEFFICIENT + 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_slug = input_message.info.pop('version', None) + if version_slug is None or version_slug not in self.TOKENS_COST: + raise ModelVersionNotAvailable(version_slug, self.TOKENS_COST) + callback_data = {'max_tokens': self.MAX_OUTPUT_TOKENS, 'tools': [], **input_message.info} + messages = [ + {'role': 'system', 'content': self.SYSTEM}, + *self.get_chat_history(), + {'role': 'user', 'content': input_message.content}, + ] + embedding_tokens = 0 + if input_message.file: + file_service = FileProcessingService + file_bytes = input_message.file.read() + input_message.file.close() + kind = filetype.guess(file_bytes[:550]) + supported_extensions = ['PDF', 'DOC', 'DOCX', 'XLSX'] + if not kind: + if Path(input_message.file.name).suffix[1:].upper() not in supported_extensions: + raise FileExtensionNotSupported(supported_extensions) + raise CorruptedFileError + 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: + raise FileExtensionNotSupported(supported_extensions) + callback_data['tools'].append( + { + 'type': 'openrouter:web_search', + 'parameters': { + 'engine': 'parallel', + 'max_results': 1, + 'max_total_results': 3, + 'search_context_size': 'low', + }, + } + ) + model_slug = f'qwen/{version_slug}' + result = openrouter_run(model_slug, messages, callback_data, 'Qwen 3.7') + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + version=version_slug, + cost=result[1], + 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 @@ -190,7 +190,7 @@ 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.3')) or re.match( - r'^qwen/qwen3\.5-.*$', data['model'] + r'^qwen/qwen3\.(?:5|6|7)-.*$', data['model'] ): answer = content elif reasoning and content: @@ -209,8 +209,12 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name logger.error(f'Model {model_name} disabled') raise DeploymentDisabled else: - input_tokens = data['usage']['prompt_tokens'] - output_tokens = data['usage']['completion_tokens'] + if re.match(r'^qwen/qwen3\.7-.*$', data['model']): + input_tokens = data['usage']['cost'] + output_tokens = 0 + else: + input_tokens = data['usage']['prompt_tokens'] + output_tokens = data['usage']['completion_tokens'] return (re.sub(r'\\+["n*]', '', answer), input_tokens, output_tokens) logger.error(f'Error occured via model {model_name}. Data: {resp.content}') raise Exception(f'No answer from {model_name}, please retry later') @@ -136,12 +136,12 @@ services: - traefik.enable=true - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure} - - traefik.http.routers.backend-static-http.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/${STATIC_PATH_PREFIX:-static}`) + - traefik.http.routers.backend-static-http.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`${STATIC_PATH_PREFIX:-static}`) - traefik.http.routers.backend-static-http.entrypoints=web - traefik.http.routers.backend-static-http.service=backend-static - traefik.http.routers.backend-static-http.middlewares=sts-header@file,https-redirect@file - - traefik.http.routers.backend-static-https.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/${STATIC_PATH_PREFIX:-static}`) + - traefik.http.routers.backend-static-https.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`${STATIC_PATH_PREFIX:-static}`) - traefik.http.routers.backend-static-https.entrypoints=websecure - traefik.http.routers.backend-static-https.tls=true - traefik.http.routers.backend-static-https.tls.certresolver=defaultresolver