각 요청에 대해 Auto, TokenLab Verified 또는 Official를 선택할 수 있으며, 가격은 사전에 표시됩니다. 새로운 기능 확인하기

출력 중복 없이 Streaming LLM 응답 재시도하기

CryptoCrypto
·2026년 9월 28일·약 3분 읽기·업데이트 2026년 9월 28일·27 조회수
#스트리밍#응답 API#신뢰성#websocket
출력 중복 없이 Streaming LLM 응답 재시도하기

스트리밍 요청은 다음 세 가지 조건이 동시에 충족될 때만 안전하게 재시도할 수 있습니다. 클라이언트에 아무것도 도달하지 않았을 것, 관찰 가능한 측정(metering)이 이루어지지 않았을 것, 그리고 요청에 서버 측 상태가 포함되지 않았을 것입니다. 첫 번째 출력 이벤트가 발생한 후에는 재시도하는 대신 실패를 보고하는 것이 올바른 대응입니다.

TokenLab은 HTTP와 WebSocket 모두에서 Responses API 스트리밍으로 향하는 게이트웨이에 이 규칙을 적용합니다. WebSocket 경로는 HTTP와 일치하도록 2026년 9월 28일에 변경되었습니다.

스트림이 일반 요청과 다른 이유

스트리밍이 아닌 호출은 본문(body)이나 오류를 반환합니다. 아무것도 받은 것이 없으므로 오류를 재시도할 수 있습니다.

스트림은 요청이 완료되기 전에 출력을 제공합니다. 첫 번째 출력 이벤트는 돌아올 수 없는 지점입니다. 그 이후 연결이 끊기면 부분적인 텍스트를 갖게 됩니다. 요청을 재시도한다는 것은 동일한 답변을 다시 생성하고 그 비용을 두 번 지불한다는 의미입니다. 또한 에이전트가 이미 실행한 도구 호출을 중복으로 수행할 수도 있습니다.

TokenLab 스트리밍 가이드는 이를 명확히 명시하고 있습니다:

첫 번째 이벤트가 도착한 후, 중단된 스트림은 불완전하며 자동으로 다시 시작되지 않습니다.

따라서 클라이언트는 saw_output이라는 로컬 상태 비트가 하나 필요합니다. 이 비트는 출력이 코드에 도달하는 순간 true로 바뀝니다. 모든 재시도 결정은 이 비트를 먼저 읽어야 합니다.

response.completed 없이 끝나는 스트림은 실패입니다. 가지고 있는 텍스트가 완전하다고 가정하지 마십시오. response.failed, response.incomplete 및 error 이벤트를 처리하십시오.

재시도 결정, 항목별 분석

TokenLab은 다음 조건이 모두 충족될 때 사용 가능한 다른 경로에서 요청을 한 번 재시도합니다. 요청이 상태 비저장(stateless)일 것. 클라이언트에 아무것도 도달하지 않았을 것. 실패한 시도에 대해 결과나 사용량이 관찰되지 않았을 것. 그리고 실패가 재시도 가능한 출력 전 이벤트이거나 첫 번째 이벤트 이전의 업스트림 읽기 오류일 것. 요청당 최대 한 번의 재시도만 발생합니다. 대체 요청도 출력 전에 실패하면 해당 실패는 다시 재시도되지 않습니다.

출처: TokenLab 스트리밍 가이드 및 게이트웨이 동작, 2026년 9월 28일 관찰.

실패 지점 TokenLab에 의한 재시도 여부 이유
재시도 가능한 출력 전 이벤트 (response.failed 또는 과부하/내부 업스트림 오류와 같이 재시도 가능으로 표시된 error 이벤트) 예, 한 번 (요청이 상태 비저장인 경우) 클라이언트에 도달한 것이 없고 사용량이 관찰되지 않았으므로 두 번째 실행은 보이지 않습니다.
첫 번째 이벤트 이전의 업스트림 스트림 끊김 (읽기 오류) 예, 한 번 (요청이 상태 비저장인 경우) 동일한 상황입니다. 클라이언트는 출력이나 청구 내역을 가지고 있지 않습니다.
한 번의 재시도 후 출력 전 두 번째 실패 아니요 요청당 재시도 한도는 1회입니다.
클라이언트에 출력 도달 후 발생하는 모든 실패 아니요 클라이언트가 이미 부분 텍스트를 가지고 있습니다. 재시도 시 출력과 비용이 중복됩니다.
저장된 응답(store), 연속(previous_response_id) 또는 원본 바인딩 요청 아니요 두 번째 실행 시 저장된 응답이 중복 생성되거나 대화 상태가 분기될 수 있습니다.
첫 번째 이벤트 타임아웃 아니요 업스트림이 여전히 생성 중일 수 있습니다. 재시도 시 첫 번째 시도가 진행되는 동안 동일한 작업을 두 번 수행할 수 있습니다.
출력 전 버퍼 오버플로우 아니요 제한은 게이트웨이 로컬입니다. 동일한 크기의 접두사는 다음 경로에서도 동일하게 제한에 걸릴 가능성이 매우 높습니다.
클라이언트 연결 끊김 아니요 클라이언트가 수신을 중단했습니다.
결정론적 실패 (예: 잘못된 요청) 아니요 재시도해도 결과가 바뀌지 않습니다. 변경 없이 전달됩니다.
이미 사용량이 청구된 실패 아니요 시도가 측정되었습니다. 변경 없이 전달됩니다.
남은 다른 경로가 없음 아니요 전송할 곳이 없습니다. 클라이언트는 자체 코드로 실패를 수신합니다.

실패가 재시도되지 않거나 남은 경로가 없는 경우, 해당 오류 코드가 포함된 실패를 수신하게 됩니다. 공개된 예시: 업스트림 스트림이 끊겼을 때의 stream_read_error, 버퍼 오버플로우 시의 upstream_stream_buffer_limit. 재시도 결정 후 경로 선택 자체가 실패하면 WebSocket 턴은 websocket_response_failed(상태 500)로 종료되고 예약된 요금은 환불됩니다.

과금도 동일한 원칙을 따릅니다. 전달된 시도에 대해서만 비용을 지불합니다. 재시도된 요청은 업스트림에서 두 번 실행되었을 수 있지만, 첫 번째 시도에서 클라이언트에 도달한 것이 없으므로 추가적인 업스트림 비용은 TokenLab의 부담입니다. 아무것도 전달하지 못한 실패한 턴은 환불됩니다.

오류 처리에 있어 한 가지 타이밍 세부 사항이 중요합니다. 출력이 시작되기 전, 게이트웨이는 첫 번째 출력 이벤트나 실패가 도착할 때까지 최대 10초 동안 response.created 및 response.in_progress를 보관합니다. 보관된 이벤트는 첫 번째 출력과 함께, 또는 종료 이벤트와 함께 귀하에게 도달합니다. 순서와 내용은 변경되지 않습니다. 단지 약간 늦게 보일 뿐입니다. 10초는 최대치이며 일반적인 지연 시간은 아닙니다.

2026년 9월 28일 WebSocket 변경 사항

TokenLab은 HTTP 스트리밍("stream": true, 서버 전송 이벤트)과 wss://api.tokenlab.sh/v1/responses의 WebSocket을 통해 Responses API를 제공하며, 여기서 클라이언트는 response.create 이벤트를 보냅니다. WebSocket 응답은 항상 스트리밍됩니다. background 또는 response.cancel은 지원하지 않습니다. 각 연결은 최대 60분 동안 한 번에 하나의 활성 응답을 처리합니다.

변경 전에는 두 경로의 동작이 달랐습니다. HTTP는 수명 주기 이벤트를 보관하고 상태 비저장 출력 전 실패를 재시도했습니다. WebSocket은 response.created를 즉시 전달하고 출력 전 실패를 클라이언트에 전달하며 환불했습니다. 동일한 업스트림 문제가 HTTP에서는 깔끔한 답변을, WebSocket에서는 오류를 발생시켰습니다.

이제 WebSocket 경로는 이벤트가 도착하기 전에 끊기는 스트림의 재시도를 포함하여 HTTP 규칙을 따릅니다. 내부적으로 WebSocket 턴에서 발생하는 대부분의 업스트림 실패는 출력 전에 발생했습니다. 이는 정확히 재시도가 안전한 구간입니다.

게이트웨이는 출력 전 실패 사례를 개선합니다. 스트림이 완료된다는 보장은 하지 않습니다.

다른 동작을 깨뜨리지 않고 변경 사항을 배포한 방법

작업은 조용한 동작 변경을 포착하기 위해 구축된 프로세스를 따랐습니다.

  • 동작 잠금(Behavior lock). 변경 전, 모든 WebSocket 턴 시나리오는 픽스처(fixture)로 기록되었습니다: 클라이언트가 수신하는 프레임, 수행된 업스트림 호출, 과금 결과. 이 작업 동안 스위트는 63개의 기록된 시나리오로 성장했습니다. 동작 변경은 사전에 선언되어야 합니다. 해당 선언에 명명된 픽스처만 변경될 수 있습니다. 다른 모든 픽스처는 바이트 단위로 동일하게 유지되어야 합니다.
  • 변이 검사(Mutation checks). 각 새로운 결정 규칙은 첫 번째 이벤트 타임아웃 재시도 또는 읽기 오류 미재시도와 같이 의도적으로 뒤집어 테스트되었으며, 잠금이 실패하는지 확인했습니다.
  • 검토를 통한 포착. 첫 번째 버전은 HTTP와의 패리티를 주장하며 버퍼 오버플로우 사례도 재시도 가능하게 만들었습니다. 검토 결과 HTTP는 표에 명시된 이유로 해당 사례를 절대 재시도하지 않음이 확인되었습니다. 후속 조치를 통해 이전 동작을 복원하고 경계 시나리오를 추가했습니다: 두 번째 읽기 오류는 재시도되지 않음, 남은 경로 없음, 보관된 response.created 이후의 실패, 그리고 이후 끊기는 대체 스트림 등.

재시도 후 성공한 턴의 요청 로그에는 HTTP와 마찬가지로 이전에 실패한 시도도 기록됩니다.

재시도 결정을 직접 관리하는 클라이언트 코드

스트리밍 호출에 대해 SDK 자동 재시도를 0으로 설정하십시오. 이렇게 하면 결정권이 귀하의 코드에 남습니다. 재시도 결정은 핸들러 전반에 분산시키지 말고 한 곳에서 관리하십시오. HTTP 오류의 경우 오류 처리 가이드에 설명된 대로 retryable 및 retry_after를 준수하고 요청 ID를 유지하십시오.

HTTP 기반 SSE

import os

from openai import OpenAI

with OpenAI(
    api_key=os.environ["TOKENLAB_API_KEY"],
    base_url="https://api.tokenlab.sh/v1",
    timeout=30.0,
    max_retries=0,  # 반쯤 읽은 스트림을 다시 보내는 대신 재시도 결정을 직접 관리
) as client:
    completed, saw_output = False, False
    with client.responses.create(
        model="gpt-5.6-terra",
        input="Reply with one short sentence about retries.",
        stream=True,
    ) as stream:
        for event in stream:
            if event.type == "response.output_text.delta":
                saw_output = True
                print(event.delta, end="", flush=True)
            elif event.type == "response.completed":
                completed = True
            elif event.type in {"response.failed", "response.incomplete", "error"}:
                raise RuntimeError(f"{event.type} after_output={saw_output}")
    if not completed:
        raise RuntimeError(f"stream closed before response.completed, after_output={saw_output}")
    print()

이 예제는 OpenAI SDK 2.15.0을 사용하여 https://api.tokenlab.sh/v1에 max_retries=0으로 요청합니다. saw_output을 추적하고, response.failed, response.incomplete, error 이벤트 발생 시, 그리고 response.completed 전에 스트림이 닫힐 때 예외를 발생시킵니다. 2026년 9월 28일 gpt-5.6-terra를 사용하여 프로덕션 환경에서 검증되었습니다.

실패가 saw_output == false인 상태로 도착하고 요청이 적격(상태 비저장, 재시도 가능한 실패)인 경우, TokenLab은 이미 한 번 재시도한 상태입니다. 저장된 응답, 연속 요청, 첫 번째 이벤트 타임아웃은 전혀 재시도되지 않았습니다. 새로운 요청은 새로운 생성을 의미하므로, 앱 수준에서 새로운 요청이 허용되는지 결정하십시오. saw_output == true인 경우, 실패를 보고하고 가지고 있는 내용을 보여주거나 부분 텍스트를 의도적으로 폐기하십시오.

WebSocket

import asyncio
import json
import os

import websockets

URL = "wss://api.tokenlab.sh/v1/responses"
TERMINAL = {"response.completed", "response.failed", "response.incomplete", "error"}


async def run_turn(prompt: str) -> str:
    headers = {"Authorization": f"Bearer {os.environ['TOKENLAB_API_KEY']}"}
    async with websockets.connect(URL, additional_headers=headers, max_size=None) as ws:
        await ws.send(json.dumps({
            "type": "response.create",
            "model": "gpt-5.6-terra",
            "input": prompt,
            "store": False,
        }))
        text, saw_output = [], False
        async for raw in ws:
            event = json.loads(raw)
            kind = event.get("type")
            if kind == "response.output_text.delta":
                saw_output = True
                text.append(event["delta"])
            elif kind in TERMINAL:
                if kind != "response.completed":
                    # 출력이 시작된 후의 실패는 이 턴에 대해 최종적입니다.
                    # 앱이 부분 텍스트를 폐기할 수 있는 경우에만 다시 전송하십시오.
                    raise RuntimeError(f"{kind} after_output={saw_output}: {json.dumps(event)[:300]}")
                return "".join(text)
        raise RuntimeError(f"socket closed before a terminal event, after_output={saw_output}")


print(asyncio.run(run_turn("Reply with one short sentence about retries.")))

이 예제는 websockets 16.0을 사용하며, Bearer 헤더와 함께 wss://api.tokenlab.sh/v1/responses에 연결하고 store: false로 response.create를 보낸 뒤 response.output_text.delta를 수집합니다. 완료되지 않은 종료 이벤트나 조기 종료 시 after_output과 함께 예외를 발생시킵니다. 2026년 9월 28일 gpt-5.6-terra를 사용하여 프로덕션 환경에서 검증되었습니다.

after_output 플래그는 saw_output과 같은 개념입니다. 부작용을 중복시키지 않고 새로운 턴이 가능한지 호출 코드에 알려줍니다.

자체 재시도 로직을 위한 체크리스트

  • response.completed 없이 끝나는 스트림은 매번 실패로 처리하십시오.
  • 첫 번째 라이프사이클 이벤트가 아닌, 첫 번째 출력 이벤트에서 출력이 코드에 도달했는지 여부를 boolean으로 추적하십시오.
  • 적격한 요청에 대한 출력 전 실패는 이미 게이트웨이에서 한 번의 재시도가 이루어졌습니다. 추가 시도는 귀하의 결정입니다.
  • 부분 출력 후에는 앱이 부분 텍스트를 폐기하고 두 번의 생성 비용을 지불할 수 있는 경우에만 재전송하십시오.
  • 에이전트 루프에서는 부분 스트림에 이미 코드가 처리한 도구 호출이 포함되어 있는지 확인하십시오. 실행 취소할 수 없는 부작용이 있는 턴은 재시도하지 마십시오.
  • 저장된 응답 및 previous_response_id 연속 요청의 경우, 재전송하기 전에 어떤 상태가 존재하는지 확인하십시오.
  • SDK의 스트리밍 재시도를 0으로 설정하고 재시도 결정을 하나의 함수에서 관리하십시오.
  • 전달된 답변을 그 뒤의 시도와 매칭할 수 있도록 요청 ID를 기록하십시오.

FAQ

TokenLab은 부분 출력 후 스트림을 다시 시작합니까?

아니요. 출력이 클라이언트에 도달하면 실패가 보고되며 절대 재시도되지 않습니다. 부분 텍스트를 가지고 있으므로 다시 시작하면 출력과 비용이 중복됩니다. 가지고 있는 내용을 보여줄지, 자를지, 폐기할지는 앱이 결정합니다.

게이트웨이가 요청을 재시도하면 두 번 청구됩니까?

아니요. 전달된 시도에 대해서만 비용을 지불합니다. 재시도된 요청은 업스트림에서 두 번 실행되었을 수 있지만, 첫 번째 시도에서 클라이언트에 도달한 것이 없으므로 추가적인 업스트림 비용은 TokenLab의 부담입니다. 아무것도 전달하지 못한 실패한 턴은 환불됩니다.

첫 번째 이벤트 타임아웃은 왜 재시도되지 않습니까?

업스트림이 여전히 생성 중일 수 있기 때문입니다. 재시도 시 첫 번째 시도가 계속되는 동안 동일한 작업을 두 번 수행할 수 있습니다. 첫 번째 이벤트 타임아웃은 첫 번째 이벤트 이전에 스트림을 끊는 읽기 오류와 다르게 취급됩니다.

저장된 응답이나 previous_response_id 연속 요청을 재시도할 수 있습니까?

자동으로는 불가능합니다. TokenLab은 저장된 응답, 연속 요청 또는 원본 바인딩 요청을 절대 재시도하지 않습니다. 두 번째 실행 시 저장된 응답이 중복 생성되거나 대화 상태가 분기될 수 있기 때문입니다. 재전송하기 전에 어떤 상태가 존재하는지 확인하고, 앱이 해당 상태를 조정할 수 있는 경우에만 재전송하십시오.

원시 이벤트 스트림을 직접 확인하려면 API 키를 생성하고 클라이언트가 수신하는 모든 이벤트 유형을 기록하십시오. 스트리밍 가이드와 오류 처리 가이드에서 전체 이벤트 세트를 다룹니다. 게이트웨이의 라우팅 및 복구 방식에 대한 배경 지식은 TokenLab AI API 신뢰성 인프라 및 에이전트를 위한 Responses API vs Chat Completions를 참조하십시오.

출처

관련 모델

최근 출시된 모델

이 가이드의 모델로 바로 구축하기

가격을 비교하고 라우트를 테스트한 뒤, 조사 내용을 실제 API 호출로 이어가세요.