@@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 10:09+0300\n" +"POT-Creation-Date: 2026-07-20 11:08+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -108,7 +108,7 @@ msgstr "При отправке письма произошла неизвест msgid "Email token not found. Please contact support" msgstr "E-mail токен не найден. Пожалуйста, свяжитесь со службой поддержки" -#: authentication/exceptions/email_token.py:6 authentication/routes/v2.py:32 +#: authentication/exceptions/email_token.py:6 authentication/routes/v2.py:31 msgid "No email token found" msgstr "Email токен не найден" @@ -116,7 +116,7 @@ msgstr "Email токен не найден" msgid "Wrong email" msgstr "Неверный email" -#: authentication/exceptions/user.py:11 backend/urls.py:49 +#: authentication/exceptions/user.py:11 backend/urls.py:97 msgid "Wrong password" msgstr "Неверный пароль" @@ -140,39 +140,39 @@ msgstr "Пользователь уже существует" msgid "Domain not found" msgstr "Домен не найден" -#: authentication/models/business_account.py:16 payments/models/promocode.py:72 +#: authentication/models/business_account.py:18 payments/models/promocode.py:72 #: tools/public_api/models.py:34 tools/public_api/services/api_key.py:24 msgid "Owner" msgstr "Владелец" -#: authentication/models/business_account.py:20 +#: authentication/models/business_account.py:22 msgid "Show balance" msgstr "Показать баланс" -#: authentication/models/business_account.py:25 +#: authentication/models/business_account.py:27 msgid "Account privileges" msgstr "Тип аккаунта" -#: authentication/models/business_account.py:31 +#: authentication/models/business_account.py:33 #: authentication/models/business_group.py:13 #: authentication/models/whitelist.py:13 msgid "Company" msgstr "Компания" -#: authentication/models/business_account.py:37 +#: authentication/models/business_account.py:39 msgid "Acceptance" msgstr "Подтверждение" -#: authentication/models/business_account.py:44 +#: authentication/models/business_account.py:46 #: authentication/models/business_group.py:21 tools/public_api/models.py:44 msgid "Token limit" msgstr "Лимит токенов" -#: authentication/models/business_account.py:52 +#: authentication/models/business_account.py:54 msgid "Group" msgstr "Группа" -#: authentication/models/business_account.py:61 +#: authentication/models/business_account.py:63 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" @@ -180,16 +180,16 @@ msgstr "" "Невозможно добавить сотрудника к группе, когда он не принадлежит данной " "компании" -#: authentication/models/business_account.py:70 +#: authentication/models/business_account.py:80 msgid "Child Business Account" msgstr "Дочерний Бизнес Аккаунт" -#: authentication/models/business_account.py:71 +#: authentication/models/business_account.py:81 msgid "Child Business Accounts" msgstr "Дочерние Бизнес Аккаунты" -#: authentication/models/business_group.py:8 ml_model/models.py:17 -#: ml_model/models.py:37 ml_model/models.py:61 ml_model/models.py:269 +#: authentication/models/business_group.py:8 ml_model/models.py:19 +#: ml_model/models.py:39 ml_model/models.py:63 ml_model/models.py:279 #: tools/chats/models.py:9 tools/media/models.py:59 tools/media/models.py:95 msgid "Title" msgstr "Название" @@ -203,10 +203,10 @@ msgid "Business Groups" msgstr "Бизнес Группы" #: authentication/models/business_host.py:22 -#: authentication/models/email_token.py:13 authentication/models/user.py:223 -#: authentication/models/user.py:224 authentication/models/user_telegram.py:22 +#: authentication/models/email_token.py:13 authentication/models/user.py:233 +#: authentication/models/user.py:234 authentication/models/user_telegram.py:22 #: authentication/models/user_vk.py:12 payments/admin.py:37 -#: payments/admin.py:95 payments/models/invoice.py:15 +#: payments/admin.py:124 payments/models/invoice.py:15 #: payments/models/payment.py:26 payments/models/payment_plan.py:43 #: tools/media/models.py:108 msgid "User" @@ -217,7 +217,7 @@ msgid "Affiliated by" msgstr "Кем привлечена" #: authentication/models/business_host.py:36 authentication/models/user.py:135 -#: authentication/models/whitelist.py:16 ml_model/models.py:167 +#: authentication/models/whitelist.py:16 ml_model/models.py:177 #: payments/models/promocode.py:85 msgid "Is active" msgstr "Является активной" @@ -258,7 +258,7 @@ msgstr "ИНН" msgid "PSRN" msgstr "ОГРН" -#: authentication/models/business_host.py:77 ml_model/models.py:180 +#: authentication/models/business_host.py:77 ml_model/models.py:190 #: tools/public_api/models.py:31 tools/public_api/services/api_key.py:24 msgid "Name" msgstr "Наименование" @@ -363,7 +363,7 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:16 ml_model/models.py:271 +#: authentication/models/email_token.py:16 ml_model/models.py:281 msgid "Key" msgstr "Ключ" @@ -507,35 +507,35 @@ msgstr "Вайтлисты для отмены политик" msgid "Invalid or expired refresh token" msgstr "Неверный или истёкший refresh токен" -#: authentication/routes/v2.py:25 +#: authentication/routes/v2.py:24 msgid "User is already confirmed" msgstr "Аккаунт уже подтвержден" -#: authentication/routes/v2.py:37 authentication/views.py:489 +#: authentication/routes/v2.py:36 authentication/views.py:489 msgid "Could not confirm email, please try again." msgstr "Невозможно подтвердить email, попробуйте позже" -#: authentication/security.py:35 +#: authentication/security.py:34 #, fuzzy #| msgid "Hidden" msgid "Forbidden" msgstr "Скрытый" -#: authentication/security.py:48 +#: authentication/security.py:47 msgid "Access token is expired" msgstr "Срок действия токена доступа истек" -#: authentication/security.py:50 +#: authentication/security.py:49 #, fuzzy #| msgid "Access token is expired" msgid "Access token invalid" msgstr "Срок действия токена доступа истек" -#: authentication/security.py:63 +#: authentication/security.py:62 msgid "User not found" msgstr "Пользователь не найден" -#: authentication/security.py:98 authentication/security.py:126 +#: authentication/security.py:97 msgid "Access token expired or does not exist" msgstr "Токен доступа просрочен или не существует" @@ -549,7 +549,7 @@ msgstr "" msgid "Host user is not registered for this account" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" -#: authentication/selectors/user_selector.py:80 +#: authentication/selectors/user_selector.py:75 msgid "No user with this uid found" msgstr "Не найден пользователь с данным ID" @@ -565,23 +565,23 @@ msgstr "Приглашенный аккаунт может принять или msgid "Error occured when proceed email sending" msgstr "Случилась ошибка во время отправки email" -#: authentication/services/user_services.py:164 +#: authentication/services/user_services.py:167 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:181 +#: authentication/services/user_services.py:184 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:205 +#: authentication/services/user_services.py:208 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:215 +#: authentication/services/user_services.py:218 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:253 +#: authentication/services/user_services.py:256 msgid "Current password is wrong" msgstr "Текущий пароль неверен" @@ -610,11 +610,15 @@ msgstr "Повторное приглашение сотруднику успе msgid "Business account password has been updated" msgstr "Пароль сотрудника успешно обновлен" -#: backend/urls.py:39 +#: backend/urls.py:79 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:44 +#: backend/urls.py:86 +msgid "Data size limit exceeded. Please reduce the size" +msgstr "Превышен лимит размера данных. Уменьшите размер" + +#: backend/urls.py:92 msgid "Token is invalid" msgstr "" @@ -628,7 +632,14 @@ msgstr "" msgid "A %(model)s with fields %(fields)s already exists" msgstr "Уже существует %(model)s с полями %(fields)s" +#: lib/parsers.py:18 +#, fuzzy +#| msgid "Invalid info payload" +msgid "Invalid JSON payload" +msgstr "Некорректные данные в поле info" + #: messages/serializers.py:44 ml_model/exceptions.py:86 +#: tools/chats/schemas.py:23 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" @@ -638,7 +649,7 @@ msgstr "Файл не может быть размером больше %(max_mb msgid "Version %(version)s already has input with the same type: %(type)s" msgstr "Версия %(version)s уже имеет входные данные с таким же типом: %(type)s" -#: ml_model/apps.py:9 ml_model/models.py:148 +#: ml_model/apps.py:9 ml_model/models.py:158 msgid "Neuron Models" msgstr "Нейронные Модели" @@ -780,332 +791,322 @@ msgid "" msgstr "" "Версия «%(version)s» недоступна. Доступные версии: %(available_versions)s." -#: ml_model/models.py:18 ml_model/models.py:38 ml_model/models.py:70 -#: ml_model/models.py:182 tools/media/models.py:67 +#: ml_model/management/commands/create_indexes.py:27 +msgid "Redis is unavailable" +msgstr "Redis недоступен" + +#: ml_model/models.py:20 ml_model/models.py:40 ml_model/models.py:72 +#: ml_model/models.py:192 tools/media/models.py:67 msgid "Slug" msgstr "Ярлык" -#: ml_model/models.py:28 ml_model/models.py:80 +#: ml_model/models.py:30 ml_model/models.py:82 msgid "Category" msgstr "Категория" -#: ml_model/models.py:29 +#: ml_model/models.py:31 msgid "Categories" msgstr "Категории" -#: ml_model/models.py:42 +#: ml_model/models.py:44 msgid "Not SVG-pictures not allowed" msgstr "Нельзя использовать не SVG-картинки" -#: ml_model/models.py:45 +#: ml_model/models.py:47 msgid "Icon" msgstr "Миниатюра" -#: ml_model/models.py:52 +#: ml_model/models.py:54 msgid "Model Tag" msgstr "Тег модели" -#: ml_model/models.py:53 +#: ml_model/models.py:55 msgid "Model Tags" msgstr "Теги модели" -#: ml_model/models.py:66 +#: ml_model/models.py:68 msgid "Alternative Titles" msgstr "Альтернативные названия" -#: ml_model/models.py:68 ml_model/models.py:181 ml_model/models.py:270 +#: ml_model/models.py:70 ml_model/models.py:191 ml_model/models.py:280 #: payments/models/payment.py:52 msgid "Description" msgstr "Описание" -#: ml_model/models.py:72 +#: ml_model/models.py:74 msgid "Fill automatically, don't touch" msgstr "Заполняется автоматически, не трогать" -#: ml_model/models.py:88 +#: ml_model/models.py:90 msgid "Avatar" msgstr "Аватар" -#: ml_model/models.py:91 +#: ml_model/models.py:93 msgid "Tags" msgstr "Теги" -#: ml_model/models.py:147 payments/models/payment_plan_feature.py:18 +#: ml_model/models.py:157 payments/models/payment_plan_feature.py:18 msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:156 ml_model/models.py:403 payments/admin.py:101 +#: ml_model/models.py:166 ml_model/models.py:413 payments/admin.py:130 msgid "Model" msgstr "Модель" -#: ml_model/models.py:172 ml_model/models.py:173 +#: ml_model/models.py:182 ml_model/models.py:183 msgid "Settings" msgstr "Настройки" -#: ml_model/models.py:176 +#: ml_model/models.py:186 #, python-format msgid "Settings of %(model_title)s" msgstr "Настройки %(model_title)s" -#: ml_model/models.py:195 +#: ml_model/models.py:205 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "%(model_title)s | %(version_name)s" -#: ml_model/models.py:201 +#: ml_model/models.py:211 msgid "Model Version" msgstr "Версия Модели" -#: ml_model/models.py:202 +#: ml_model/models.py:212 msgid "Model Versions" msgstr "Версии Модели" -#: ml_model/models.py:211 +#: ml_model/models.py:221 msgid "Versions" msgstr "Версии" -#: ml_model/models.py:212 +#: ml_model/models.py:222 msgid "Link to versions" msgstr "Привязка к версиям" -#: ml_model/models.py:221 +#: ml_model/models.py:231 msgid "Text" msgstr "Текст" -#: ml_model/models.py:222 +#: ml_model/models.py:232 msgid "Image" msgstr "Картинка" -#: ml_model/models.py:223 +#: ml_model/models.py:233 msgid "PDF" msgstr "PDF" -#: ml_model/models.py:224 +#: ml_model/models.py:234 msgid "DOCX" msgstr "DOCX" -#: ml_model/models.py:225 +#: ml_model/models.py:235 msgid "DOC" msgstr "DOC" -#: ml_model/models.py:226 +#: ml_model/models.py:236 msgid "Text File (Notebook)" msgstr "Текстовый файл (Блокнот)" -#: ml_model/models.py:227 +#: ml_model/models.py:237 msgid "ZIP Archive" msgstr "ZIP архив" -#: ml_model/models.py:228 payments/tests/test_plans.py:35 +#: ml_model/models.py:238 payments/tests/test_plans.py:35 msgid "Audio" msgstr "Аудио" -#: ml_model/models.py:229 payments/tests/test_plans.py:31 +#: ml_model/models.py:239 payments/tests/test_plans.py:31 msgid "Video" msgstr "Видео" -#: ml_model/models.py:235 ml_model/models.py:273 +#: ml_model/models.py:245 ml_model/models.py:283 #: payments/models/promocode.py:41 msgid "Type" msgstr "Тип" -#: ml_model/models.py:237 ml_model/models.py:284 +#: ml_model/models.py:247 ml_model/models.py:294 msgid "Required" msgstr "Обязательный" -#: ml_model/models.py:240 +#: ml_model/models.py:250 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "%(model_title)s | %(input_type)s" -#: ml_model/models.py:246 +#: ml_model/models.py:256 msgid "Model Input" msgstr "Модель" -#: ml_model/models.py:247 +#: ml_model/models.py:257 msgid "Model Inputs" msgstr "Входящий поток модели" -#: ml_model/models.py:252 +#: ml_model/models.py:262 msgid "Integer" msgstr "Целое число" -#: ml_model/models.py:253 +#: ml_model/models.py:263 msgid "Float" msgstr "Вещественное число" -#: ml_model/models.py:254 +#: ml_model/models.py:264 msgid "String" msgstr "Строка" -#: ml_model/models.py:257 +#: ml_model/models.py:267 msgid "List" msgstr "Список" -#: ml_model/models.py:261 +#: ml_model/models.py:271 msgid "Float range" msgstr "Вещественный диапазон" -#: ml_model/models.py:265 +#: ml_model/models.py:275 msgid "Integer range" msgstr "Целочисленный диапазон" -#: ml_model/models.py:267 +#: ml_model/models.py:277 msgid "Logical" msgstr "Логический" -#: ml_model/models.py:280 +#: ml_model/models.py:290 msgid "Values" msgstr "Значения" -#: ml_model/models.py:281 +#: ml_model/models.py:291 msgid "" "These values can contain different interfaces and default value optional" msgstr "" "Значения могут содержать различные интерфейс и, опционально, значение по " "умолчанию" -#: ml_model/models.py:283 +#: ml_model/models.py:293 msgid "Hidden" msgstr "Скрытый" -#: ml_model/models.py:289 +#: ml_model/models.py:299 #, python-format msgid "Parameter of %(model_title)s" msgstr "Параметр %(model_title)s" -#: ml_model/models.py:292 +#: ml_model/models.py:302 msgid "Parameter" msgstr "Параметр" -#: ml_model/models.py:293 +#: ml_model/models.py:303 msgid "Parameters" msgstr "Параметры" -#: ml_model/models.py:298 +#: ml_model/models.py:308 msgid "Fixed" msgstr "Фикса" -#: ml_model/models.py:299 +#: ml_model/models.py:309 msgid "Per generation second" msgstr "За секунду генерации" -#: ml_model/models.py:300 +#: ml_model/models.py:310 msgid "Per one text token" msgstr "За один текстовый токен" -#: ml_model/models.py:301 +#: ml_model/models.py:311 msgid "Per image pixel" msgstr "За один пиксель" -#: ml_model/models.py:304 +#: ml_model/models.py:314 msgid "By input data" msgstr "По входящим данным" -#: ml_model/models.py:305 +#: ml_model/models.py:315 msgid "By output data" msgstr "По исходящим данным" -#: ml_model/models.py:306 +#: ml_model/models.py:316 msgid "By all data" msgstr "По всем данным" -#: ml_model/models.py:311 +#: ml_model/models.py:321 msgid "Strategy" msgstr "Стратегия" -#: ml_model/models.py:316 +#: ml_model/models.py:326 msgid "Interaction Type" msgstr "Тип взаимодействия" -#: ml_model/models.py:321 payments/models/invoice.py:19 +#: ml_model/models.py:331 payments/models/invoice.py:19 msgid "Cost" msgstr "Цена" -#: ml_model/models.py:322 +#: ml_model/models.py:332 msgid "In RUB, per specified strategy" msgstr "В рублях, за указанную стратегию" -#: ml_model/models.py:327 +#: ml_model/models.py:337 msgid "Coefficient" msgstr "Коэффициент" -#: ml_model/models.py:328 +#: ml_model/models.py:338 msgid "Cost multiplier" msgstr "Цена" -#: ml_model/models.py:335 +#: ml_model/models.py:345 msgid "Rate" msgstr "Ставка" -#: ml_model/models.py:339 +#: ml_model/models.py:349 msgid "Payment Rule" msgstr "Платежное правило" -#: ml_model/models.py:340 +#: ml_model/models.py:350 msgid "Payment Rules" msgstr "Платежные правила" -#: ml_model/models.py:401 +#: ml_model/models.py:411 msgid "Descriptor" msgstr "Дескриптор" -#: ml_model/models.py:407 +#: ml_model/models.py:417 #, python-format msgid "Instruction of %(model_title)s" msgstr "Инструкция %(model_title)s" -#: ml_model/models.py:410 +#: ml_model/models.py:420 msgid "Model Instruction" msgstr "Инструкция Модели" -#: ml_model/models.py:411 +#: ml_model/models.py:421 msgid "Model Instructions" msgstr "Инструкции Моделей" -#: ml_model/selectors/ml_models_selector.py:122 +#: ml_model/selectors/ml_models_selector.py:114 msgid "no model by this id" msgstr "Не найдено моделей по этому ID" -#: ml_model/services/FileService.py:110 tools/media/apis.py:258 +#: ml_model/services/FileService.py:110 tools/media/apis.py:280 #: tools/public_api/views/ml_service.py:56 -#: tools/public_api/views/providers/openai_compatible.py:209 +#: tools/public_api/views/providers/openai_compatible.py:208 msgid "Voice not found." msgstr "Голос не найден." -#: ml_model/services/chatgpt_5.py:133 ml_model/services/chatgpt_5_4.py:159 -#: ml_model/services/chatgpt_5_5.py:210 -msgid "The \"Use code\" option cannot be used together with an attached image." -msgstr "" -"Нельзя одновременно использовать параметр «Использовать код» вместе с " -"прикреплённым изображением." - -#: ml_model/services/chatgpt_5_4.py:318 ml_model/services/chatgpt_5_5.py:362 +#: ml_model/services/chatgpt.py:244 msgid "Image is ready" msgstr "Изображение готово" -#: ml_model/services/chatgpt_5_5.py:188 +#: ml_model/services/chatgpt.py:360 ml_model/services/claude.py:266 +#: ml_model/services/grok.py:190 msgid "File analysis" msgstr "Анализ файлов" +#: ml_model/services/chatgpt.py:382 ml_model/services/chatgpt_5.py:133 +msgid "The \"Use code\" option cannot be used together with an attached image." +msgstr "" +"Нельзя одновременно использовать параметр «Использовать код» вместе с " +"прикреплённым изображением." + #: ml_model/services/elevenlabs_music.py:45 msgid "Duration cannot be less than 5 seconds" msgstr "Длительность не может быть меньше 5 секунд" -#: ml_model/services/hunyuan.py:103 -#, python-format -msgid "This video duration is not allowed for %(quality)s quality." -msgstr "" -"Для качества %(quality)s такая продолжительность видео не поддерживается." - -#: ml_model/services/hunyuan.py:108 -msgid "" -"Smooth motion mode is available only for 5-second videos at 540p and 720p " -"quality" -msgstr "" -"Режим «Плавное движение» доступен только для 5-секундных видео в качестве " -"540p и 720p" - #: ml_model/services/minio_service.py:37 ml_model/services/minio_service.py:55 #: ml_model/services/minio_service.py:63 ml_model/services/minio_service.py:72 msgid "Unknown bucket destination" @@ -1115,7 +1116,7 @@ msgstr "Неизвестный бакет для загрузки" msgid "1080p output is not supported for Seedance Dreamina 2.0 Fast." msgstr "1080р разрешение не поддерживается для Seedance Dreamina 2.0 Fast." -#: ml_model/services/seedream.py:90 +#: ml_model/services/seedream.py:84 msgid "3K output is not supported for this model" msgstr "3К разрешение не поддерживается для этой модели" @@ -1123,7 +1124,7 @@ msgstr "3К разрешение не поддерживается для это msgid "No image given for improving" msgstr "Нет изображения для улучшения" -#: ml_model/tasks.py:137 +#: ml_model/tasks.py:144 msgid "Lyrics is too long" msgstr "Текст песни слишком длинный" @@ -1131,13 +1132,13 @@ msgstr "Текст песни слишком длинный" msgid "Model data cannot be retrieved" msgstr "Невозможно получить данные модели" -#: payments/admin.py:35 payments/admin.py:67 payments/admin.py:93 +#: payments/admin.py:35 payments/admin.py:76 payments/admin.py:122 msgid "You can search by user email, exacted company name" msgstr "" "Вы можете осуществлять поиск по e-mail пользователя, точному названию " "компании" -#: payments/admin.py:40 payments/admin.py:98 +#: payments/admin.py:40 payments/admin.py:127 msgid "Missing" msgstr "Отсутствующий" @@ -1178,10 +1179,6 @@ msgstr "Списания" msgid "Amount" msgstr "Количество" -#: lib/middleware.py:40 -msgid "Data size limit exceeded. Please reduce the size" -msgstr "Превышен лимит размера данных. Уменьшите размер" - #: payments/models/payment.py:41 msgid "Plan" msgstr "План" @@ -1359,19 +1356,19 @@ msgstr "Попытки" msgid "Payment Methods" msgstr "Платежные методы" -#: payments/routes/v1.py:92 +#: payments/routes/v1.py:93 msgid "You do not have an active subscription to cancel" msgstr "У вас нет активной подписки для отмены" -#: payments/routes/v1.py:93 +#: payments/routes/v1.py:94 msgid "The recurring payment is successfully cancelled" msgstr "Автоплатежи успешно отключены" -#: payments/routes/v1.py:143 +#: payments/routes/v1.py:144 msgid "Expenses" msgstr "Затраты" -#: payments/routes/v1.py:147 +#: payments/routes/v1.py:148 msgid "Refills" msgstr "Пополнения" @@ -1379,10 +1376,6 @@ msgstr "Пополнения" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/services/model_billing_service.py:31 -msgid "Unknown account type" -msgstr "Неизвестный тип аккаунта" - #: payments/tests/test_plans.py:23 payments/tests/test_plans.py:205 #: payments/tests/test_plans.py:208 msgid "Chat-bots" @@ -1425,8 +1418,8 @@ msgstr "Публичный API" msgid "Media" msgstr "Медиа" -#: tools/chats/apis.py:205 tools/media/apis.py:211 -#: tools/public_api/views/base.py:104 +#: tools/chats/apis.py:201 tools/media/apis.py:229 +#: tools/public_api/views/base.py:102 msgid "" "An unexpected generation error has occurred. Please try again later or use a " "different model" @@ -1434,7 +1427,7 @@ msgstr "" "Произошла непредвиденная ошибка при генерации. Пожалуйста попробуйте позже " "или используйте другую модель" -#: tools/chats/apis.py:261 +#: tools/chats/apis.py:257 msgid "The message has already been deleted" msgstr "Сообщение уже было удалено" @@ -1447,7 +1440,33 @@ msgstr "Чат %(id)s" msgid "Chat" msgstr "Чат" -#: tools/media/apis.py:177 +#: tools/chats/routes/v1.py:37 tools/public_api/routes/v1.py:66 +#, fuzzy +#| msgid "User not found" +msgid "Stream not found" +msgstr "Пользователь не найден" + +#: tools/chats/routes/v1.py:51 +msgid "Chat not found" +msgstr "Чат не найден" + +#: tools/chats/routes/v1.py:54 tools/public_api/routes/v1.py:106 +msgid "Stream not supported for this model" +msgstr "Стриминг не поддерживается для этой модели" + +#: tools/chats/routes/v1.py:58 +msgid "Stream already in progress" +msgstr "" + +#: tools/chats/schemas.py:34 tools/public_api/views/ml_service.py:88 +msgid "Invalid info payload" +msgstr "Некорректные данные в поле info" + +#: tools/chats/services/sse_chat_stream.py:54 +msgid "Stream timeout" +msgstr "" + +#: tools/media/apis.py:221 msgid "" "Temporary issues with the service, we are already working on a solution." msgstr "Временные неполадки с сервисом, мы уже работаем над их решением." @@ -1508,19 +1527,11 @@ msgstr "Неизвестный файл" msgid "Voices" msgstr "Голоса" -#: tools/media/routes/v1.py:76 tools/public_api/views/voice.py:100 +#: tools/media/routes/v1.py:75 tools/public_api/views/voice.py:100 #: tools/public_api/views/voice.py:114 msgid "Voice not found" msgstr "Голос не найден" -#: tools/chats/routes/v1.py:36 -msgid "Chat not found" -msgstr "Чат не найден" - -#: tools/chats/routes/v1.py:41 -msgid "Stream not supported for this model" -msgstr "Стриминг не поддерживается для этой модели" - #: tools/public_api/exceptions.py:7 msgid "Upgrade token limit on your api-key" msgstr "Необходимо повысить лимит токенов у API-ключа" @@ -1541,60 +1552,113 @@ msgstr "API Ключ" msgid "API Keys" msgstr "API Ключи" -#: tools/public_api/views/base.py:65 -msgid "Key limit exceeded" -msgstr "Превышен лимит по ключу" +#: tools/public_api/routes/providers/openai.py:19 +#, fuzzy +#| msgid "Missing required parameter: model_id" +msgid "Missing required parameter: input" +msgstr "Отсутствует обязательный параметр: 'model_id'" -#: tools/public_api/views/base.py:70 -msgid "Model is blocked by outdating or temporary block, please retry later" -msgstr "" -"Модель заблокирована, т.к закончила обновляться или временно заблокирована, " -"попробуйте позже" +#: tools/public_api/routes/providers/openai.py:24 +#, fuzzy +#| msgid "Invalid info payload" +msgid "Invalid input payload" +msgstr "Некорректные данные в поле info" -#: tools/public_api/views/base.py:77 +#: tools/public_api/routes/providers/openai.py:58 +#: tools/public_api/routes/v1.py:109 tools/public_api/views/base.py:75 msgid "The request must not be empty" msgstr "Запрос не должен быть пустым" -#: tools/public_api/views/ml_service.py:88 -msgid "Invalid info payload" -msgstr "Некорректные данные в поле info" - -#: tools/public_api/views/providers/elevenlabs_compatible.py:133 -msgid "Missing required parameter: model_id" -msgstr "Отсутствует обязательный параметр: 'model_id'" - +#: tools/public_api/routes/providers/openai.py:85 #: tools/public_api/views/providers/elevenlabs_compatible.py:147 #: tools/public_api/views/providers/openai_compatible.py:110 -#: tools/public_api/views/providers/openai_compatible.py:246 +#: tools/public_api/views/providers/openai_compatible.py:245 msgid "Model not found" msgstr "Модель не найдена" +#: tools/public_api/routes/providers/openai.py:118 +#, fuzzy +#| msgid "File Uploading Not supported" +msgid "Only streaming is supported" +msgstr "Загрузка файлов не поддерживается" + +#: tools/public_api/routes/providers/openai.py:120 #: tools/public_api/views/providers/openai_compatible.py:61 msgid "You must provide a model parameter" msgstr "Необходимо указать параметр 'model'" +#: tools/public_api/routes/providers/openai.py:136 +msgid "Only streaming reconnect is supported" +msgstr "" + +#: tools/public_api/routes/providers/openai.py:138 +msgid "message_uuid is not provided" +msgstr "" + +#: tools/public_api/routes/v1.py:31 +msgid "No API Key in Authorization header" +msgstr "" + +#: tools/public_api/routes/v1.py:43 +#, fuzzy +#| msgid "API Key not found" +msgid "API key not found" +msgstr "API-ключ не найден" + +#: tools/public_api/routes/v1.py:46 +#, fuzzy +#| msgid "Access token is expired" +msgid "API key expired" +msgstr "Срок действия токена доступа истек" + +#: tools/public_api/routes/v1.py:48 +#, fuzzy +#| msgid "Key limit exceeded" +msgid "API key limit exceeded" +msgstr "Превышен лимит по ключу" + +#: tools/public_api/routes/v1.py:62 tools/public_api/routes/v1.py:96 +#, fuzzy +#| msgid "Host user is not registered for this account" +msgid "API key is not available for this account type" +msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" + +#: tools/public_api/routes/v1.py:104 tools/public_api/views/base.py:68 +msgid "Model is blocked by outdating or temporary block, please retry later" +msgstr "" +"Модель заблокирована, т.к закончила обновляться или временно заблокирована, " +"попробуйте позже" + +#: tools/public_api/views/base.py:63 +msgid "Key limit exceeded" +msgstr "Превышен лимит по ключу" + +#: tools/public_api/views/providers/elevenlabs_compatible.py:133 +msgid "Missing required parameter: model_id" +msgstr "Отсутствует обязательный параметр: 'model_id'" + #: tools/public_api/views/providers/openai_compatible.py:67 msgid "Missing required parameter: 'messages'" msgstr "Отсутствует обязательный параметр: 'messages'" -#: tools/public_api/views/providers/openai_compatible.py:177 +#: tools/public_api/views/providers/openai_compatible.py:176 msgid "Missing audio_sample." msgstr "Отсутствует параметр audio_sample." -#: tools/public_api/views/providers/openai_compatible.py:232 +#: tools/public_api/views/providers/openai_compatible.py:231 #, python-format msgid "Missing required parameter: %(param)s" msgstr "Отсутствует обязательный параметр: %(param)s" -#: tools/public_api/views/providers/openai_compatible.py:250 +#: tools/public_api/views/providers/openai_compatible.py:249 msgid "Voice must be an object with id." msgstr "Поле voice должно быть объектом с полем id." -#: tools/public_api/views/providers/openai_compatible.py:256 +#: tools/public_api/views/providers/openai_compatible.py:255 msgid "Voice id is empty." msgstr "Идентификатор голоса не указан." -#: tools/public_api/views/providers/openai_compatible.py:287 +#: tools/public_api/views/providers/openai_compatible.py:286 msgid "Failed to fetch generated audio" msgstr "Не удалось получить сгенерированное аудио" @@ -1610,6 +1674,21 @@ msgstr "Название голоса успешно обновлено" msgid "Preset voices are shared and cannot be deleted. Use your own voice id." msgstr "Пресеты общие и не удаляются. Используйте id собственного голоса." +#, python-format +#~ msgid "This video duration is not allowed for %(quality)s quality." +#~ msgstr "" +#~ "Для качества %(quality)s такая продолжительность видео не поддерживается." + +#~ msgid "" +#~ "Smooth motion mode is available only for 5-second videos at 540p and 720p " +#~ "quality" +#~ msgstr "" +#~ "Режим «Плавное движение» доступен только для 5-секундных видео в качестве " +#~ "540p и 720p" + +#~ msgid "Unknown account type" +#~ msgstr "Неизвестный тип аккаунта" + #~ msgid "No matching version found" #~ msgstr "Соответствующая версия не найдена" @@ -0,0 +1,10 @@ +class MessageService: + @classmethod + def prepare_output_message(cls, reasoning_text: str, output_text: str) -> str: + reasoning = reasoning_text.strip() + output = output_text.strip() + if reasoning and output: + return f'{reasoning}\n{output}' + if reasoning: + return f'{reasoning}' + return output @@ -7,6 +7,7 @@ from typing import Any, Generator, TypeAlias, TypedDict import httpx from backend import settings +from messages.services.message_service import MessageService from ml_model.exceptions import ( FileExtensionNotSupported, GenerationException, @@ -15,6 +16,7 @@ from ml_model.exceptions import ( RequestBlocked, ) from poller.models import Proxy +from tools.chats.domain import RawSSEChunk logger = logging.getLogger(__name__) @@ -70,7 +72,7 @@ class BytedanceVideoTaskResponse(TypedDict, total=False): RunChatResult: TypeAlias = tuple[str, int, int] -RunStreamChatResult: TypeAlias = Generator[str, None, BytedanceUsage] +RunStreamChatResult: TypeAlias = Generator[RawSSEChunk, None, BytedanceUsage] RunImageResult: TypeAlias = list[str] RunVideoResult: TypeAlias = tuple[str, int] BytedanceRunResult: TypeAlias = RunChatResult | RunImageResult | RunVideoResult @@ -131,7 +133,6 @@ class BytedanceModelArkAdapter: return str(error.get('code', '')) == 'InputTextSensitiveContentDetected' - @classmethod def _extract_chat_answer( cls, @@ -144,11 +145,14 @@ class BytedanceModelArkAdapter: for choice in choices if choice.get('finish_reason') in (BytedanceFinishReason.STOP, BytedanceFinishReason.LENGTH) and choice.get('message') - and choice['message'].get('content') is not None + and ( + choice['message'].get('content') is not None + or choice['message'].get('reasoning_content') is not None + ) ] if stop_choices: - content = ','.join(str(choice['message']['content']) for choice in stop_choices) - # TODO: включить после разделения reasoning и content в хранении + content = ','.join(choice['message'].get('content') or '' for choice in stop_choices) + reasoning = '' if include_reasoning: reasoning = ','.join( str(reasoning) @@ -156,11 +160,7 @@ class BytedanceModelArkAdapter: if choice.get('message') and (reasoning := choice['message'].get('reasoning_content')) is not None ) - if reasoning and content: - return f'**Рассуждение:**\n\n{reasoning}\n\n**Основная мысль:**\n\n{content}' - if reasoning: - return reasoning - return content + return MessageService.prepare_output_message(reasoning, content) cls._raise_by_error_payload(data, choices) @@ -286,8 +286,6 @@ class BytedanceModelArkAdapter: ) raise GenerationException usage: BytedanceUsage = {} - reasoning_started = False - content_started = False for line in resp.iter_lines(): if not line: continue @@ -305,16 +303,10 @@ class BytedanceModelArkAdapter: delta = choices[0].get('delta', {}) reasoning_chunk = delta.get('reasoning_content') or '' if include_reasoning and reasoning_chunk: - if not reasoning_started: - yield '**Рассуждение:**\n\n' - reasoning_started = True - yield reasoning_chunk + yield RawSSEChunk(event='think', data={'content': reasoning_chunk}) chunk = delta.get('content') or '' if chunk: - if include_reasoning and reasoning_started and not content_started: - yield '\n\n**Основная мысль:**\n\n' - content_started = True - yield chunk + yield RawSSEChunk(event='token', data={'content': chunk}) if ( fr := choices[0].get('finish_reason') ) and fr not in ( @@ -7,7 +7,9 @@ import httpx import tiktoken from backend import settings +from messages.services.message_service import MessageService from poller.models import Proxy +from tools.chats.domain import RawSSEChunk from .models import ModelResponse @@ -35,7 +37,7 @@ class OpenrouterAdapter: @classmethod def run_streaming_api( cls, version: str, messages: list, callback_data: dict, model_name: str - ) -> Iterator[str]: + ) -> Iterator[RawSSEChunk]: for proxy in Proxy.objects.all(): with httpx.Client( base_url=cls.BASE_URL, @@ -55,8 +57,6 @@ class OpenrouterAdapter: }, ) as resp: content = '' - # reasoning используем только для фоллбэк-подсчёта токенизатора - # в ответ не кладём, заполняет буфер истории сообщений reasoning = '' input_tokens = output_tokens = cost = 0 for line in resp.iter_lines(): @@ -70,11 +70,14 @@ class OpenrouterAdapter: try: data_obj = json.loads(data) - chunk = data_obj['choices'][0]['delta'].get('content') or '' - reasoning += data_obj['choices'][0]['delta'].get('reasoning') or '' - if chunk: - content += chunk - yield chunk + content_chunk = data_obj['choices'][0]['delta'].get('content') or '' + reasoning_chunk = data_obj['choices'][0]['delta'].get('reasoning') or '' + if content_chunk: + content += content_chunk + yield RawSSEChunk(event='token', data={'content': content_chunk}) + if reasoning_chunk: + reasoning += reasoning_chunk + yield RawSSEChunk(event='think', data={'content': reasoning_chunk}) if data_obj.get('usage'): input_tokens = data_obj['usage']['prompt_tokens'] output_tokens = data_obj['usage']['completion_tokens'] @@ -100,15 +103,24 @@ class OpenrouterAdapter: cls, version: str, messages: list, callback_data: dict, model_name: str ) -> ModelResponse: stream = cls.run_streaming_api(version, messages, callback_data, model_name) - content_parts: list[str] = [] + reasoning = '' + content = '' try: while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] except StopIteration as exc: input_tokens, output_tokens, cost = exc.value - return ModelResponse(''.join(content_parts), input_tokens, output_tokens, cost) + return ModelResponse( + MessageService.prepare_output_message(reasoning, content), + input_tokens, + output_tokens, + cost, + ) @classmethod def _fallback_tokenize(cls, model_name: str, messages: list, content: str) -> tuple[int, int]: @@ -1,6 +1,13 @@ +import time + import redis + from django.conf import settings +from django.core.management import CommandError from django.core.management.base import BaseCommand +from django.utils.translation import gettext as _ + + from redis.commands.search.field import TagField, TextField, VectorField from redis.commands.search.index_definition import IndexDefinition, IndexType @@ -11,6 +18,15 @@ class Command(BaseCommand): A command for creating indexes for storing a chunk's data (content, vectors, etc.) """ redis_client = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0) + for attempt in range(10): + try: + redis_client.ping() + break + except (redis.ConnectionError, redis.TimeoutError): + if attempt == 9: + raise CommandError(_('Redis is unavailable')) + time.sleep(2) + index_configs = ( ('ml_model-index', 3072), ('ml_model-index-1536', 1536), @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from decimal import Decimal -from typing import Any, Generator, Never +from typing import Any, Iterator, Never from asgiref.sync import async_to_sync from googletrans import Translator @@ -86,4 +86,4 @@ class SimpleService(ABC): class StreamSimpleService(SimpleService): @abstractmethod - def make_stream(self, input_message: Message, save: bool = True) -> Generator: ... + def make_stream(self, input_message: Message, save: bool = True) -> Iterator: ... @@ -37,6 +37,7 @@ from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector from poller.models import Proxy +from tools.chats.domain import RawSSEChunk class Chatgpt(Chatgpt_4, StreamSimpleService, OpenAIStreamMixin): @@ -265,7 +266,7 @@ class Chatgpt(Chatgpt_4, StreamSimpleService, OpenAIStreamMixin): ) return self.save_results([response], process_time, generated_image, save) - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: ctx: dict[str, Any] = {} content_parts: list[str] = [] input_tokens = output_tokens = 0 @@ -284,7 +285,7 @@ class Chatgpt(Chatgpt_4, StreamSimpleService, OpenAIStreamMixin): chunk = next(stream) if chunk: content_parts.append(chunk) - yield chunk + yield RawSSEChunk(event='token', data={'content': chunk}) except StopIteration as exc: input_tokens, output_tokens = exc.value or (0, 0) break @@ -11,6 +11,7 @@ from messages.models import Message from ml_model.exceptions import ModelVersionNotAvailable, PaidPlanRequiredError from ml_model.services.chatgpt import Chatgpt from poller.models import Proxy +from tools.chats.domain import RawSSEChunk class Chatgpt_5_4(Chatgpt): @@ -94,7 +95,7 @@ class Chatgpt_5_4(Chatgpt): price += self.TOKENS_COST[model]['generated_image'] return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: return (yield from super().make_stream(input_message, save)) def _build_payload( @@ -11,6 +11,7 @@ from PIL import Image from django.utils.translation import gettext from messages.models import Message +from messages.services.message_service import MessageService from ml_model.adapters.openrouter import OpenrouterAdapter from ml_model.exceptions import ( CorruptedFileError, @@ -25,6 +26,7 @@ from ml_model.services.serper_mixin import SerperMixin from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector from poller.models import Proxy +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore @@ -132,7 +134,7 @@ class Claude(SerperMixin, StreamSimpleService): ) return self.save_results(result.content, process_time, save) - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: start_time = time.time() version_slug = input_message.info.get('version') if version_slug is None or version_slug not in self.TOKENS_COST: @@ -140,9 +142,10 @@ class Claude(SerperMixin, StreamSimpleService): model_slug = f'anthropic/{version_slug}' callback_data = self._build_callback_data(input_message) messages, embedding_tokens = self._prepare_messages(input_message, version_slug, callback_data) - content_parts: list[str] = [] input_tokens = output_tokens = 0 cost = 0 + reasoning = '' + content = '' result = '' try: @@ -151,13 +154,16 @@ class Claude(SerperMixin, StreamSimpleService): while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] yield chunk except StopIteration as exc: input_tokens, output_tokens, cost = exc.value finally: - if content_parts: - result = ''.join(content_parts) + if reasoning or content: + result = MessageService.prepare_output_message(reasoning, content) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, @@ -12,6 +12,7 @@ import filetype from PIL import Image from messages.models import Message +from messages.services.message_service import MessageService from ml_model.adapters.bytedance_model_ark import BytedanceContentType, BytedanceModelArkAdapter from ml_model.services.FileService import FileProcessingService from ml_model.exceptions import GenerationException, ModelVersionNotAvailable @@ -19,6 +20,7 @@ from ml_model.services.base import SimpleService from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector from ml_model.tasks import bytedance_model_ark_run, stream_bytedance_model_ark_run +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore @@ -284,12 +286,13 @@ class Dola_Seed(SimpleService): msgs = self.save_results(result[0], process_time, save) return msgs - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: version, callback_data, messages = self._prepare_data(input_message) model = self.VERSION_MAPPING[version] start_time = time.time() - content_parts: list[str] = [] input_tokens = output_tokens = 0 + reasoning = '' + content = '' result = '' try: @@ -303,15 +306,18 @@ class Dola_Seed(SimpleService): while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] yield chunk except StopIteration as exc: usage = exc.value or {} input_tokens = int(usage.get('prompt_tokens') or 0) output_tokens = int(usage.get('completion_tokens') or 0) finally: - if content_parts: - result = ''.join(content_parts) + if reasoning or content: + result = MessageService.prepare_output_message(reasoning, content) if not (input_tokens + output_tokens): input_text_parts = [] for message in messages: @@ -10,6 +10,7 @@ import filetype from PIL import Image from messages.models import Message +from messages.services.message_service import MessageService from ml_model.adapters.openrouter import OpenrouterAdapter from ml_model.exceptions import CorruptedFileError, FileExtensionNotSupported, ModelVersionNotAvailable from ml_model.services.EmbeddingService import EmbeddingService @@ -17,6 +18,7 @@ from ml_model.services.FileService import FileProcessingService from ml_model.services.base import StreamSimpleService from ml_model.tasks import openrouter_run from poller.models import Proxy +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore @@ -94,7 +96,7 @@ class Gemini_3_1(StreamSimpleService): ) return self.save_results(result[0], process_time, save) - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: start_time = time.time() version_slug = input_message.info.get('version') if version_slug is None or version_slug not in self.TOKENS_COST: @@ -105,8 +107,9 @@ class Gemini_3_1(StreamSimpleService): **input_message.info, } messages, embedding_tokens = self._prepare_messages(input_message) - content_parts: list[str] = [] input_tokens = output_tokens = 0 + reasoning = '' + content = '' result = '' try: @@ -115,13 +118,16 @@ class Gemini_3_1(StreamSimpleService): while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] yield chunk except StopIteration as exc: input_tokens, output_tokens, _ = exc.value finally: - if content_parts: - result = ''.join(content_parts) + if reasoning or content: + result = MessageService.prepare_output_message(reasoning, content) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, @@ -5,12 +5,14 @@ from decimal import Decimal from typing import Any, Iterator from messages.models import Message +from messages.services.message_service import MessageService from ml_model.adapters.bytedance_model_ark import BytedanceContentType, BytedanceModelArkAdapter from ml_model.exceptions import GenerationException from ml_model.services.base import SimpleService from ml_model.tasks import bytedance_model_ark_run, stream_bytedance_model_ark_run from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore @@ -107,11 +109,12 @@ class Glm_4_7(SimpleService): msgs = self.save_results(result[0], process_time, save) return msgs - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: version, callback_data, messages = self._prepare_data(input_message) start_time = time.time() - content_parts: list[str] = [] input_tokens = output_tokens = 0 + reasoning = '' + content = '' result = '' try: @@ -125,15 +128,18 @@ class Glm_4_7(SimpleService): while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] yield chunk except StopIteration as exc: usage = exc.value or {} input_tokens = int(usage.get('prompt_tokens') or 0) output_tokens = int(usage.get('completion_tokens') or 0) finally: - if content_parts: - result = ''.join(content_parts) + if reasoning or content: + result = MessageService.prepare_output_message(reasoning, content) if not (input_tokens + output_tokens): input_tokens = BytedanceModelArkAdapter.tokenize( version, ''.join([m['content'] for m in messages]) @@ -11,6 +11,7 @@ from PIL import Image from django.utils.translation import gettext from messages.models import Message +from messages.services.message_service import MessageService from ml_model.adapters.openrouter import OpenrouterAdapter from ml_model.exceptions import CorruptedFileError, FileExtensionNotSupported, PaidPlanRequiredError from ml_model.services.base import StreamSimpleService @@ -20,6 +21,7 @@ from ml_model.services.serper_mixin import SerperMixin from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector from poller.models import Proxy +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore @@ -81,14 +83,15 @@ class Grok(SerperMixin, StreamSimpleService): ) return self.save_results(result.content, process_time, save) - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: start_time = time.time() version = input_message.info.get('version') or 'grok-4.5' callback_data = {**input_message.info, 'tools': []} messages, embedding_tokens = self._prepare_messages(input_message, version, callback_data) - content_parts: list[str] = [] input_tokens = output_tokens = 0 cost = 0 + reasoning = '' + content = '' result = '' try: @@ -97,13 +100,16 @@ class Grok(SerperMixin, StreamSimpleService): while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] yield chunk except StopIteration as exc: input_tokens, output_tokens, cost = exc.value finally: - if content_parts: - result = ''.join(content_parts) + if reasoning or content: + result = MessageService.prepare_output_message(reasoning, content) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, @@ -1,4 +1,3 @@ -import json import time from datetime import timedelta from decimal import Decimal @@ -6,10 +5,9 @@ from pathlib import Path from typing import Iterator import filetype -import httpx -from backend import settings from messages.models import Message +from messages.services.message_service import MessageService from ml_model.adapters.openrouter import OpenrouterAdapter from ml_model.exceptions import CorruptedFileError, FileExtensionNotSupported from ml_model.services.EmbeddingService import EmbeddingService @@ -18,6 +16,7 @@ from ml_model.exceptions import ModelVersionNotAvailable from ml_model.services.base import StreamSimpleService from ml_model.tasks import openrouter_run from poller.models import Proxy +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore @@ -92,32 +91,34 @@ class Qwen_3_7(StreamSimpleService): msgs = self.save_results(result[0], process_time) return msgs - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: start_time = time.time() version_slug, model_slug, callback_data, messages, embedding_tokens = self._prepare_data( input_message ) - content_parts: list[str] = [] + input_tokens = output_tokens = 0 cost = 0 + reasoning = '' + content = '' result = '' try: - stream = self._run_streaming_api(model_slug, messages, callback_data) + stream = OpenrouterAdapter.run_streaming_api(model_slug, messages, callback_data, 'Qwen') try: while True: chunk = next(stream) if chunk: - content_parts.append(chunk) + if chunk.event == 'think': + reasoning += chunk.data['content'] + else: + content += chunk.data['content'] yield chunk except StopIteration as exc: - cost = exc.value + input_tokens, output_tokens, cost = exc.value finally: - if content_parts: - result = ''.join(content_parts) + if reasoning or content: + result = MessageService.prepare_output_message(reasoning, content) if not cost: - input_tokens, output_tokens = OpenrouterAdapter.count_tokens_fallback( - 'Qwen', messages, result - ) cost = self._estimate_cost(version_slug, input_tokens, output_tokens) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( @@ -240,46 +241,3 @@ class Qwen_3_7(StreamSimpleService): + output_tokens * price_map['output'] / 1_000_000 ) return float(price / self.COEFFICIENT) - - def _run_streaming_api( - self, version: str, messages: list, callback_data: dict - ) -> Iterator[str]: - for proxy in Proxy.objects.all(): - with httpx.Client( - 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: - with client.stream( - 'POST', - 'chat/completions', - json={ - 'model': version, - 'stream': True, - 'messages': messages, - 'transforms': ['middle-out'], - **callback_data, - }, - ) as resp: - cost = 0 - for line in resp.iter_lines(): - line = line.strip() - if not line or not line.startswith('data: '): - continue - - data = line[6:] - if data == '[DONE]': - break - - try: - data_obj = json.loads(data) - chunk = data_obj['choices'][0]['delta'].get('content') or '' - if chunk: - yield chunk - if data_obj.get('usage'): - cost = data_obj['usage'].get('cost') or 0 - except json.JSONDecodeError: - continue - - return cost @@ -1,25 +1,32 @@ -import base64 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.adapters.bytedance_model_ark import BytedanceContentType +from ml_model.exceptions import InvalidParameterError from ml_model.services.base import SimpleService -from ml_model.tasks import bytedance_model_ark_run, replicate_run +from ml_model.tasks import bytedance_model_ark_run + +# import base64 +# import filetype +# from ml_model.tasks import replicate_run class Reve(SimpleService): # Reve временно не работает на репликейте. Временно используем сидрим TEMPORARY_PROVIDER_MODEL = 'seedream-5-0-260128' - PRICE = Decimal('25') + PRICE = { + '2K': Decimal('25'), + '3K': Decimal('50'), + '4K': Decimal('100'), + } # PRICE = { # 'create': Decimal('12.5'), # 'edit-fast': Decimal('5'), @@ -27,10 +34,12 @@ class Reve(SimpleService): @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - return cls.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') + price = cls.PRICE.get(info.get('size', '2K')) + + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def calculate_price(self) -> Decimal: - return self.PRICE.quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, size: str) -> Decimal: + return self.PRICE[size].quantize(Decimal('0.1'), rounding='ROUND_UP') # @classmethod # def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: @@ -62,10 +71,14 @@ class Reve(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() + size = input_message.info.get('size', '2K') + if size not in self.PRICE: + raise InvalidParameterError(f'Unsupported size: {size}') + callback_data = { 'prompt': self.translate_prompt(input_message.content), **input_message.info, - 'size': '2K', + 'size': size, 'watermark': False, } if image := input_message.file: @@ -76,7 +89,7 @@ class Reve(SimpleService): content_type=BytedanceContentType.IMAGE, )[0] process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model) + self.handle_invoice(input_message.content_object.model, size=size) msgs = self.save_results(input_message.content, image, process_time, save) return msgs @@ -9,10 +9,14 @@ from decimal import Decimal from django.core.cache import cache from messages.models import Message +from messages.services.message_service import MessageService +from ml_model.exceptions import GenerationException from ml_model.services.base import StreamSimpleService from typing import Iterator +from tools.chats.domain import RawSSEChunk + type TestTextTokens = list[str] @@ -20,6 +24,7 @@ type TestTextTokens = list[str] class PrepareTestData: input_tokens: TestTextTokens output_tokens: TestTextTokens + reasoning_tokens: TestTextTokens ttft: float tbt: float start_time: float @@ -39,6 +44,13 @@ class Text_Test_Model(StreamSimpleService): fermentum lorem sit amet tortor ultricies, id pulvinar nibh pulvinar. """ + BASE_REASONING_MESSAGE = """ + In a dapibus nulla. Aenean erat orci, egestas non orci at, varius tempus risus. Ut suscipit lorem magna, + quis auctor leo molestie ac. Integer ut efficitur neque. Curabitur sollicitudin ipsum dolor, et tempus massa + lacinia a. Donec efficitur egestas facilisis. Aliquam feugiat convallis arcu quis sollicitudin. + Nullam eleifend iaculis sapien id scelerisque. + """ + def calculate_price(self, input_tokens: int, output_tokens: int) -> Decimal: price = ( input_tokens * self.TOKENS_COST['input'] / 1_000_000 @@ -58,23 +70,49 @@ class Text_Test_Model(StreamSimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: prepare = self._prepare(input_message) - result = ''.join(self._stream(prepare)) + reasoning = '' + output = '' + for token in self._stream(prepare): + if token.event == 'think': + reasoning += token.data['content'] + else: + output += token.data['content'] + result = MessageService.prepare_output_message(reasoning, output) + if not result: + raise GenerationException return self._finalize( - input_message, prepare, result, save, output_token_count=len(prepare.output_tokens) + input_message, + prepare, + result, + save, + output_token_count=len(prepare.output_tokens) + len(prepare.reasoning_tokens), ) - def make_stream(self, input_message: Message, save: bool = True) -> Iterator[str]: + def make_stream(self, input_message: Message, save: bool = True) -> Iterator[RawSSEChunk]: prepare = self._prepare(input_message) - result = '' + output = '' + reasoning = '' output_token_count = 0 try: for token in self._stream(prepare): - result += token + if token.event == 'think': + reasoning += token.data['content'] + else: + output += token.data['content'] output_token_count += 1 yield token finally: + result = MessageService.prepare_output_message(reasoning, output) if result: - self._finalize(input_message, prepare, result, save, output_token_count=output_token_count) + self._finalize( + input_message, + prepare, + result, + save, + output_token_count=output_token_count, + ) + if not result: + raise GenerationException return result def _prepare(self, input_message: Message) -> PrepareTestData: @@ -82,16 +120,21 @@ class Text_Test_Model(StreamSimpleService): info = input_message.info.copy() input_tokens = self._get_cached_tokens(input_message.content) output_tokens = self._get_cached_tokens(info.get('cm') or self.BASE_OUTPUT_MESSAGE) + reasoning_tokens = [] + if info.get('reasoning'): + reasoning_tokens = self._get_cached_tokens(info.get('rm') or self.BASE_REASONING_MESSAGE) ttft = info.get('ttft', 0.5) tbt = info.get('tbt', 0.35) - return PrepareTestData(input_tokens, output_tokens, ttft, tbt, start_time) + return PrepareTestData(input_tokens, output_tokens, reasoning_tokens, ttft, tbt, start_time) - def _stream(self, prepare: PrepareTestData) -> Iterator[str]: + def _stream(self, prepare: PrepareTestData) -> Iterator[RawSSEChunk]: time.sleep(prepare.ttft) - for i, token in enumerate(prepare.output_tokens, start=1): - yield token - if i < len(prepare.output_tokens): - time.sleep(prepare.tbt) + streaming_data = {'think': prepare.reasoning_tokens, 'token': prepare.output_tokens} + for k, v in streaming_data.items(): + for i, token in enumerate(v, start=1): + yield RawSSEChunk(event=k, data={'content': token}) + if i < len(v): + time.sleep(prepare.tbt) def _finalize( self, @@ -20,6 +20,7 @@ from replicate.exceptions import ModelError from requests import Response from backend import settings +from messages.services.message_service import MessageService from ml_model.adapters.bytedance_model_ark import BytedanceContentType, BytedanceModelArkAdapter from ml_model.adapters.openrouter import OpenrouterAdapter from ml_model.exceptions import ( @@ -167,7 +168,12 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name for c in data.get('choices', []) if c.get('message') and c['message'].get('content') is not None ] - if not raw_content: + raw_reasoning = ','.join( + reasoning + for choice in data.get('choices', []) + if (reasoning := choice['message'].get('reasoning')) is not None + ) + if not raw_content and not raw_reasoning: if any( c.get('native_finish_reason') == 'SAFETY_CHECK_TYPE_CSAM' for c in (data.get('choices') or []) @@ -182,22 +188,8 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name ) raise GenerationException content = ','.join(raw_content) - reasoning = ','.join( - reasoning - for choice in data.get('choices', []) - if (reasoning := choice['message'].get('reasoning')) is not None - ) - 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|6|7)-.*$', data['model'] - ): - answer = content - elif reasoning and content: - # TODO: переделать рендеринг сообщения на Jinja 2 - answer = f'**Рассуждение:**\n\n{reasoning}\n\n**Основная мысль:**\n\n{content}' - elif content: - answer = content + reasoning = re.sub(r'Вывод:|Основная мысль:|Рассуждение:|\*\*', '', raw_reasoning) + answer = MessageService.prepare_output_message(reasoning, content) if int(data.get('choices')[0].get('error', {}).get('code', 0)) == 502: error_type = re.sub(r'["\']', '', str(data['choices'][0]['error']['message'])) if error_type == 'Overloaded': @@ -15,6 +15,10 @@ class SSEChunkService: def token(cls, event_id: int, content: str) -> SSEChunk: return cls._chunk(event_id, 'token', {'content': content}) + @classmethod + def think(cls, event_id: int, content: str) -> SSEChunk: + return cls._chunk(event_id, 'think', {'content': content}) + @classmethod def error(cls, event_id: int, content: str) -> SSEChunk: return cls._chunk(event_id, 'error', {'detail': content}) @@ -5,12 +5,16 @@ from dataclasses import asdict, dataclass from tools.chats.typing import SSEData, SSEEvent -@dataclass -class SSEChunk: - event_id: int +@dataclass(frozen=True, slots=True) +class RawSSEChunk: event: SSEEvent data: SSEData + +@dataclass(frozen=True, slots=True) +class SSEChunk(RawSSEChunk): + event_id: int + def encode(self): payload = orjson.dumps(self.data, default=str).decode() return f'id: {self.event_id}\nevent: {self.event}\ndata: {payload}\n\n' @@ -8,6 +8,7 @@ from django.db.models.functions import Greatest from messages.models import Message from ml_model.models import NeuronModel from ml_model.services.base import StreamSimpleService +from tools.chats.domain import RawSSEChunk from tools.chats.models import Chat from tools.chats.services.sse_chunk_service import SSEChunkService from tools.chats.services.sse_store import PublicSSEStoreService, SSEStoreService @@ -35,10 +36,17 @@ def _run_stream( stream = service.make_stream(message) while True: token = next(stream) - if not token: + if not isinstance(token, RawSSEChunk): + continue + token_data = token.data.get('content') + if not token_data or not isinstance(token_data, str): continue event_id += 1 - store.push(SSEChunkService.token(event_id, token)) + store.push( + SSEChunkService.think(event_id, token_data) + if token.event == 'think' + else SSEChunkService.token(event_id, token_data) + ) except StopIteration as exc: event_id += 1 store.push(SSEChunkService.done(event_id, exc.value or ''), ttl=settings.SSE_DONE_STREAM_TTL) @@ -1,4 +1,4 @@ from typing import Any, Literal -type SSEEvent = Literal['pending', 'start', 'token', 'error', 'done'] +type SSEEvent = Literal['pending', 'start', 'token', 'think', 'error', 'done'] type SSEData = dict[str, Any]