До списку уроків
10 клас · Урок 13 з 16

Чат-бот із серверним проксі: ключ ніколи не на фронтенді

виконано0%
Крок 1

Що ти будуєш сьогодні і де лежить ключ

Сам бот — це п'ятнадцять рядків коду. Урок про інше: який файл відвідувач твого сайту побачить, а який — ні

У дев'ятому класі бот був таблицею заготовок. Учень тиснув кнопку — бот видавав готове речення. Усе це лежало прямо в сторінці.

Тепер відповідь пише модель — чужа програма на чужому сервері. Щоб до неї звернутися, потрібен ключ. Це довгий рядок символів, який доводить, що за запит платиш саме ти. І одразу постає нове питання. Де цей ключ лежить, поки бот працює?

Відповідь на весь урок коротка. Ключ лежить в одному файлі на твоєму сервері — ~/.env. У браузер він не потрапляє ніколи й ні в якому вигляді. Решта уроку — наслідки цього правила.

Чого навчишся за цей урок

Хід уроку

ЕтапХвЩо робимо
Демо «F12»15Витягуємо ключ навчального бота за 20 секунд через панель мережі
Архітектура проксі15Дві схеми — де саме проходить межа довіри між браузером і сервером
Персона й контекст15Пишемо роль бота, межі й формулу відмови в редакторі з живим прев'ю
Практика: system.md15Складаємо системний промпт ≥200 символів, тестуємо в прев'ю
Практика: проксі30Пишемо chat.php, що читає ключ із ~/.env, перевіряємо код на захардкоджений ключ
Практика: фронтенд15Робимо сторінку чату, що звертається лише до свого /api/chat.php
Git-гігієна10.gitignore, git status — перевіряємо, що .env не потрапив в історію
Автоперевірка5🔒 no_key_front, key_in_env, gitignore, git_history
Разом120

Слова «проксі», «токен» і «системний промпт» поки нічого не означають. Розберемо їх по черзі, кожне на своєму місці.

Як зробить AI-редактор, якщо не сперечатися

браузер ходить прямо до моделі

  • ключ лежить рядком у chat.js;
  • файл віддається кожному відвідувачу цілком;
  • ключ їде в заголовку кожного запиту;
  • перевірити ввід нема кому: код виконує браузер.
Як зробиш ти

браузер ходить до твого chat.php

  • ключ читається з ~/.env уже на сервері;
  • браузер знає лише адресу /api/chat.php;
  • сервер перевіряє довжину, тип і частоту;
  • системний промпт відвідувач не може підмінити.

Що ти зробиш за 120 хвилин

  1. знайдеш ключ у навчальному «дірявому» боті за три кліки — і зрозумієш, чому це три кліки, а не злам;
  2. напишеш system.md: роль бота, межі, формула відмови;
  3. напишеш api/chat.php — проксі, який бере ключ із ~/.env і додає його вже на сервері;
  4. додаси до проксі перевірки: метод, тип вмісту, довжину, частоту, таймаут і журнал без персональних даних;
  5. зробиш сторінку чату bot/index.html + bot/chat.js, яка звертається лише до твого власного API;
  6. перевіриш у вкладці «Мережа», що заголовка Authorization у запитах браузера немає взагалі;
  7. налаштуєш .gitignore і переконаєшся, що .env не потрапив ані у робочу теку, ані в історію комітів.
Що пишеш локально, а що їде на сервер Код ти пишеш і перечитуєш у себе на комп'ютері. По FTP у кабінет їдуть чотири текстові файли: bot/index.html, bot/chat.js, api/chat.php, bot/system.md. Поруч, у теці репозиторію, лежить .gitignore. А файл ~/.env ти створюєш по SSH, прямо на сервері. У ~/www він не потрапляє ніколи. По FTP його теж не заливають: FTP-клієнт уміє «синхронізувати теку» і легко забере зайве.
Урок 14 добудує те, що сьогодні лишиться відкритим Сьогоднішній бот чесно передає до моделі будь-який текст. Наступного уроку ти навчиш його впізнавати підкинуті команди, поставиш жорсткіший ліміт на кількість запитів і напишеш план дій на випадок витоку ключа. Сьогоднішнє завдання вужче: ключ не в браузері, обмеження — на сервері.
Крок 2

Усе, що прийшло в браузер, — уже не таємниця

Це не про вразливість і не про недогляд. Це про те, як улаштований веб: браузер не може виконати код, якого він не отримав

Коли ти відкриваєш сторінку, сервер віддає її вміст файл за файлом: HTML, CSS, кожен .js. Браузер мусить мати повний текст скрипта, інакше він його не виконає. Отже, цей текст є і в тебе, і в кожного, хто відкрив ту саму адресу.

Подивитися на нього можна трьома способами. Жоден із них не потребує нічого, крім самого браузера.

Ctrl+U
вихідний код сторінки: усе, що між <html> і </html>, включно з інлайновими <script>
пряме посилання
/bot/chat.js відкривається як звичайна сторінка: це такий самий файл, як index.html
F12 → «Мережа»
кожен запит, який зробив браузер, з усіма заголовками, тілом і відповіддю

Тому «ключ у JavaScript» і «ключ опублікований на сайті» означають те саме. Просто сказано двома різними способами.

Два шляхи запиту: з ключем у браузері й через власний проксі Угорі браузер звертається просто до API моделі: ключ лежить у файлі chat.js, їде в заголовку запиту, і все це видно відвідувачу. Унизу браузер звертається до chat.php на сервері учня; ключ додається вже на сервері, за межею довіри, і відвідувач бачить лише адресу свого сайту й текст повідомлення. Шлях A — ключ у браузері Браузер відвідувача index.html chat.js + КЛЮЧ Authorization: Bearer sk-… запит іде прямо з машини відвідувача API моделі рахунок виставляють власнику ключа — тобі Відвідувач бачить: адресу API, свій ключ, системний промпт, усе тіло запиту Шлях B — через власний проксі Браузер відвідувача index.html chat.js без ключа {"message":"…"} Твій сервер api/chat.php читає ~/.env + ключ API моделі бачить запит із твого сервера, не з чужої машини межа довіри Відвідувач бачить: /api/chat.php і текст свого повідомлення. Більше нічого
Різниця не в кількості коду. Різниця в тому, де саме до запиту додають ключ. У шляху A це робить код на комп'ютері відвідувача — тобто ключ спершу туди приїжджає.
Питання

Твій сайт відкритий лише за посиланням, яке ти нікому не давав, і в robots.txt написано Disallow: /. У chat.js лежить ключ. Наскільки це безпечно?

Крок 3

Інспектор фронтенду: подивись на свою сторінку як відвідувач

Нижче — навчальний бот. Перемикач угорі вибирає, як він улаштований усередині, кнопка запускає звичайний огляд сторінки відвідувачем

Спочатку постав «ключ у фронтенді» і натисни кнопку. Порахуй, у скількох місцях ключ видно. Потім спробуй кожен спосіб «сховати» його і дивись, як міняється число. І аж наприкінці перемкни на «через проксі». Ті самі три місця — і порожньо.

Інспектор фронтенду

Як улаштований бот
Спроба сховати ключ
три дії, жодного інструмента складнішого за F12
місць, де видно ключ
0,0 с
від кліку до знахідки
    Натисни кнопку

    Інспектор пройде трьома місцями, у які дивиться будь-який відвідувач: вихідний код сторінки, файл скрипта і вкладка «Мережа».

    Секунди на таймері — це темп анімації, а не заміряний час живої людини. Важливе тут не число. Важливо, що дій рівно три і жодна з них не є зламом: усі три файли сервер віддає сам, кожному, хто попросив.

    Вкладка «Мережа» у панелі розробника очима цікавого відвідувача Ліворуч список запитів сторінки: index.html, chat.js, і запит до API моделі. Праворуч розгорнуті заголовки виділеного запиту, серед яких Authorization з ключем. Унизу підпис: панель показує кожен запит, який зробив браузер, і жодних прав для цього не треба. Елементи Мережа Консоль Застосунок Ім'я Статус index.html 200 chat.js 200 v1/chat 200 style.css 200 4 запити · панель відкривається клавішею F12, права не потрібні Заголовки запиту POST /v1/chat HTTP/2 host: api.провайдер.tld content-type: application/json authorization: Bearer sk-demo-НЕ-СПРАВЖНІЙ-9f3a2c Тіло запиту {"system":"Ти помічник школи…", "messages":[{"role":"user",…}]} Панель показує не «код сайту», а те, що браузер уже відправив і отримав. Її не можна вимкнути з боку сайту: вона працює до того, як твій код почав виконуватись.
    У шляху A ключ видно навіть тому, хто не відкривав жодного .js. Досить розгорнути один запит у вкладці «Мережа». Разом із ключем там же видно й системний промпт: він їде в тілі того самого запиту.
    Питання

    Однокласник каже: «я вимкну праву кнопку миші й перехоплю клавішу F12 у скрипті, тоді панель розробника не відкриють». Що з цим не так?

    Крок 4

    Чому «сховати» ключ не працює в принципі

    Кожна спроба сховати ключ у фронтенді додає рівно один крок до пошуку. Нуль захисту, плюс хибне відчуття, що захист є

    Причина для всіх способів одна. Щоб скрипт міг скористатися ключем, він мусить у якусь мить тримати його у відкритому вигляді. А те, що бачить скрипт, бачить і людина.

    Досить зупинити скрипт у налагоджувачі саме в цю мить. Або взагалі не читати код, а подивитися на запит, який уже пішов: ключ там у заголовку, звичайним текстом.

    СпробаЩо насправдіСкільки додаткових дій
    мінімізація chat.js прибираються пробіли й переноси; рядок із ключем лишається рядком 0 (Ctrl+F по sk-)
    обфускація, «незрозумілі» імена змінних імена змінних не ховають значення; ключ видно у вкладці «Мережа» без читання коду взагалі 0
    ключ у base64 це кодування, а не шифрування: atob() поруч у тому самому файлі 1
    ключ по частинах, склеюється в коді склеєний рядок видно в налагоджувачі й у самому запиті 1
    окремий config.js + .gitignore .gitignore ховає файл від git, а не від nginx: файл усе одно віддається по HTTP 1
    ключ забирається запитом із сервера при старті сторінки цей запит теж видно у «Мережі»: ти зробив ендпойнт, який роздає ключ усім 0
    Правило, з якого немає винятків Секрет, який мусить потрапити в браузер, щоб працювати, — уже не секрет. Єдиний спосіб не показати ключ — не надсилати його в браузер узагалі. Тому ключ доклеює хтось посередині, кому ти довіряєш. Цей «хтось» — твій власний сервер, і такий посередник зветься проксі.

    Що станеться далі, якщо ключ утік

    Ключ ніхто не «зламує». Його просто знаходять. По інтернету цілодобово ходять боти й шукають рядки характерного вигляду — ті, що починаються на sk- чи AIza.

    Вони перебирають публічні репозиторії, файли .js і випадково викладені .env. Знайдений ключ потрапляє в чужі руки за години, іноді за хвилини. Далі відбувається ось що.

    Тому ключ не «виправляють» — його відкликають Побачив свій ключ там, де його бути не мало? У chat.js, у чаті класу, у коміті, на скріншоті — байдуже де. Далі вважай його чужим. Порядок дій такий: відкликати → перевипустити → замінити рядок у ~/.env. І тільки потім розбиратися, як він туди потрапив. Повний план реагування ти напишеш на уроці 14, у файлі incident.md.
    Питання

    Ти виніс ключ із chat.js в окремий файл config.js. Потім додав рядок config.js у .gitignore і переконався: git status цього файла не показує. Ключ у безпеці?

    Крок 5

    Проксі: маленький посередник, який знає ключ

    Слово страшне, суть проста: замість «браузер → модель» стає «браузер → твій chat.php → модель»

    Уяви бібліотеку з закритим сховищем. Ти кажеш бібліотекарці, яку книжку хочеш. Вона йде у сховище своїм ключем і виносить книжку тобі. Ключа тобі ніхто не дає. Ти навіть не знаєш, на якій полиці лежала книжка.

    Такий посередник у вебі зветься проксі: він приймає запит від одного і переадресовує другому. Твій chat.php — саме така бібліотекарка. І в нього є властивість, якої в браузера немає: він працює на машині, до якої маєш доступ тільки ти. Тому ключ можна тримати там.

    Браузер після цього не знає ні адреси API моделі, ні назви моделі, ні ключа. Він знає рівно одне. Є адреса /api/chat.php, туди надсилають {"message": "…"} і отримують відповідь.

    Що робить проксі з запитом, крок за кроком Сім кроків згори вниз: прийняти запит, перевірити метод і тип вмісту, розібрати JSON, перевірити довжину, перевірити частоту запитів з цієї адреси, узяти системний промпт із файла, додати ключ із .env і сходити до моделі з таймаутом, записати рядок у журнал без персональних даних і повернути відповідь браузеру. Ключ додається тільки на сьомому кроці, вже на сервері. 1 · Прийняти POST на /api/chat.php браузер знає лише цю адресу 2 · Перевірити метод і Content-Type не POST і не JSON → 405 / 415 3 · Розібрати тіло, узяти лише поле message битий JSON → 400 4 · Перевірити довжину повідомлення більше 400 символів → 413 5 · Перевірити, скільки запитів було з цієї адреси більше 10 за хвилину → 429 6 · Прочитати системний промпт із файла його склав ти, не відвідувач 7 · Додати ключ із ~/.env і сходити до моделі з таймаутом єдине місце у всій системі, де ключ узагалі з'являється 8 · Записати рядок у журнал: час, довжина, код відповіді без тексту повідомлення 9 · Повернути браузеру тільки текст відповіді без заголовків і службових полів
    Кроки 2—6 і 8 — це та частина, заради якої проксі варто писати самому. Якби він просто переадресовував запит, ключ був би захищений, а гаманець — ні.
    Проксі — це не просто «ключ у PHP-файлі» Уяви проксі, який бере від браузера готове тіло запиту й пересилає його до моделі, доклеївши ключ. Ключ справді не в браузері. Але відвідувач тепер керує усім: моделлю, довжиною відповіді, системним промптом. Виходить безкоштовний доступ до твого ключа через зручну адресу. Тому проксі приймає одне полеmessage. Решту запиту до моделі він складає сам.
    Питання

    Учень зробив chat.php, який приймає від браузера повний JSON запиту до моделі. Проксі лише додає до нього заголовок із ключем. Ключ у ~/.env, у браузері його немає. Що не так?

    Крок 6

    Воронка обмежень: що проксі відсіює до того, як витратити гроші

    Кожна перевірка коштує мікросекунди твого сервера і рятує від запиту, який коштує грошей

    Порядок перевірок не випадковий. Спершу йдуть найдешевші: подивитися на метод і тип вмісту можна за мікросекунду. Потім ті, що потребують розбору тіла запиту — JSON і довжина повідомлення.

    Далі перевірка частоти: вона вже лізе на диск за лічильником, тому коштує дорожче. І аж останнім іде єдиний по-справжньому дорогий крок — похід до моделі. Той, за який платять грошима.

    Воронка обмежень проксі Згори в проксі приходить потік запитів. Кожен рівень воронки відсікає частину: не-POST, не-JSON, битий JSON, надто довгі повідомлення, перевищений ліміт частоти. До моделі доходить лише те, що пройшло всі рівні, і кожен відсіяний запит коштує нуль. усі запити, що прийшли на /api/chat.php що завгодно з інтернету вартість: 0 метод POST − GET із адресного рядка, робот пошуковика Content-Type: application/json − форма, сирий текст валідний JSON із полем message − сміття, порожнє тіло довжина ≤ 400 символів − «книга» в одному полі ≤ 10 запитів за хвилину з IP − скрипт у циклі похід до моделі єдиний платний крок Кожен рівень воронки — це кілька рядків PHP і нуль звернень до мережі
    Воронка захищає дві різні речі. Верхні рівні — сервер (щоб не витрачати процес на сміття), нижні — гаманець (щоб не платити за запит, який ти не хотів).

    Шість перевірок і що вони ловлять

    метод
    приймати лише POST. Інакше твій ендпойнт відкриють переходом за посиланням — і кожен такий перехід буде запитом до моделі
    тип вмісту
    лише application/json. Це відсікає надіслані форми й випадкові звернення, які інакше провалилися б у json_decode і пішли далі на null
    довжина
    400 символів. Довге повідомлення — це і рахунок, і ризик упертися в межу контексту вже на першій репліці
    частота
    10 запитів за хвилину з однієї адреси. Головний захист гаманця: без нього один скрипт вичерпує квоту за ніч
    таймаут
    20 секунд на відповідь моделі. Без нього завислий запит тримає процес PHP, і десяток таких кладе сайт цілком
    журнал
    час, довжина, код відповіді. Без тексту повідомлень і без IP у відкритому вигляді — правила уроку 5 нікуди не поділися

    Обмеження частоти має простий побутовий двійник. У шкільній їдальні хліб лежить у спільному кошику. Взяти можна скільки треба, але не всю буханку за раз — інакше наступним не лишиться. Правило «10 запитів за хвилину з однієї адреси» робить те саме. Англійською таке обмеження звуть rate limit.

    Ліміт на фронтенді — це підказка, а не ліміт maxlength="400" у полі вводу й лічильник символів у chat.js корисні: вони пояснюють користувачу правила. Але діють вони лише на того, хто справді відкрив твою сторінку. Хто надішле запит командою curl, твій JS не виконує взагалі. Це те саме правило, що на уроці 4 про перевірку форми.
    Питання

    У chat.js стоїть maxlength="400", лічильник символів і затримка 3 секунди між надсиланнями. Наскільки це захищає твою квоту?

    Крок 7

    Конструктор проксі: збери свій chat.php

    Вмикай перевірки по одній і дивись, як росте код — і що написано під кожною вимкненою

    Почни з усіх вимкнених. Це той самий «зручний» варіант, який видає AI-редактор, коли не поставити йому умов. Він працює. І він же за одну ніч здатен з'їсти квоту всього класу.

    Конструктор проксі

    Перевірки на сервері
    код унизу перезбирається одразу
    0 із 7 перевірок беззахисний проксі
      ~/www/api/chat.php — збирається конструктором вище
      
        

      Лістинг у власному блоці, а не в терміналі: у PHP символ $ на початку рядка значущий, і кнопка копіювання термінала зрізала б його як промпт.

      Три речі, які в цьому коді варто прочитати уважно Перше: parse_ini_file читає ~/.env і бере звідти рядок MODEL_API_KEY=…. Сам файл лежить поза ~/www. Друге: заголовок із ключем у різних провайдерів зветься по-різному — Authorization: Bearer … або x-api-key: …. Подивись у документації тієї моделі, ключ до якої видали тобі. Третє: браузеру повертається тільки текст відповіді. Якщо віддати сиру відповідь API цілком, туди поїдуть службові поля, які браузера не стосуються.
      Що обов'язково перевірити в згенерованому коді AI-редактор часто робить «зручно»: кладе ключ рядком просто в PHP, щоб не морочитися з .env. Буває гірше — дублює його в chat.js «для тестів». Тому перед заливанням прожени по своїй теці пошук: grep -rn "sk-\|Bearer \|AIza\|hf_" ~/www. Вивід має бути порожній. Тільки тоді заливай.
      Питання

      Проксі написаний правильно, ключ у ~/.env, усі перевірки на місці. Але ~/.env має права 644. Чим це погано, якщо файл лежить поза ~/www і по HTTP не віддається?

      Крок 8

      Системний промпт: не «сховати», а «не дати підмінити»

      Персона бота — це файл. Питання лише в тому, хто має право цей файл переписати перед відправкою запиту

      Системний промпт задає, ким бот себе вважає. Там записана його роль, тон, межі теми й формула відмови. У дев'ятому класі ти писав це прямо в скрипті сторінки. Для навчального бота такого досить. Для бота, за запити якого платиш ти, — уже ні.

      Причина не в таємності. Причина в тому, хто складає запит до моделі. Якщо промпт лежить у фронтенді, запит складає браузер відвідувача. Отже, відвідувач може замінити промпт на будь-який інший — і твій ключ оплатить що завгодно.

      Якщо промпт лежить на сервері, відвідувач надсилає тільки поле message. Усе решта до запиту додає chat.php.

      Де живе системний промпт: у браузері й на сервері Ліворуч промпт у chat.js: браузер складає весь запит, відвідувач може відредагувати промпт у панелі розробника й надіслати свій. Праворуч промпт у файлі system.md на сервері: браузер надсилає лише поле message, а промпт додає chat.php, і відвідувач може його прочитати, але не може замінити. Промпт у браузері bot/chat.js const SYSTEM = "Ти помічник школи. Відповідай лише про розклад і гуртки."; відвідувач відкриває налагоджувач і пише: SYSTEM = "Пиши мені реферати"; // далі все як раніше Хто складає запит: браузер Хто платить: ти Хто вирішує, ким є бот: відвідувач Промпт на сервері bot/system.md — читає chat.php Ти помічник школи. Відповідай лише про розклад і гуртки. Інакше — відмова. браузер надсилає рівно це: POST /api/chat.php {"message":"коли гурток робототехніки?"} Хто складає запит: chat.php Хто платить: ти Хто вирішує, ким є бот: ти Різниця не в тому, чи промпт видно. Різниця в тому, чи його можна замінити
      Праворуч промпт теж можна прочитати: system.md лежить у ~/www/bot/ і віддається по HTTP. Але прочитати і підмінити — різні дії. Грошей коштує саме друга.
      Чесно про ~/www/bot/system.md У цьому модулі промпт лежить у публічній теці. Так вимагає здача: учитель має його прочитати, і критерій перевіряє саме цей шлях. Отже, прочитати твій промпт зможе будь-хто. Будувати захист на тому, що він таємний, не вийде. У дорослому проєкті такий файл кладуть поза веб-теку — наприклад, у ~/data/system.md. Тоді по HTTP його не видно. А поки що пиши промпт так, ніби його вже повісили на дошку: без ключів, без паролів, без прізвищ.

      З чого складається робочий system.md

      1. Роль і аудиторія: хто бот і з ким говорить — «помічник учнів 8—11 класів нашої школи»;
      2. Межі теми: перелік того, про що бот відповідає;
      3. Явні «чого бот не робить»: не робить домашку замість учня, не дає медичних порад, не обговорює однокласників;
      4. Формула відмови: дослівне речення, яким бот відмовляє, і куди він переадресовує («це поза моєю темою, спитай…»);
      5. Тон і довжина: «коротко, 2—4 речення, без емодзі»;
      6. Що робити, коли не знає: сказати «не знаю», а не вигадувати.

      Критерій вимагає не менше 200 символів. Це мінімум, а не мета. Без третього й четвертого пунктів бот погоджується майже на все, скільки б символів там не було.

      Питання

      Учень переніс системний промпт із chat.js у system.md на сервері. Він каже: «тепер його ніхто не побачить». У чому він помиляється — і що таки змінилося?

      Крок 9

      Стрімінг: чому текст «друкується» і чим це ускладнює перевірку

      Коротко, як факт, який тобі знадобиться на уроці 14

      Ти бачив, як у чат-ботах текст ніби набирається на очах, по кілька слів. Це не анімація. Модель справді пише відповідь по частинах і віддає кожну частину одразу, не чекаючи кінця.

      Такий режим зветься стрімінг (streaming). Технічно це один HTTP-запит, відповідь на який не закривається до кінця генерації. Звичайний запит поводиться інакше: чекає всю відповідь і повертає її одним шматком.

      Звичайна відповідьСтрімінг
      Що бачить користувачпавза, потім увесь тексттекст з'являється по словах
      Коли проксі бачить відповідьцілком, до того як віддатишматками, уже віддаючи попередні
      Перевірка відповіді на серверіможлива: є весь текстскладна: половину вже відправлено
      Складність коду проксідесяток рядківпомітно більше: буфер, події, обрив

      Ось у чому проблема. Фільтр на виході не дає боту сказати зайвого. Але працює він над готовим текстом, а в стрімінгу готового тексту немає.

      Проксі помічає заборонений фрагмент — а попередні речення вже в браузері. Забрати їх звідти неможливо. Виходить вибір із двох. Або тримаєш увесь текст у себе до кінця і втрачаєш сенс стрімінгу. Або перевіряєш по шматках і миришся з тим, що фільтр спрацює із запізненням.

      Що з цим робити на цьому уроці Нічого. Твій chat.php сьогодні працює без стрімінгу. Він чекає повну відповідь і віддає її одним шматком. Так простіше й надійніше, а на уроці 14 фільтр виводу додасться одним рядком. «Друкування» при цьому можна вдати у chat.js: виводити готовий текст по кілька символів. Виглядає так само, а перевірка лишається на сервері.
      Питання

      Учень увімкнув стрімінг, «щоб було як у справжніх ботів». І додав у проксі перевірку: якщо у відповіді трапиться заборонене слово, обірвати. Чому ця перевірка працює гірше, ніж він думає?

      Крок 10

      Скільки коштує одна репліка бота

      Гроші рахують не за запитами, а за токенами: окремо за те, що ти надіслав, і окремо за те, що модель написала

      Модель не бачить тексту так, як ти. Вона ріже його на шматочки завбільшки приблизно з частину слова. Такий шматочок і зветься токен.

      Для латиниці грубе наближення таке: близько 4 символів на токен. Для кирилиці помітно гірше — приблизно 2 символи. Тому те саме речення українською коштує дорожче за англійське. Це саме наближення, а не правило. Точну кількість дає окремий запит до API, який рахує токени: у Claude це /v1/messages/count_tokens.

      Ціни оголошують за мільйон токенів, окремо за вхідні й вихідні. Вихідні всюди дорожчі. Дивись тарифи своєї моделі. Числа в калькуляторі нижче — реальні ціни трьох моделей Claude станом на вересень 2026. Вони тут лише як порядок величини.

      Чому десята репліка діалогу дорожча за першу Стовпчики зростають зліва направо. У кожній наступній репліці до запиту додається вся попередня історія, тому кількість вхідних токенів росте, хоча користувач щоразу пише одне коротке речення. Внизу підпис: історію оплачують заново в кожному запиті. Вхідні токени в кожній репліці одного діалогу репліка 1: системний промпт + питання репліка 2: + відповідь 1 + питання 2 репліка 3 репліка 5 репліка 8 репліка 10 — те саме коротке питання користувач щоразу пише одне речення; платиш ти за весь діалог заново Тому в проксі є межа на кількість попередніх реплік, які ти вкладаєш у запит. Пропорції на схемі ілюстративні: точні числа порахуй калькулятором нижче.
      Модель не пам'ятає діалог сама. Її пам'ять — це те, що ти щоразу вкладаєш у запит. Тому довга розмова дорожчає швидше, ніж просто «удвічі більше реплік — удвічі дорожче».

      Калькулятор вартості

      Один запит


      За день
      За 30 днів
      За 180 навчальних днів

      Скрипт у циклі

      5 запитів за секунду протягом 10 хвилин — це 3000 запитів, які зробить п'ять рядків коду з чужого ноутбука

      Порівняй два числа: «за 30 днів» і «скрипт у циклі». Друге зазвичай більше. І набирається воно за десять хвилин. Тому обмеження частоти в проксі — не педантизм. Це єдине, що стоїть між твоїм ключем і чиєюсь цікавістю.

      Нескінченний цикл буває і без зловмисника Найчастіше квоту випалює власна помилка. Бот, який відповідає сам собі. Автоматичний перезапит при помилці, без павзи. Забутий після налагодження setInterval. Сервер при цьому почувається чудово: навантаження мізерне. Дізнаєшся ти з рахунку — або з того, що модель раптом почала відповідати «квоту вичерпано».
      Питання

      Твій бот працює нормально, сервер не навантажений, сторінка відкривається миттєво. Але за ніч квота вигоріла. Куди дивитися першим ділом?

      Питання

      Учень хоче здешевити бота і прибирає з запиту всю історію діалогу. Тепер у кожному запиті лише системний промпт і останнє питання. Що він на цьому виграв і що втратив?

      Крок 11

      П'ять файлів і одне місце, де лежить ключ

      Чотири текстові файли їдуть по FTP. П'ятий створюється по SSH і в публічну частину не потрапляє ніколи

      ФайлКудиЩо в ньому
      bot/index.htmlFTP → ~/www/bot/сторінка чату: поле вводу, кнопка, список реплік
      bot/chat.jsFTP → ~/www/bot/надсилає {"message":…} на свій API. Ключа немає
      api/chat.phpFTP → ~/www/api/проксі: перевірки, ключ із .env, похід до моделі
      bot/system.mdFTP → ~/www/bot/персона бота, ≥200 символів
      .gitignoreу теку репозиторіюрядок .env і все, що не має потрапити в git
      ~/.envтільки SSHMODEL_API_KEY=…, права 600

      Фронтенд: усе, що йому дозволено знати

      ~/www/bot/chat.js — жодного ключа, жодної адреси API моделі
      const box  = document.getElementById('log');
      const inp  = document.getElementById('msg');
      const send = document.getElementById('send');
      
      function add(who, text){
        const p = document.createElement('p');
        p.className = 'm ' + who;
        p.textContent = text;          // textContent, не innerHTML: правило уроку 4
        box.appendChild(p);
        box.scrollTop = box.scrollHeight;
      }
      
      send.onclick = async () => {
        const text = inp.value.trim();
        if(!text) return;
        if(text.length > 400){ add('sys', 'Задовге повідомлення'); return; }
      
        add('me', text);
        inp.value = '';
        send.disabled = true;
        add('sys', 'бот друкує…');
      
        try{
          const r = await fetch('/api/chat.php', {
            method : 'POST',
            headers: { 'Content-Type': 'application/json' },
            body   : JSON.stringify({ message: text })
          });
          const data = await r.json();
          box.lastChild.remove();                       // прибрати «друкує…»
          if(r.status === 429){ add('sys', 'Забагато запитів, зачекай хвилину'); }
          else if(r.status === 413){ add('sys', 'Повідомлення задовге'); }
          else if(!r.ok){ add('sys', 'Помилка: ' + (data.error || r.status)); }
          else { add('bot', data.reply); }
        }catch(e){
          box.lastChild.remove();
          add('sys', 'Немає зв\'язку із сервером');
        }
        send.disabled = false;
      };

      Зверни увагу на дві речі. Перша: fetch іде на відносну адресу /api/chat.php. Це твій власний сервер і твій власний домен, жодних заголовків із ключем тут немає.

      Друга: перевірка довжини в цьому файлі теж є. Так і має бути — вона підказує користувачу, що повідомлення завелике, ще до надсилання. Справжня перевірка все одно стоїть на сервері.

      Ключ: створюється по SSH, читається сервером

      SSH — створити ~/.env і закрити його від усіх, крім себе
      $ printf 'MODEL_API_KEY=твій-ключ\n' > ~/.env $ chmod 600 ~/.env $ stat -c '%a %n' ~/.env 600 /home/твій-логін/.env $ grep -rn "sk-\|Bearer \|AIza\|hf_" ~/www (порожньо — саме так і має бути)
      Чому ~/.env не заливають по FTP FTP-клієнт уміє «синхронізувати теку» і робить це охоче. Один необережний клік — і .env опиняється в ~/www. Звідти його віддадуть по HTTP за прямим посиланням. Правило просте: файл із секретом народжується там, де живе, і нікуди не подорожує. А права 600 потрібні тому, що на цій машині ще 60 акаунтів. Права 644 означали б «читати може кожен, хто зайшов на сервер».

      Git: .gitignore і те, чого він не вміє

      .gitignore — у корені репозиторію, ще ДО першого коміту
      # секрети
      .env
      .env.*
      !.env.example
      config.js
      *.key
      *.pem
      
      # сміття
      node_modules/
      .DS_Store

      У .gitignore є одна властивість, про яку постійно забувають. Він діє на файли, за якими git ще не стежить. Якщо .env уже потрапив у коміт, рядок в ігнорі його звідти не прибере. Git продовжить стежити за файлом просто тому, що вже стежить.

      Історія комітів схожа на класний журнал. Оцінку можна виправити на новій сторінці, але попередня сторінка нікуди не зникає — її досить перегорнути назад. Ключ, який хоч раз потрапив у коміт, дістають однією командою git log -p.

      Перевірка: чи бачить git те, чого не має бачити
      $ git status --porcelain (рядка з .env тут бути не повинно) $ git check-ignore -v .env .gitignore:2:.env .env $ git log --all -p | grep -c "MODEL_API_KEY" 0 $ git rm --cached .env потрібно лише тоді, коли файл уже відстежується
      Найгірша послідовність дій, і вона дуже поширена Закомітив .env → помітив → видалив файл наступним комітом → запушив → заспокоївся. Ключ при цьому лишився в історії. Кожен, хто клонує репозиторій, дістане його командою git log -p. Чистити історію складно й ненадійно. Правильна дія одна: негайно відкликати ключ і перевипустити новий. Історію чистять уже потім, для порядку, — а не щоб урятувати ключ.
      Питання

      Репозиторій приватний, доступ мають лише двоє однокласників. В історії комітів лежить .env із робочим ключем. Чи можна вважати ключ безпечним, поки репозиторій приватний?

      Крок 12

      Практика: персона, проксі, фронтенд, git

      Кроки 1—6 і 12—16 — у себе на комп'ютері та по SSH. По FTP їдуть чотири файли з кроків 8—11. Галочки зберігаються

      Одне правило, яке не має винятків Ключ не потрапляє ні в ~/www, ні в ~/upload, ні в git. Не з'являється в чаті класу, на скріншоті, у повідомленні вчителю чи в коментарі коду «щоб не забути». Якщо він десь там уже опинився, вважай його чужим. Перша дія — відкликати ключ, а не виправляти файл.
      Крок 13

      Автоперевірка

      Чекер надсилає боту тестове повідомлення, сканує ~/www/bot/ на рядки, схожі на ключі, і дивиться git. Спроби не обмежені

      Демонстраційний режим: результат згенеровано для показу. На сервері ця кнопка викликає POST /api/check, який запускає /opt/lessons/13/check.sh від імені учня.

      Що вчитель дивиться на твоєму екрані

      Крок 14

      Шкала виконання

      Це те, що бачить учитель, коли виставляє оцінку.

      СкладникВагаРезультат
      Виконано
      0%
      Рекомендований бал за 12-бальною

      Сім речень, які варто запам'ятати

      Де найчастіше помиляються

      Домашнє завдання

      Допиши в ~/www/bot/system.md три приклади запитів, на які бот має відмовити. Не категорії, а дослівні фрази — такі, які справді напишуть однокласники. Для кожної вкажи, якими словами бот відмовляє і куди переадресовує.

      Другим абзацом опиши, що зробиш у перші п'ять хвилин, якщо побачиш свій ключ на чужому скріншоті. Потрібні чотири дії, у правильному порядку. Вони стануть основою файла incident.md на уроці 14.

      Джерела

      Звідки взяті числа й формулювання

      Ціни й ліміти перевіряй самостійно: вони змінюються частіше, ніж виходять підручники

      Повний список із поясненнями, що звідки взято і що є ілюстративним прикладом, — у файлі urok-13-джерела.md поруч із цією сторінкою.