@@ -9,11 +9,13 @@ import filetype from PIL import Image from messages.models import Message -from ml_model.exceptions import FileExtensionNotSupported, ModelVersionNotAvailable +from ml_model.exceptions import FileExtensionNotSupported, FileUploadUnsupported, ModelVersionNotAvailable 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 payments.exceptions.insufficient_balance import InsufficientBalance +from payments.selectors.payment_plan_selector import PaymentPlanSelector from poller.models import Proxy from tools.chats.models import Chat from tools.copywrite.models import Copywrite @@ -35,8 +37,22 @@ class Claude(SimpleService): 'input': Decimal('1500'), 'output': Decimal('7500'), }, # 1M tokens + 'claude-fable-5': { + 'input': Decimal('3000'), + 'output': Decimal('15000'), + }, # 1M tokens } + FABLE_SYSTEM_PROMPT = """ + You are a helpful assistant. + Answer the user’s question directly and accurately. + If the request is simple, respond briefly. + If the request is complex, provide a structured and clear explanation. + If context is missing, make reasonable assumptions and continue. + Use tools when they are available and clearly useful. + Keep responses natural, grounded, and consistent with the user’s intent. + """ + TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} def calculate_price( @@ -70,12 +86,34 @@ class Claude(SimpleService): raise ModelVersionNotAvailable(version_slug, self.TOKENS_COST) version = f'anthropic/{version_slug}' system_prompt = input_message.info.pop('system_prompt', '') - callback_data = {'provider': {'order': ['Anthropic']}, **input_message.info} + callback_data = {'provider': {'order': ['anthropic']}, **input_message.info, 'tools': []} messages = [ {'role': 'system', 'content': system_prompt}, *self.get_chat_history(), {'role': 'user', 'content': input_message.content}, ] + if version_slug == 'claude-fable-5': + current_user_balance = PaymentPlanSelector(self.store.user).get_current_balance() + if current_user_balance < (cost := Decimal('100')) and input_message.file: + raise InsufficientBalance(current_user_balance, cost) + messages.insert(0, {'role': 'system', 'content': self.FABLE_SYSTEM_PROMPT}) + if reasoning := input_message.info.get('reasoning'): + reasoning_data = { + 'Средний': 'low', + 'Высокий': 'medium', + } + callback_data['reasoning'] = {'effort': reasoning_data[reasoning]} + callback_data['tools'].append( + { + 'type': 'openrouter:web_search', + 'parameters': { + 'engine': 'parallel', + 'max_results': 1, + 'max_total_results': 3, + 'search_context_size': 'low', + }, + } + ) file = input_message.file embedding_tokens = 0 if file: @@ -118,14 +156,16 @@ class Claude(SimpleService): with Image.open(file) as normalized_image: with BytesIO() as buf: normalized_image.save(buf, format=format) - image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + image_url = ( + f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + ) messages[-1]['content'] = [ {'type': 'text', 'text': input_message.content}, {'type': 'image_url', 'image_url': {'url': image_url}}, ] except Exception: raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX', 'XLSX', 'JPG', 'JPEG', 'PNG']) - result = openrouter_run(f"{version}:online", messages, callback_data, 'Claude') + result = openrouter_run(f'{version}', messages, callback_data, 'Claude') process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model,