@@ -7,6 +7,7 @@ from ml_model.services.deepseek import Deepseek from ml_model.services.djourney import Djourney from ml_model.services.epicphotogasm import Epicphotogasm from ml_model.services.flux import Flux +from ml_model.services.fluxproultra import Fluxproultra from ml_model.services.granite import Granite from ml_model.services.grok import Grok from ml_model.services.iconic import Iconic @@ -17,6 +18,7 @@ from ml_model.services.logoai import Logoai from ml_model.services.midjourney import Midjourney from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen +from ml_model.services.perplexity import Perplexity from ml_model.services.pulid import Pulid from ml_model.services.qwen import Qwen from ml_model.services.recraft import Recraft @@ -1,10 +1,21 @@ +import base64 import time -from _decimal import Decimal from datetime import timedelta +from decimal import Decimal +from io import BytesIO +from typing import Iterator, Any + +import filetype +from PIL import Image +from django.db.models.fields.files import FieldFile from messages.models import Message +from ml_model.models import ModelCategory, ModelVersion, ModelInput, ModelParameter from ml_model.services.base import SimpleService -from ml_model.tasks import claude_run +from ml_model.tasks import openrouter_run +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore class Claude(SimpleService): @@ -13,25 +24,27 @@ class Claude(SimpleService): contains abstract method make, which makes a generation """ - TOKEN_PAYMENT_RULES = { - 'claude-instant-1.2': Decimal(0.6), - 'claude-2.1': Decimal(2.572), - } - def __init__(self, store): - super().__init__(store) + TOKENS_COST = { + 'claude-3.7-sonnet:thinking': {'input': Decimal('3000'), 'output': Decimal('3000'), 'input_imgs': Decimal('960')}, # 1M tokens + } - def calculate_price(self, model_name: str, usage: dict[str, int]) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int, image: FieldFile) -> Decimal: + price_map = self.TOKENS_COST[version.split('/')[1]] price = ( - usage['input_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] - + usage['output_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] + input_tokens * price_map['input'] / 1_000_000 + + output_tokens * price_map['output'] / 1_000_000 ) + if image: + price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, r: str, t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, content: Iterator[Any], t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( - content=r, + content=content, content_object=self.store, elapsed_time=t, ) @@ -40,20 +53,84 @@ class Claude(SimpleService): return Message.objects.bulk_create(msgs) return msgs + def make(self, input_message: Message, save: bool = True) -> list[Message]: - callback_data = dict( - { - 'messages': [{'role': 'user', 'content': input_message.content}], - **input_message.info, - } - ) start_time = time.time() - result = claude_run(callback_data) + version = f'anthropic/{input_message.info.pop('version', 'claude-3.7-sonnet:thinking')}' + callback_data = { + 'provider': { + 'order': ['Anthropic'] + }, + **input_message.info + } + messages = self.get_chat_history() + image = input_message.file + if image: + kind = filetype.guess(image.read(20)) + mime = kind.mime if kind else 'application/octet-stream' + normalized_image = Image.open(image) + 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 + } + } + ] + result = openrouter_run(version, messages, callback_data, self.title) process_time = timedelta(seconds=(time.time() - start_time)) - msgs = self.save_results(result['content']['text'], process_time, save) self.handle_invoice( - self.neuron_model, - model_name=input_message['model'], - usage=result['usage'], + input_message.content_object.model, + version=version, + input_tokens=result[1], + output_tokens=result[2], + image=image ) + msgs = self.save_results(result[0], process_time) return msgs + + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + 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')[:message_limit] + ) + ) + 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: + memory.pop(0) + character_length = sum(len(content['content']) for content in memory) + + return memory \ No newline at end of file @@ -17,7 +17,7 @@ class Dalle(SimpleService): contains abstract method make, which makes a generation """ - PRICE = Decimal('1') + PRICE = Decimal('2') _CALLBACK = ( 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' @@ -1,14 +1,11 @@ -import base64 import time from datetime import timedelta from decimal import Decimal from io import BytesIO -import filetype import requests from django.core.files import File -from backend import settings from messages.models import Message from ml_model.models import ( ModelCategory, @@ -33,39 +30,17 @@ class Flux(SimpleService): category = ModelCategory(title='Изображения', slug='images') versions = [ ModelVersion(name='Flux-Schnell', slug='flux-schnell'), - ModelVersion(name='Flux-Pro1.1', slug='flux-pro-1.1'), - ModelVersion(name='Flux-Dev', slug='flux-dev'), - ModelVersion(name='Ultra', slug='flux-1.1-pro-ultra'), ] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( - name='Ширина', - key='width', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 256, 'end': 1440, 'step': 32, 'default': 1024}, - ), - ModelParameter( - name='Высота', - key='height', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 256, 'end': 1440, 'step': 32, 'default': 768}, - ), - ModelParameter( - name='Ускорить', + name='Ускорение генерации', key='go_fast', type=ModelParameter.TypeChoices.BOOL, values={'default': True}, ), - ModelParameter( - name='Приближенность к запросу', - key='guidance', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 0, 'end': 10, 'step': 1, 'default': 3}, - ), ModelParameter( name='Мегапиксели', key='megapixels', @@ -85,13 +60,7 @@ class Flux(SimpleService): values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, ), ModelParameter( - name='Количество шагов вывода', - key='num_inference_steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 50, 'step': 1, 'default': 28}, - ), - ModelParameter( - name='Количество шагов вывода', + name='Количество шагов обработки', key='num_inference_steps', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 1, 'end': 4, 'step': 1, 'default': 4}, @@ -118,94 +87,34 @@ class Flux(SimpleService): }, ), ModelParameter( - name='Качество вывода', + name='Качество вывода (в %)', key='output_quality', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 0, 'end': 100, 'step': 1, 'default': 80}, ), - ModelParameter( - name='Апсемплинг', - key='prompt_upsampling', - type=ModelParameter.TypeChoices.BOOL, - values={'default': False}, - ), - ModelParameter( - name='Отключить обработку', - key='raw', - type=ModelParameter.TypeChoices.BOOL, - values={'default': False}, - ), ] payments_rules = { versions[0].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, cost=0.3, - coefficient=5.00, - ), - versions[1].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=4.00, - coefficient=2.00, - ), - versions[2].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=2.5, - coefficient=2.00, - ), - versions[3].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=6.00, - coefficient=2.00, + coefficient=10.00, ), } - _CALLBACK_BASE = 'black-forest-labs/' @property def neuron_model(self): return NeuronModel.objects.get(title='Flux') - def _call_bfl_api(self, payload: dict) -> list: - bfl_headers = { - 'Content-Type': 'application/json', - 'X-Key': settings.FLUX_API_KEY, - } - bfl_urls = { - 'generate': 'https://api.bfl.ml/v1/', - 'get': 'https://api.bfl.ml/v1/get_result?id=', - } - response = requests.post( - url=f'{bfl_urls["generate"]}{payload["version"]}', - headers=bfl_headers, - json=payload, - ) - if response.status_code != 200: - raise Exception(response.json()) - result = requests.get( - url=f'{bfl_urls["get"]}{response.json().get("id")}', - headers=bfl_headers, - ) - while result.json()['status'] not in ('Ready', 'Error'): - result = requests.get( - url=f'{bfl_urls["get"]}{response.json().get("id")}', - headers=bfl_headers, - ) - return result.json()['result']['sample'] - def calculate_price(self, input_message: Message) -> Decimal: - version_slug = input_message.info.get('version', 'flux-pro-1.1') + version_slug = input_message.info.get('version', 'flux-schnell') payment_rule = self.payments_rules[version_slug] if not payment_rule.pk: payment_rule.model = self.neuron_model payment_rule.save() - if version_slug in (self.versions[1].slug,): - return payment_rule.rate * input_message.info.get('num_outputs', 1) - else: - return payment_rule.rate + return payment_rule.rate * input_message.info.get('num_outputs', 1) + def save_results( self, @@ -236,21 +145,11 @@ class Flux(SimpleService): **input_message.info, } ) - if input_message.file: - kind = filetype.guess(input_message.file.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - input_message.file.seek(0) - image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' - input_message.file.close() - callback_data.update({'image': image}) - if callback_data['version'] == 'flux-pro-1.1': - images = [self._call_bfl_api(payload=callback_data)] - else: - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data["version"]}', - callback_data, - ) - images = runner if isinstance(runner, list) else [runner] + runner = replicate_run( + f'{self._CALLBACK_BASE}{callback_data.get('version', 'flux-schnell')}', + callback_data, + ) + images = runner if isinstance(runner, list) else [runner] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message) msgs = self.save_results(input_message.content, images, process_time, save) @@ -0,0 +1,244 @@ +import base64 +import time +from datetime import timedelta +from decimal import Decimal +from io import BytesIO + +import filetype +import requests +from django.core.files import File + +from backend import settings +from messages.models import Message +from ml_model.models import ( + ModelCategory, + ModelInput, + ModelParameter, + ModelPaymentRule, + ModelVersion, + NeuronModel, +) +from ml_model.services.base import SimpleService +from ml_model.tasks import replicate_run + + +class Fluxproultra(SimpleService): + """ + Flux Service + contains abstract method make, which makes a generation + """ + + title = 'Flux Pro Ultra' + description = 'Нейросеть, способная генерировать картинки из вашего текста' + category = ModelCategory(title='Изображения', slug='images') + versions = [ + ModelVersion(name='Flux-Pro1.1', slug='flux-pro-1.1'), + ModelVersion(name='Flux-Dev', slug='flux-dev'), + ModelVersion(name='Ultra', slug='flux-1.1-pro-ultra'), + ] + inputs = [ + ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE), + ] + parameters = [ + ModelParameter( + name='Ширина', + key='width', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 256, 'end': 1440, 'step': 32, 'default': 1024}, + ), + ModelParameter( + name='Высота', + key='height', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 256, 'end': 1440, 'step': 32, 'default': 768}, + ), + ModelParameter( + name='Ускорение генерации', + key='go_fast', + type=ModelParameter.TypeChoices.BOOL, + values={'default': True}, + ), + ModelParameter( + name='Приближенность к запросу', + key='guidance', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 0, 'end': 10, 'step': 1, 'default': 3}, + ), + ModelParameter( + name='Мегапиксели', + key='megapixels', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': [ + '1', + '0.25', + ], + 'default': '1', + }, + ), + ModelParameter( + name='Количество изображений', + key='num_outputs', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='Количество шагов вывода', + key='num_inference_steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 50, 'step': 1, 'default': 28}, + ), + ModelParameter( + name='Соотношение сторон', + key='aspect_ratio', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': [ + '1:1', + '16:9', + '21:9', + '3:2', + '2:3', + '4:5', + '5:4', + '3:4', + '4:3', + '9:16', + '9:21', + ], + 'default': '1:1', + }, + ), + ModelParameter( + name='Качество вывода (в %)', + key='output_quality', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 0, 'end': 100, 'step': 1, 'default': 80}, + ), + ModelParameter( + name='Апсемплинг', + key='prompt_upsampling', + type=ModelParameter.TypeChoices.BOOL, + values={'default': False}, + ), + ModelParameter( + name='Отключить пост-обработку', + key='raw', + type=ModelParameter.TypeChoices.BOOL, + values={'default': False}, + ), + ] + payments_rules = { + versions[0].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=4.00, + coefficient=2.00, + ), + versions[1].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=2.5, + coefficient=2.00, + ), + versions[2].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=6.00, + coefficient=2.00, + ), + } + + _CALLBACK_BASE = 'black-forest-labs/' + + @property + def neuron_model(self): + return NeuronModel.objects.get(title='Flux Pro Ultra') + + def _call_bfl_api(self, payload: dict) -> list: + bfl_headers = { + 'Content-Type': 'application/json', + 'X-Key': settings.FLUX_API_KEY, + } + bfl_urls = { + 'generate': 'https://api.bfl.ml/v1/', + 'get': 'https://api.bfl.ml/v1/get_result?id=', + } + response = requests.post( + url=f'{bfl_urls["generate"]}{payload["version"]}', + headers=bfl_headers, + json=payload, + ) + if response.status_code != 200: + raise Exception(response.json()) + result = requests.get( + url=f'{bfl_urls["get"]}{response.json().get("id")}', + headers=bfl_headers, + ) + while result.json()['status'] not in ('Ready', 'Error'): + result = requests.get( + url=f'{bfl_urls["get"]}{response.json().get("id")}', + headers=bfl_headers, + ) + return result.json()['result']['sample'] + + def calculate_price(self, input_message: Message) -> Decimal: + version_slug = input_message.info.get('version', 'flux-pro-1.1') + payment_rule = self.payments_rules[version_slug] + if not payment_rule.pk: + payment_rule.model = self.neuron_model + payment_rule.save() + if version_slug in (self.versions[1].slug,): + return payment_rule.rate * input_message.info.get('num_outputs', 1) + else: + return payment_rule.rate + + def save_results( + self, + prompt: str, + images: list, + time: timedelta, + save: bool = True, + ) -> list[Message]: + messages: list[Message] = [] + for image in images: + messages.append( + Message( + content_object=self.store, + elapsed_time=time, + content=prompt, + file=File(BytesIO(requests.get(image).content), '.png'), + ) + ) + if save: + return Message.objects.bulk_create(messages) + return messages + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + start_time = time.time() + callback_data = dict( + { + 'prompt': self.translate_prompt(input_message.content), + **input_message.info, + } + ) + if input_message.file: + kind = filetype.guess(input_message.file.read(20)) + mime = kind.mime if kind else 'application/octet-stream' + input_message.file.seek(0) + image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' + input_message.file.close() + callback_data.update({'image': image}) + if callback_data['version'] == 'flux-pro-1.1': + images = [self._call_bfl_api(payload=callback_data)] + else: + runner = replicate_run( + f'{self._CALLBACK_BASE}{callback_data["version"]}', + callback_data, + ) + images = runner if isinstance(runner, list) else [runner] + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model, input_message) + msgs = self.save_results(input_message.content, images, process_time, save) + return msgs @@ -3,10 +3,11 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO -from typing import Any, Iterator +from typing import Any, Iterator, Dict import filetype from PIL import Image +from django.db.models.fields.files import FieldFile from messages.models import Message from ml_model.services.base import SimpleService @@ -24,19 +25,20 @@ class Grok(SimpleService): TOKENS_COST = { 'grok-2-vision-1212': { - 'input': Decimal('400'), + 'input': Decimal('2000'), 'output': Decimal('2000'), 'input_imgs': Decimal('720'), }, # 1M tokens and 1K imgs } - def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int, image: FieldFile) -> Decimal: price_map = self.TOKENS_COST[version.split('/')[1]] price = ( input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 - + price_map['input_imgs'] / 1_000 ) + if image: + price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: @@ -77,11 +79,12 @@ class Grok(SimpleService): version=version, input_tokens=result[1], output_tokens=result[2], + image=image ) msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: if isinstance(self.store, Chat): air_messages = list( reversed( @@ -0,0 +1,103 @@ +import time +from datetime import timedelta +from decimal import Decimal +from typing import Iterator, Any, Dict + +from messages.models import Message +from ml_model.models import ModelCategory, ModelVersion, ModelInput, ModelParameter +from ml_model.services.base import SimpleService +from ml_model.tasks import openrouter_run +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + + +class Perplexity(SimpleService): + """ + Perplexity Service + contains abstract method make, which makes a generation + """ + + TOKENS_COST = { + 'sonar': {'input': Decimal('200'), 'output': Decimal('200')}, # 1M tokens + } + + def calculate_price( + self, version: str, input_tokens: int, output_tokens: int + ) -> Decimal: + price_map = self.TOKENS_COST[version.split('/')[1]] + price = ( + input_tokens * price_map['input'] / 1_000_000 + + output_tokens * price_map['output'] / 1_000_000 + ) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results( + self, content: Iterator[Any], 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]: + start_time = time.time() + version = f'perplexity/{input_message.info.pop('version', 'sonar')}' + callback_data = { + 'provider': { + 'order': ['Perplexity'] + }, + **input_message.info + } + messages = self.get_chat_history() + result = openrouter_run(version, messages, callback_data, self.title) + 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] + ) + 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]: + 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')[:message_limit] + ) + ) + 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: + memory.pop(0) + character_length = sum(len(content['content']) for content in memory) + + return memory @@ -1,7 +1,7 @@ import time from datetime import timedelta from decimal import Decimal -from typing import Any, Iterator +from typing import Any, Iterator, Dict from messages.models import Message from ml_model.services.base import SimpleService @@ -18,7 +18,7 @@ class Qwen(SimpleService): """ TOKENS_COST = { - 'qwq-32b': {'input': Decimal('24'), 'output': Decimal('36')}, # 1M tokens + 'qwq-32b': {'input': Decimal('36'), 'output': Decimal('36')}, # 1M tokens 'qwq-32b:free': {'input': Decimal('0'), 'output': Decimal('0')}, # 1M tokens } @@ -57,7 +57,7 @@ class Qwen(SimpleService): msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: if isinstance(self.store, Chat): air_messages = list( reversed( @@ -149,10 +149,10 @@ class ModelTagAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin 'fill': 'currentColor', 'stroke': 'currentColor', }.items(): - print(element.tag == 'svg' and attr == 'stroke') if element.tag == 'svg' and attr == 'stroke': continue - element.attrib[attr] = val + if attr in element.attrib: + element.attrib[attr] = val buf = BytesIO() ET.ElementTree(root).write( buf, encoding='utf-8', xml_declaration=True, default_namespace={}.get(None) @@ -117,6 +117,7 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name base_url='https://openrouter.ai/api/v1', headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, proxy=f'{proxy.protocol}://{proxy.address}', + timeout=600, ) as client: resp = client.post( 'chat/completions',