Вирішення проблемПосібник
Потік зламався після HTTP 200: перехоплення помилок посеред потоку в Python
Чому потік може зламатися після статусу 200, як помітити event: error і повторна спроба в Python, яка зберігає частковий текст без дублювання відповіді.

На цій сторінці
Ваш чат-інтерфейс передав половину відповіді, завис, а в лозі записано 200 OK. Це не суперечність: 200 лише означає, що потік розпочався, а не що він завершився. Якщо щось зламається пізніше, сервер може надіслати помилку лише всередині потоку, тож перевірка коду стану її ніколи не побачить. Виправлення полягає в тому, щоб вважати потік успішним лише тоді, коли він завершується своїм сигналом завершення, і повторювати спробу з чистого буфера, якщо ні.
Решта цієї сторінки показує, як виглядає зламаний потік, два обробники Python (Anthropic Messages та OpenAI SDK) і повторну спробу, яка не друкує початок відповіді двічі. Обидва обробники використовують CheapTokens як кінцеву точку API: документація містить базові URL-адреси, а зразкова модель — DeepSeek-V4-Flash (id deepseek-, контекст 380K, до 32 768 вихідних токенів на відповідь, одна ціна в будь-яку годину — див. сторінку моделі).
Що насправді обіцяє 200#
Уявіть кур'єра, який підписує квитанцію про отримання посилки біля дверей: підпис доводить, що він прибув, а не що посилка ціла. HTTP працює так само. Сервер спочатку надсилає рядок стану та заголовки, потім тіло надходить частинами. Якщо бекенд моделі перевантажується на півдорозі, рядок стану вже надіслано.
Сторінка помилок API Anthropic (перевірено 1 листопада 2026 року) каже це прямо: коли ви отримуєте потокову відповідь через SSE, помилка може виникнути після того, як API повернув 200, і тоді стандартна обробка помилок не застосовується. Її документація щодо потоків наводить форму цієї події:
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}overloaded_error зазвичай відповідає HTTP 529 у не потоковому виклику (тимчасове перевантаження через високий трафік). Та сама сторінка перелічує 500 api_error (повторіть з експоненційною затримкою) та 504 timeout_error (використовуйте потоки для довгих запитів). У мережі зламаний потік виглядає так:

Документація також попереджає, що можуть додаватися нові типи подій, тому ваш код має коректно обробляти невідомі події. Ось чому наведені нижче обробники не намагаються розпізнати кожен байт помилки. Вони ставлять одне запитання: чи завершився потік своїм сигналом завершення?
- Anthropic Messages: остання подія —
message_stop, аstop_reasonуmessage_deltaвказує, чому він завершився (end_turn,max_tokens,stop_sequence). - OpenAI Chat Completions: останній фрагмент містить непорожній
finish_reason(stop,length, …).
Немає сигналу завершення або виняток на шляху — відповідь неповна, незалежно від того, що казав статус.
Зловіть це в Python#
Візьміть бота підтримки, який передає відповіді в браузер потоком. Цикл збирає текст у буфер і повідомляє три речі: текст на даний момент, чи завершився потік і помилку, якщо вона була. Виберіть вкладку для SDK, який ви використовуєте.
Один обробник, два SDK
Anthropic SDK
import os
import anthropic
client = anthropic.Anthropic(
base_url='https://api.cheaptokens.dev', # no /v1 for the Anthropic SDK
api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'
def ask_once(prompt, on_text=lambda t: None):
parts, finished = [], False
try:
with client.messages.stream(
model=MODEL,
max_tokens=1024,
messages=[{'role': 'user', 'content': prompt}],
) as stream:
for event in stream:
if event.type == 'content_block_delta' and event.delta.type == 'text_delta':
parts.append(event.delta.text)
on_text(event.delta.text)
elif event.type == 'message_stop':
finished = True
except anthropic.APIError as err:
return ''.join(parts), False, err
return ''.join(parts), finished, NoneOpenAI SDK
import os
import openai
client = openai.OpenAI(
base_url='https://api.cheaptokens.dev/v1',
api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'
def ask_once(prompt, on_text=lambda t: None):
parts, finish = [], None
try:
stream = client.chat.completions.create(
model=MODEL,
max_tokens=1024,
stream=True,
messages=[{'role': 'user', 'content': prompt}],
)
for chunk in stream:
if not chunk.choices:
continue
choice = chunk.choices[0]
if choice.delta.content:
parts.append(choice.delta.content)
on_text(choice.delta.content)
if choice.finish_reason:
finish = choice.finish_reason
except openai.APIError as err:
return ''.join(parts), False, err
return ''.join(parts), finish is not None, NoneДві деталі варті другого погляду. По-перше, обидва обробники перехоплюють базовий клас помилок SDK (anthropic., openai.APIError), тож неважливо, який підклас SDK викличе для конкретної невдачі. По-друге, прапорець finished відокремлений від винятку: потік також може просто закінчитися раніше, без жодного винятку, і цей випадок теж має вважатися невдачею.
README openai-python додає, чого очікувати на боці OpenAI (перевірено 1 листопада 2026 року): тайм-аут читання в потоці викликає APITimeoutError, інші збої HTTPX викликають APIConnectionError з оригінальним винятком у __cause__. Якщо потік зависає замість того, щоб зазнати невдачі, див. тайм-аути та повторні спроби в OpenAI Python SDK.
Повторіть, не друкуючи відповідь двічі#
Ось помилка, яку легко випустити. Перша спроба друкує "Політика повернення коштів така…", помирає, повторна спроба починається заново і знову друкує "Політика повернення коштів така…" — тепер користувач бачить початок двічі, або речення, склеєне з власним перезапуском. Звичайна причина — один спільний буфер: answer += text живе поза спробою, тож друга спроба дописує до першої.
Ліки нудні. Одна спроба володіє одним буфером, текст невдалої спроби відкидається (або зберігається лише для логу), а повторна спроба починається з нуля:
import time
def ask_with_retry(prompt, attempts=3, on_text=lambda t: None, on_restart=lambda: None):
for attempt in range(attempts):
text, finished, err = ask_once(prompt, on_text)
if finished:
return text
status = getattr(err, 'status_code', None)
if status is not None and 400 <= status < 500 and status not in (408, 409, 429):
raise err # a 400/401/404 will not fix itself
print(f'attempt {attempt + 1} failed after {len(text)} chars: {err!r}')
if attempt == attempts - 1:
break
on_restart() # clear the message box in your UI
time.sleep(2 ** attempt) # 1 s, 2 s
raise RuntimeError('stream failed after all attempts')on_restart — це те, що зупиняє дублювання. У веб-інтерфейсі це очищає бульбашку повідомлення перед тим, як наступна спроба почне передаватися потоком. У звичайному терміналі ви не можете скасувати друк, тож або друкуйте лише після того, як finished стане true, або надрукуйте видимий рядок --- retrying ---. Повторна спроба з нуля повторно надсилає весь запит, тож вхідні дані оплачуються знову для нової спроби; для більшості відповідей у чаті це справедлива ціна за чисту відповідь.
Розставте по порядку
Розташуйте кроки повторної спроби в тому порядку, в якому ваш код має їх виконувати
Перетягніть елементи або скористайтеся стрілками
- Припиніть читати зламаний потік і збережіть його частковий текст для логу
- Перевірте помилку: 4xx, крім 408, 409 і 429, означає зупинку, а не повтор
- Очистіть поле повідомлення і почніть новий буфер
- Зачекайте з експоненційною затримкою (1 с, потім 2 с)
- Надішліть повний запит знову і покажіть лише нову відповідь
SDK Anthropic уже повторює тимчасові збої — помилки з'єднання, обмеження швидкості, 5xx — з експоненційною затримкою, за замовчуванням 2 рази, з повагою до retry-after, і max_retries змінює це. Це охоплює запит. Потік, який помирає після 200, — інша історія: документація каже, що стандартна обробка помилок там не застосовується, тож цикл вище — ваш, щоб його зберегти. Для 429 окремо є інший посібник: 429 Too Many Requests: повторні спроби із затримкою.
Не кожна рання зупинка є помилкою#
Перш ніж повторювати, перевірте, чому відповідь закінчилася. Потік, що завершується з stop_ (Anthropic) або finish_ (OpenAI), завершився — він просто досяг встановленого вами ліміту. Повтор цього поверне той самий обрізаний текст і той самий рахунок. Натомість підвищте ліміт; посібник щодо обрізаної відповіді розповідає про це.
Є третя група: мережа. Неактивні з'єднання можуть розриватися на шляху, що проявляється як помилка з'єднання або потік, який зупиняється без жодної події. Обробляйте це так само, як помилку посеред потоку, і див. поширені помилки з'єднання, якщо це повторюється. Для довгих генерацій Anthropic рекомендує потокову передачу замість великого не потокового max_tokens, тож ви вже на правильному боці цього.
Перед випуском#
Зберігайте ідентифікатор запиту кожної невдалої спроби. Тіла помилок Anthropic містять поле request_id і заголовок відповіді request-id, а Python SDK надає message.; з цим ідентифікатором невдача стає відстежуваною, а не анекдотичною.
Чек-лист
Обробка помилок потоків, зроблена правильно
Якщо ви хочете побачити обробник у дії, спрямуйте його на власний ключ: створіть ключ API у документації, встановіть CHEAPTOKENS_ і викличте ask_. Для основ потокової передачі в Python почніть з потокових відповідей у Python; бік Anthropic висвітлено в Anthropic Python SDK з CheapTokens.




