Чат с нейросетью на Streamlit: стриминг, история и настройки
Собираем чат-приложение на Streamlit: история в session_state, потоковый вывод через st.write_stream, выбор модели в боковой панели, ключ в secrets.toml и расход по usage.
4 мин · · разработка
Streamlit позволяет собрать чат с нейросетью на чистом Python за один вечер: готовые элементы st.chat_message и st.chat_input рисуют интерфейс, st.session_state хранит историю, а st.write_stream выводит ответ по мере генерации. Ниже — полный app.py на OpenAI SDK с выбором модели, обработкой ошибок и подсчётом стоимости, плюс разбор мест, где такие прототипы обычно ломаются.
Установка и секреты
pip install streamlit openaiСоздайте файл .streamlit/secrets.toml в корне проекта:
ZUKKO_API_KEY = "fetch_ваш_ключ"Streamlit читает его через st.secrets. Добавьте файл в .gitignore (.streamlit/secrets.toml) и не коммитьте ключ. При деплое на Streamlit Community Cloud те же строки вставляются в раздел секретов в настройках приложения. Ключ создаётся на странице API-ключи; для публичного демо заведите отдельный ключ с дневным лимитом и списком разрешённых моделей.
Чат с нейросетью на Streamlit: app.py
import streamlit as st
from openai import OpenAI, APIStatusError
PRICES = {
"claude-sonnet-5": 0.3,
"gpt-5.6-sol": 0.3,
"glm-5.3-flash": 0.04,
}
MAX_HISTORY = 20
ERRORS = {
401: "Неверный API-ключ",
402: "На балансе недостаточно средств",
403: "Модель не разрешена для этого ключа",
429: "Слишком много запросов, попробуйте позже",Запуск: streamlit run app.py, приложение откроется на http://localhost:8501.
Как это работает
Скрипт перезапускается при каждом действии. Streamlit выполняет app.py сверху вниз после каждого ввода или нажатия кнопки. Поэтому история хранится в st.session_state — это словарь, который переживает перезапуски в пределах одной вкладки браузера. Цикл for msg in ... заново рисует все прошлые сообщения, а новый ответ добавляется в конец.
Клиент создаётся один раз. @st.cache_resource кэширует результат функции глобально — для всех сессий и пользователей приложения, а не для одного пользователя. Для клиента OpenAI это ровно то, что нужно: соединения переиспользуются. Хранить в cache_resource данные конкретного пользователя нельзя — для этого есть session_state.
Стриминг через st.write_stream. По документации Streamlit функция принимает генератор, итерируемый объект или поток OpenAI, выводит текст с эффектом печатной машинки и возвращает полный текст ответа. Мы передаём собственный генератор, а не сырой поток SDK, по двум причинам:
- последний чанк потока содержит только
usageи пустой списокchoices; проверкаif chunk.choices:защищает отIndexErrorи позволяет забратьusage; - из генератора удобно сохранять расход в словарь
usageи показывать стоимость под ответом.
Ручной вариант со st.empty() и placeholder.markdown(text) тоже работает, но st.write_stream короче и сразу возвращает итоговую строку для истории.
История ограничена. В запрос уходят последние MAX_HISTORY сообщений. Каждый новый запрос оплачивается вместе со всей отправленной историей, поэтому без ограничения длинный разговор будет дорожать с каждым ходом.
Ошибки. Если API вернул ошибку, показываем понятный текст и убираем последний вопрос из истории, чтобы при следующей попытке в запросе не оказалось двух сообщений пользователя подряд. Коды Zukko: 401 — неверный ключ, 402 — не хватает баланса, 403 — модель не разрешена для ключа, 429 — слишком частые запросы, 503 — провайдер недоступен.
Модели и параметры
В боковой панели можно переключать модель. Набор моделей меняется, актуальный список — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Какая модель лучше отвечает в вашем сценарии, проверьте на одинаковых вопросах — переключатель в боковой панели для этого и нужен.
Ползунок temperature показывается только для моделей GPT и GLM. Новые модели Claude (claude-sonnet-5, claude-opus-5-5) могут отклонить temperature, отличную от 1, и top_p ниже 0.99 с ошибкой 400, поэтому для них параметр не передаётся.
Сколько стоит такой чат
Подпись под ответом считает стоимость по usage из последнего чанка. Пример для claude-sonnet-5 ($0.3 за 1M входных и выходных токенов): ход с 3 000 входных токенов (системный промпт и история) и 500 выходных стоит (3 000 + 500) × 0.3 / 1 000 000 = $0.00105. Для glm-5.3-flash тот же ход — (3 000 + 500) × 0.04 / 1 000 000 = $0.00014. Общий расход по ключу виден в разделе «Использование» кабинета, а лимиты ключа описаны в документации по лимитам.
Картинки в чате
Изображения понимают модели Claude и GPT; GLM 5.3 и GLM 5.3 Flash работают только с текстом. Чтобы отправить картинку, загрузите её через st.file_uploader и передайте в сообщении пользователя как data URL:
import base64
file = st.file_uploader("Изображение", type=["png", "jpg", "jpeg"])
if file and prompt:
b64 = base64.b64encode(file.getvalue()).decode("utf-8")
content = [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:{file.type};base64,{b64}"}},
]Такой content подставляется вместо строки в последнее сообщение пользователя. Учтите, что картинка будет отправляться заново с каждым следующим запросом, пока остаётся в истории. Формат запроса описан в справке по chat completions, настройка Python-клиента — в руководстве по Python.
Частые вопросы
Почему история пропадает после обновления страницы?
st.session_state живёт, пока открыта вкладка. Чтобы сохранять разговоры между визитами, записывайте сообщения в базу или файл и загружайте их при старте.
Можно ли передать в st.write_stream поток OpenAI напрямую?
Да, документация Streamlit прямо поддерживает потоки OpenAI. Собственный генератор нужен, если вы хотите сами обработать пустые choices, забрать usage или изменить текст по пути.
Почему ответ пользователя A видит пользователь B?
Скорее всего, история хранится в глобальной переменной или в функции с @st.cache_resource/@st.cache_data. Данные пользователя должны лежать только в st.session_state.
Как спрятать ключ при публичном деплое?
Ключ хранится в секретах приложения и используется только на сервере Streamlit — в браузер он не попадает. Для защиты баланса задайте ключу дневной лимит и разрешите только нужные модели.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.