@@ -107,7 +107,8 @@ class Chatgpt_5_4(Chatgpt): model_name = info.pop('version', 'gpt-5.4') user_system_prompt = info.pop('system_prompt', '') plan_info = self.store.user.payment_plan - is_free_plan = plan_info and plan_info.plan.price <= 0 + is_regular_user = self.store.user.account_type == 'regular' + is_free_plan = is_regular_user and plan_info and plan_info.plan.price <= 0 if is_free_plan and model_name == 'gpt-5.4-pro': raise PaidPlanRequiredError() if is_free_plan: @@ -7,9 +7,10 @@ from io import BytesIO import filetype from PIL import Image +from PIL.Image import DecompressionBombError from messages.models import Message -from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError +from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError, ImageTooLargeError from ml_model.services.EmbeddingService import EmbeddingService from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService @@ -27,6 +28,9 @@ class Grok_4_1_Fast(SimpleService): TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} + MAX_PIXELS = 178956970 + + def calculate_price(self, input_tokens: int, output_tokens: int, embedding_tokens: int) -> Decimal: price = ( input_tokens * self.TOKENS_COST['input'] / 1_000_000 @@ -57,6 +61,7 @@ class Grok_4_1_Fast(SimpleService): if input_message.file: file_service = FileProcessingService file_bytes = input_message.file.read() + input_message.file.close() kind = filetype.guess(file_bytes[:550]) if not kind: raise CorruptedFileError @@ -100,14 +105,20 @@ class Grok_4_1_Fast(SimpleService): f'{chunks}. Вопрос: {input_message.content}' ) elif file_extension in ('jpg', 'jpeg', 'png', 'webp'): - kind = filetype.guess(file_bytes[:20]) + try: + with Image.open(BytesIO(file_bytes)) as normalized_image: + current_pixels = normalized_image.width * normalized_image.height + if current_pixels > self.MAX_PIXELS: + raise ImageTooLargeError(self.MAX_PIXELS) + except DecompressionBombError: + raise ImageTooLargeError(self.MAX_PIXELS) + 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() + image_url = ( + f'data:{mime};base64,' + f'{base64.b64encode(file_bytes).decode("utf-8")}' + ) + messages[-1]['content'] = [ {'type': 'text', 'text': input_message.content}, {'type': 'image_url', 'image_url': {'url': image_url}}, @@ -39,6 +39,14 @@ class UnsupportedSize(Exception): self.current_size | self.required_size ) +class ImageTooLargeError(Exception): + def __init__(self, max_pixels: int) -> None: + self.max_pixels = max_pixels + + def __str__(self) -> str: + return _('Image exceeds the maximum allowed pixel count (%(max_pixels)d).') % { + 'max_pixels': self.max_pixels + } class ModelTimeoutError(Exception): def __str__(self): @@ -153,6 +161,11 @@ class ServiceHighDemandError(Exception): return _('Service is currently unavailable due to high demand. Please try again later') +class OpenRouterCreditsError(Exception): + def __str__(self) -> str: + return _('OpenRouter credits are exhausted. Please try again later.') + + class PaidPlanRequiredError(Exception): def __str__(self) -> str: return _('Available only in paid plan') @@ -160,4 +173,4 @@ class PaidPlanRequiredError(Exception): class FaceNotFoundError(Exception): def __str__(self) -> str: - return _('Face not found in the image. Please try another image with a face.') + return _('Face not found in the image. Please try another image with a face.') \ No newline at end of file