> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aihubmix.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 실시간 대화

> WebSocket으로 지속 연결을 맺어 음성 대화 모델과 저지연 양방향 음성·텍스트 대화를 수행

## 소개

실시간 대화는 WebSocket(클라이언트와 서버 사이에 장시간 연결을 유지하며 양방향으로 데이터를 푸시할 수 있는 프로토콜)으로 지속 연결을 맺어, 오디오 또는 텍스트 입력을 실시간으로 대화 모델에 전달하고 모델이 텍스트와 음성 답변을 증분 방식으로 푸시합니다. 음성 비서, 실시간 질의응답, 회화 연습 등 주고받는 상호작용이 필요한 시나리오에 적합합니다.

[실시간 음성 전사](/ko/api/realtime-transcription)와 마찬가지로 WebSocket을 사용하지만 용도가 다릅니다:

| 항목            | 실시간 음성 전사                         | 실시간 대화(본 페이지)                 |
| ------------- | --------------------------------- | ----------------------------- |
| 목적            | 음성을 텍스트로 변환                       | 모델과 여러 턴의 대화를 진행하고 모델이 답변을 생성 |
| 방향            | 단방향: 오디오 입력, 텍스트 출력               | 양방향: 오디오·텍스트 입력, 텍스트와 음성 출력   |
| 연결 파라미터       | `?intent=transcription&model=...` | `?model=...`만( `intent` 없음)   |
| 음성 활동 감지(VAD) | 미지원, 반드시 `null`                   | 지원, 자동 턴 감지 활성화 가능            |
| 대표 시나리오       | 회의 자막, 라이브 방송 받아쓰기                | 음성 비서, 실시간 음성 상호작용            |

**사용 가능한 모델:**

* **gpt-realtime-2.1**: 음성 대화 모델이며 오디오와 텍스트 입력을 지원하고 텍스트와 음성 답변을 실시간으로 출력합니다.

<Warning>
  **본 API는 서버 측 통합용이며, 브라우저에서 직접 연결할 수 없습니다.** 보안상의 이유로 게이트웨이는 `Origin` 헤더가 포함된 연결을 검증하여 거부하고, `openai-insecure-api-key` 서브 프로토콜을 거부하며, API 키는 표준 `Authorization` 헤더로만 받습니다. 브라우저에서 시작하는 WebSocket은 `Origin` 헤더를 자동으로 첨부하므로 거부됩니다. 프런트엔드에서 실시간 대화가 필요하면, 직접 운영하는 서버에서 게이트웨이로 연결을 맺은 뒤 오디오와 결과를 프런트엔드와 서버 사이에서 전달하십시오.
</Warning>

## 빠른 시작

### 연결 엔드포인트

```text theme={null}
wss://aihubmix.com/v1/realtime?model=gpt-realtime-2.1
```

* `model=gpt-realtime-2.1`: **필수**, 모델은 연결 시 URL 파라미터로 고정되며 세션 중에는 변경할 수 없습니다(아래 제약 참조).
* **전사와의 차이에 주의**: 대화 엔드포인트에는 `intent=transcription`을 **붙이지 않습니다**.

### 인증

핸드셰이크 시 표준 HTTP 헤더로 API 키를 전달합니다:

```text theme={null}
Authorization: Bearer $AIHUBMIX_API_KEY
```

### 오디오 형식 요구사항

현재 오디오 입력과 출력은 각각 한 가지 형식만 지원합니다. 전송 전에 오디오를 다음으로 변환하십시오:

* **인코딩**: PCM16(16비트 부호 있는 정수, 리틀 엔디언)
* **샘플 레이트**: 24000 Hz
* **채널**: 모노(mono)

즉 `audio/pcm@24000`입니다. 입력 또는 출력에서 다른 형식(예: G.711/µ-law)을 선언하면 거부되고 세션이 닫힙니다.

<Note>
  전사와 달리 대화 세션은 음성 활동 감지(turn\_detection / VAD)를 **지원합니다**. 활성화하면 모델이 발화의 끝을 자동으로 판단해 답변을 트리거하고, 비활성화(`null` 설정)하면 오디오 커밋과 답변 요청 시점을 직접 제어합니다. 필요에 따라 선택하십시오.
</Note>

## 세션 구성(session.update)

연결이 맺어진 뒤 클라이언트는 `session.update` 한 프레임을 보내 대화 파라미터(음성 톤, 시스템 지시, VAD 활성화 여부 등)를 구성할 수 있습니다. 대화 세션의 모델은 연결 URL로 이미 고정되어 있으므로 **`session.update`를 보내지 않아도 바로 대화할 수 있습니다**. 음색이나 지시를 사용자 지정할 때 보내십시오.

```json theme={null}
{
  "type": "session.update",
  "session": {
    "type": "realtime",
    "instructions": "You are a helpful voice assistant. Keep answers concise.",
    "audio": {
      "input": {
        "format": { "type": "audio/pcm", "rate": 24000 },
        "turn_detection": { "type": "server_vad" }
      },
      "output": {
        "format": { "type": "audio/pcm", "rate": 24000 },
        "voice": "alloy"
      }
    }
  }
}
```

### 구성 파라미터

<ParamField body="session.type" type="string" required>
  세션 유형이며 대화 시나리오에서는 `realtime`입니다.
</ParamField>

<ParamField body="session.instructions" type="string">
  시스템 지시로, 모델의 역할과 어조, 답변 제약을 설정합니다.
</ParamField>

<ParamField body="session.output_modalities" type="string[]">
  출력 모달리티이며 `["audio"]` 또는 `["text"]`입니다. `["audio"]`(기본값)이면 모델이 음성을 출력하고 답변 텍스트는 `response.output_audio_transcript.delta` 이벤트로 전달됩니다. `["text"]`이면 텍스트만 출력하고 텍스트는 `response.output_text.delta` 이벤트로 전달됩니다. 그 밖의 조합(예: `["audio", "text"]`)은 거부되고 `error` 이벤트가 반환됩니다.
</ParamField>

<ParamField body="session.audio.input.format" type="object" required>
  입력 오디오 형식이며 `{ "type": "audio/pcm", "rate": 24000 }`로 고정됩니다.
</ParamField>

<ParamField body="session.audio.input.turn_detection" type="object | null">
  음성 활동 감지입니다. `{ "type": "server_vad" }`를 전달하면 자동 턴 감지를 켜고, `null`을 전달하면 꺼서 클라이언트가 직접 커밋과 답변 요청을 수행합니다.
</ParamField>

<ParamField body="session.audio.output.format" type="object" required>
  출력 오디오 형식이며 `{ "type": "audio/pcm", "rate": 24000 }`로 고정됩니다.
</ParamField>

<ParamField body="session.audio.output.voice" type="string">
  답변 음성의 음색입니다. **첫 답변이 시작된 뒤에는 변경할 수 없습니다**: 세션이 생성 상태에 들어간 뒤 다시 보낸 `voice`는 무시됩니다(나머지 설정은 정상 반영). 음색을 지정하려면 첫 답변 요청 전에 설정하십시오.
</ParamField>

<Note>
  다음 기능은 **이번 릴리스에서 지원하지 않으며**, 구성하면 세션이 닫힙니다(닫힘 코드 `1008`): 대화 세션 내 인라인 전사 활성화(`audio.input.transcription`, 사유 `input_transcription_not_supported`), 대화 아이템을 통한 오디오(사유 `item_audio_not_supported`) 또는 이미지(사유 `image_input_not_supported`) 주입, 텍스트 이외의 콘텐츠 아이템 유형(사유 `unsupported_content_part`). 오디오 입력은 모두 `input_audio_buffer.append` 채널로 보내십시오.
</Note>

## 입력 전송

### 오디오 전송

PCM16 오디오를 작은 조각(예: 100ms당 한 조각)으로 자르고 base64로 인코딩한 뒤 `input_audio_buffer.append` 이벤트로 계속 전송합니다:

```json theme={null}
{
  "type": "input_audio_buffer.append",
  "audio": "<base64로 인코딩된 PCM16 오디오 조각>"
}
```

VAD를 켜면 모델이 발화 종료를 자동으로 판단해 답변을 트리거합니다. VAD를 끄면 일정 분량의 오디오를 보낸 뒤 직접 커밋하고 답변을 요청합니다:

```json theme={null}
{ "type": "input_audio_buffer.commit" }
{ "type": "response.create" }
```

### 텍스트 전송

텍스트 메시지를 직접 주입한 뒤 답변을 요청할 수도 있습니다:

```json theme={null}
{
  "type": "conversation.item.create",
  "item": {
    "type": "message",
    "role": "user",
    "content": [{ "type": "input_text", "text": "Introduce yourself in one sentence." }]
  }
}
{ "type": "response.create" }
```

## 답변 수신

서버는 이벤트를 계속 푸시합니다. 주요 이벤트 유형:

<ParamField body="session.created / session.updated" type="event">
  세션 생성과 구성 업데이트 확인입니다. `session.created`를 받으면 오디오와 텍스트를 보낼 수 있습니다.
</ParamField>

<ParamField body="conversation.item.added / conversation.item.done" type="event">
  대화 아이템 쓰기가 완료되었습니다. 사용자 입력과 모델 답변이 각각 하나의 아이템이 됩니다.
</ParamField>

<ParamField body="input_audio_buffer.speech_started / input_audio_buffer.speech_stopped" type="event">
  VAD가 켜진 상태에서 사용자가 말을 시작하고 끝냈음을 서버가 감지한 알림입니다. `speech_started`는 보통 사용자가 모델을 방해하고 있음을 뜻하며, 처리 방법은 [방해와 잘라내기](#interruption)를 참고하십시오.
</ParamField>

<ParamField body="response.created" type="event">
  한 턴의 답변 생성이 시작되었습니다.
</ParamField>

<ParamField body="response.output_item.added / response.output_item.done" type="event">
  이번 턴 출력 아이템의 시작과 종료입니다. `added` 이벤트의 `item.id`는 이후 오디오를 잘라낼 때 참조하는 대화 아이템 ID입니다.
</ParamField>

<ParamField body="response.output_audio.delta / response.output_audio.done" type="event">
  답변 음성의 **증분** 조각(base64 인코딩 PCM16)과 종료 표시이며, 받는 대로 재생할 수 있습니다.
</ParamField>

<ParamField body="response.output_audio_transcript.delta / response.output_audio_transcript.done" type="event">
  답변 음성과 문장 단위로 대응하는 전사의 **증분**과 종료 표시입니다. `delta` 필드에 이번에 추가된 텍스트가 들어 있습니다. **음성 출력을 켠 경우 답변 텍스트는 이 이벤트에서 가져옵니다**. 음성을 재생하면서 자막을 표시할 수 있습니다.
</ParamField>

<ParamField body="response.output_text.delta / response.output_text.done" type="event">
  텍스트 전용 답변의 **증분**과 종료 표시이며, 출력 모달리티를 텍스트 전용(`output_modalities: ["text"]`)으로 설정한 경우에만 발생합니다.
</ParamField>

<ParamField body="response.done" type="event">
  한 턴의 답변이 완료되었습니다. 이 이벤트는 해당 턴의 token 사용량(`usage`)을 담고 있으며 과금의 근거가 됩니다.
</ParamField>

<ParamField body="conversation.item.truncated" type="event">
  잘라내기 요청이 반영되었음을 확인합니다. [방해와 잘라내기](#interruption)를 참고하십시오.
</ParamField>

<ParamField body="error" type="event">
  오류 이벤트이며 오류 코드와 설명을 담고 있습니다. 요청 자체의 문제(예: `output_modalities` 값 오류)는 `error` 이벤트 한 건만 반환하고 세션은 계속 사용할 수 있습니다. 정책에 관련된 경우(모델 변경, 잔액 소진 등)에는 세션이 닫힙니다.
</ParamField>

<Note>
  **답변 텍스트는 이벤트를 정확히 골라야 합니다.** 기본(출력에 음성 포함)에서는 모델이 `response.output_audio_transcript.delta`만 푸시하고 `response.output_text.delta`는 **푸시하지 않습니다**. 출력 모달리티를 텍스트만으로 설정하면 텍스트가 `response.output_text.delta`로 이동합니다. 어느 모드에서든 둘 다 감시해 글자가 누락되지 않게 하십시오(아래 [실행 결과](#run-output) 참조).
</Note>

<h2 id="interruption">
  방해와 잘라내기
</h2>

모델이 말하는 중에 사용자가 말을 시작하면 이미 생성되었지만 아직 재생되지 않은 내용이 사용자의 다음 발화와 어긋납니다. WebSocket 연결에서는 재생을 클라이언트가 담당하므로, 방해 이후 정리도 클라이언트가 수행합니다.

VAD가 켜져 있으면 서버는 사용자가 말을 시작했음을 감지한 뒤 `input_audio_buffer.speech_started`를 보냅니다. 이 이벤트를 받은 클라이언트는 다음을 수행합니다:

1. **로컬 재생을 즉시 중지**하고 이번 턴 답변을 어디까지 재생했는지(밀리초) 기록합니다.
2. `conversation.item.truncate`를 보내 재생하지 않은 오디오를 대화에서 제거합니다. 다음 턴에서 모델이 해당 내용을 이미 말한 것으로 취급하지 않게 합니다.

```json theme={null}
{
  "type": "conversation.item.truncate",
  "item_id": "item_ABC123",
  "content_index": 0,
  "audio_end_ms": 1500
}
```

* `item_id`: 이번 턴 답변의 대화 아이템 ID이며 `response.output_item.added` 이벤트의 `item.id`에서 가져옵니다.
* `content_index`: 오디오 콘텐츠 파트의 인덱스이며 항상 `0`입니다.
* `audio_end_ms`: 유지할 오디오 길이(밀리초)이며 클라이언트가 실제로 재생한 위치를 적습니다.

서버가 처리를 마치면 `conversation.item.truncated`를 반환합니다. 잘라내기는 이번 턴 답변의 오디오와 해당 전사에만 영향을 주고 세션 자체는 그대로이므로 다음 턴을 이어갈 수 있습니다. OpenAI SDK에서는 `conn.conversation.item.truncate(item_id=..., content_index=0, audio_end_ms=...)`에 해당합니다.

VAD를 끈 경우(예: 누르고 말하기)에는 버튼을 누르는 것이 방해입니다. 누를 때 `response.cancel`을 보내 진행 중인 답변을 취소하고 위 절차대로 잘라냅니다. 놓으면 `input_audio_buffer.append`, `input_audio_buffer.commit`, `response.create`를 차례로 보냅니다.

## 전체 예제

아래에 세 가지 방식을 제시합니다. 하나를 선택하십시오:

* **OpenAI 공식 SDK(권장)**: WebSocket을 직접 작성할 필요 없이 `websocket_base_url`(SDK의 WebSocket 베이스 URL 파라미터)을 게이트웨이로 지정하면 공식 라이브러리를 그대로 재사용할 수 있습니다.
* **OpenAI Agents SDK**: 공식 agent 프레임워크의 실시간 음성 형태이며, `model_config`의 `url`을 게이트웨이 주소로 바꾸면 됩니다.
* **원시 websockets**: SDK를 설치하지 않고 프로토콜에 따라 프레임을 직접 주고받습니다. 의존성이 가장 적고 문제 파악이 쉽습니다.

<Note>
  **연결할 때 모델 이름을 왜 전달할까요?** AiHubMix 게이트웨이는 WebSocket 핸드셰이크 시점에 `model`로 모델 프로바이더 선택, 인증, 할당량 예약을 수행해야 하는데, `session.update`는 핸드셰이크가 끝난 뒤에 도착해 시기적으로 늦습니다. 그래서 SDK를 쓸 때는 `connect()`에 `model`을 명시적으로 전달해야 합니다(SDK가 URL 쿼리에 넣습니다). 이것이 없으면 게이트웨이가 **핸드셰이크 단계에서 거부**해 연결이 아예 맺어지지 않습니다. 전사와 달리 대화 엔드포인트에는 `intent=transcription`이 **필요하지 않습니다**.
</Note>

<CodeGroup>
  ```python Python (OpenAI SDK) theme={null}
  # 의존성: pip install "openai[realtime]"
  import asyncio
  import base64
  from openai import AsyncOpenAI

  # 공식 SDK 재사용은 두 곳만 맞추면 됩니다:
  #   1) websocket_base_url을 AiHubMix 게이트웨이로 지정(OpenAI 기본 주소가 아님)
  #   2) connect()에 model을 명시적으로 전달, 게이트웨이 핸드셰이크에 필수(대화에는 intent 불필요)
  client = AsyncOpenAI(
      api_key="sk-***",  # AiHubMix API 키로 교체하십시오
      websocket_base_url="wss://aihubmix.com/v1",
  )

  async def main():
      async with client.realtime.connect(model="gpt-realtime-2.1") as conn:
          # 1) 세션 구성: 시스템 지시 + 음색 + VAD 켜기(자동 턴 감지)
          await conn.session.update(session={
              "type": "realtime",
              "instructions": "You are a helpful voice assistant. Keep answers concise.",
              "audio": {
                  "input": {
                      "format": {"type": "audio/pcm", "rate": 24000},
                      "turn_detection": {"type": "server_vad"},
                  },
                  "output": {
                      "format": {"type": "audio/pcm", "rate": 24000},
                      "voice": "alloy",
                  },
              },
          })

          # 2) 텍스트 메시지를 보내고 답변 요청(오디오 입력은 원시 예제의 append/commit 참조)
          await conn.conversation.item.create(item={
              "type": "message",
              "role": "user",
              "content": [{"type": "input_text", "text": "What is the capital of France?"}],
          })
          await conn.response.create()

          # 3) 이번 턴 답변 수신: 텍스트는 자막 델타, 음성은 오디오 델타
          async for event in conn:
              if event.type == "response.output_audio_transcript.delta":
                  print(event.delta, end="", flush=True)  # 답변 텍스트(출력에 음성 포함 시)
              elif event.type == "response.output_audio_transcript.done":
                  print()  # 이번 턴 전사 종료
              elif event.type == "response.output_text.delta":
                  print(event.delta, end="", flush=True)  # 텍스트만 출력 시
              elif event.type == "response.output_text.done":
                  print()
              elif event.type == "response.output_audio.delta":
                  pass  # base64 PCM16 오디오 조각, 디코딩해 재생 가능
              elif event.type == "input_audio_buffer.speech_started":
                  pass  # 사용자 방해: 재생을 멈추고 conn.conversation.item.truncate(...)로 잘라내기
              elif event.type == "response.done":
                  print("\n[턴 완료]", event.response.usage)
                  break
              elif event.type == "error":
                  print("\n[오류]", event.to_dict())
                  break

  asyncio.run(main())
  ```

  ```python Python (websockets) theme={null}
  import asyncio
  import base64
  import json
  import websockets

  API_KEY = "sk-***"  # AiHubMix API 키로 교체하십시오
  URL = "wss://aihubmix.com/v1/realtime?model=gpt-realtime-2.1"

  async def main():
      # websockets >= 13은 additional_headers, 구버전은 extra_headers
      async with websockets.connect(
          URL, additional_headers={"Authorization": f"Bearer {API_KEY}"}
      ) as ws:
          # 1) 세션 구성(VAD 끄기, 답변 시점을 직접 제어)
          await ws.send(json.dumps({
              "type": "session.update",
              "session": {
                  "type": "realtime",
                  "instructions": "You are a helpful voice assistant.",
                  "audio": {
                      "input": {
                          "format": {"type": "audio/pcm", "rate": 24000},
                          "turn_detection": None,
                      },
                      "output": {
                          "format": {"type": "audio/pcm", "rate": 24000},
                          "voice": "alloy",
                      },
                  },
              },
          }))

          # 2) 로컬 PCM16 / 24kHz / 모노 원본 오디오를 읽어 조각으로 보내고 commit + 답변 요청
          async def send_audio():
              with open("audio_pcm16_24k.raw", "rb") as f:
                  pcm = f.read()
              chunk = 24000 * 2 // 10  # 100ms = 샘플 레이트 x 2바이트 / 10
              for i in range(0, len(pcm), chunk):
                  await ws.send(json.dumps({
                      "type": "input_audio_buffer.append",
                      "audio": base64.b64encode(pcm[i:i + chunk]).decode(),
                  }))
                  await asyncio.sleep(0.1)  # 실시간 속도 모사
              # VAD를 끈 경우 직접 커밋하고 답변 요청
              await ws.send(json.dumps({"type": "input_audio_buffer.commit"}))
              await ws.send(json.dumps({"type": "response.create"}))

          asyncio.create_task(send_audio())

          # 3) 답변 수신
          async for msg in ws:
              evt = json.loads(msg)
              etype = evt.get("type", "")
              if etype == "response.output_audio_transcript.delta":
                  print(evt.get("delta", ""), end="", flush=True)  # 답변 텍스트(출력에 음성 포함 시)
              elif etype == "response.output_audio_transcript.done":
                  print()  # 이번 턴 전사 종료
              elif etype == "response.output_text.delta":
                  print(evt.get("delta", ""), end="", flush=True)  # 텍스트만 출력 시
              elif etype == "input_audio_buffer.speech_started":
                  pass  # 사용자 방해: 재생을 멈추고 conversation.item.truncate 전송
              elif etype == "response.output_audio.delta":
                  pass  # base64 PCM16 오디오 조각, 디코딩해 재생 가능
              elif etype == "response.done":
                  print("\n[턴 완료]", evt.get("response", {}).get("usage"))
                  break
              elif etype == "error":
                  print("\n[오류]", evt.get("error"))
                  break

  asyncio.run(main())
  ```

  ```python Python (Agents SDK) theme={null}
  # 의존성: pip install openai-agents
  import asyncio
  from agents.realtime import (
      OpenAIRealtimeWebSocketModel,
      RealtimeAgent,
      RealtimeSession,
  )

  agent = RealtimeAgent(
      name="Assistant",
      instructions="You are a helpful voice assistant. Keep answers concise.",
  )

  async def main():
      session = RealtimeSession(
          OpenAIRealtimeWebSocketModel(),
          agent,
          None,
          model_config={
              "api_key": "sk-***",  # AiHubMix API 키로 교체하십시오
              "url": "wss://aihubmix.com/v1/realtime?model=gpt-realtime-2.1",
          },
          run_config={
              "model_settings": {
                  "modalities": ["audio"],
                  "output_audio_format": "pcm16",
                  # 반드시 꺼야 함: 기본으로 켜지는 입력 오디오 전사는 본 API가 지원하지 않아 그대로 두면 세션이 1008로 닫힙니다
                  "input_audio_transcription": None,
              },
          },
      )
      async with session:
          await session.send_message("What is the capital of France?")
          async for event in session:
              if getattr(event, "type", "") == "raw_model_event":
                  data = getattr(event, "data", None)
                  if getattr(data, "type", "") == "transcript_delta":
                      print(getattr(data, "delta", ""), end="", flush=True)
                  elif getattr(data, "type", "") == "turn_ended":
                      print("\n[턴 완료]")
                      return

  asyncio.run(main())
  ```

  ```bash 연결 테스트 (wscat) theme={null}
  # wscat으로 연결성과 인증을 빠르게 검증(먼저 npm i -g wscat 필요)
  wscat -c "wss://aihubmix.com/v1/realtime?model=gpt-realtime-2.1" \
    -H "Authorization: Bearer $AIHUBMIX_API_KEY"

  # 연결 후 session.update 한 프레임을 붙여넣고 텍스트 메시지 + response.create를 보내면 시작됩니다
  ```
</CodeGroup>

<Tip>
  임의의 오디오를 본 API가 요구하는 원본 PCM 형식으로 변환하려면 ffmpeg을 사용하십시오:

  ```bash theme={null}
  ffmpeg -i input.mp3 -f s16le -acodec pcm_s16le -ac 1 -ar 24000 audio_pcm16_24k.raw
  ```
</Tip>

### 공식 예제 재사용

OpenAI가 공개한 실시간 대화 예제는 대부분 SDK의 베이스 URL 파라미터에만 의존하므로 주소를 AiHubMix 엔드포인트로 바꾸면 재사용할 수 있습니다:

| 공식 예제                                | 주소만 바꿔 재사용 가능 여부 | 변경해야 할 부분                                                                                                   |
| ------------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| 공식 Python SDK 실시간 대화 예제              | 가능               | `websocket_base_url`을 `wss://aihubmix.com/v1`로 설정하고 `connect()`에 `model` 전달                                 |
| 공식 Node / JS SDK 실시간 대화 예제           | 가능               | `baseURL`을 `https://aihubmix.com/v1`로 설정(SDK가 자동으로 `wss`로 바꾸고 `/realtime?model=...`을 구성)                    |
| 공식 Agents SDK 실시간 음성 예제              | 가능               | `model_config.url`을 `wss://aihubmix.com/v1/realtime?model=gpt-realtime-2.1`로 설정하고 기본으로 켜지는 입력 오디오 전사를 끄기    |
| 공식 브라우저 예제(실시간 콘솔, 멀티 Agent 음성 예제 등) | 불가               | 이런 예제는 브라우저에서 직접 연결하므로 게이트웨이의 `Origin` 검증에 거부됩니다. 또한 서버에서 임시 키를 발급하는 구조에 의존하지만 본 API는 서버 측 WebSocket만 개방합니다 |

<h2 id="run-output">
  실행 결과(프로덕션 실측)
</h2>

다음은 OpenAI SDK 예제를 `aihubmix.com` 프로덕션 환경(모델 `gpt-realtime-2.1`)에서 실행한 실제 결과입니다. 세션은 `server_vad`를 켜고, prompt와 음성은 모두 영어입니다.

**텍스트 입력**

```text theme={null}
# 텍스트 "What is the capital of France?" 전송
[턴 1·텍스트] 완료 2.33s | 음성 출력 2.70s PCM16
  답변: The capital of France is Paris.
  usage: input_tokens 29 (text 29) / output_tokens 81 (audio 54 + text 27, reasoning 8 포함)
```

**오디오 입력**

```text theme={null}
# 6.3s 영어 음성을 조각으로 보내면 모델이 자동으로 턴을 감지해 답변
[턴 2·오디오] 완료 9.59s | 음성 출력 4.35s PCM16
  답변: Everything sounds good on my side, and I'm ready to keep this conversation going.
  usage: input_tokens 117 (audio 67 + text 50) / output_tokens 129 (audio 87 + text 42, reasoning 11 포함)
VAD 이벤트: input_audio_buffer.speech_started / input_audio_buffer.speech_stopped
```

**두 구성의 실측 차이**

| 구성                                       | 실측 결과                                                                                                                                      |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `turn_detection: {"type": "server_vad"}` | 오디오를 다 보내면 `input_audio_buffer.speech_started`와 `speech_stopped`를 받고 모델이 자동으로 턴을 감지해 답변을 시작합니다. 직접 `commit`과 `response.create`를 할 필요가 없습니다 |
| `output_modalities: ["text"]`            | 답변 텍스트가 `response.output_text.delta`로 이동하고 해당 턴에 음성 출력 이벤트가 없으며 usage의 `audio_tokens`가 0입니다                                                |

<Note>
  실측 확인: 기본(출력에 음성 포함)에서는 답변 텍스트가 `response.output_audio_transcript.delta`로만 글자 단위로 도착하고 `response.output_text.delta`는 나타나지 않습니다. `response.done`은 해당 턴의 `usage`를 담습니다. 핸드셰이크에는 약 2\~4초가 걸리며, 이는 세션을 맺을 때의 할당량 예약 비용입니다.
</Note>

## 과금

* **token 과금**: 대화 세션은 각 턴의 답변이 끝날 때 `response.done` 이벤트로 해당 턴의 token 사용량(`usage`)을 반환하며 이를 기준으로 과금됩니다. 사용량은 **오디오 입력 / 오디오 출력 / 텍스트 입력 / 텍스트 출력** 등의 구성 요소별로 각각 계량되며, 각 구성 요소의 단가는 [모델 상세 페이지](https://aihubmix.com/model/gpt-realtime-2.1)의 실시간 표시 가격을 따릅니다.
* **사용한 만큼 수시 정산**: 이는 장시간 연결이며, 요금은 세션 진행 중 각 턴의 답변마다 실시간으로 차감되고 세션이 끝날 때 한꺼번에 정산되지 않습니다. 세션을 맺을 때 약 1분 분량의 **할당량 예약**을 먼저 수행하며(입장 심사일 뿐 실제 과금이 아님), 세션이 끝나면 남은 예약을 해제합니다. **따라서 계정의 사용 가능 잔액이 약 1분 분량을 감당할 수 있어야 세션을 맺을 수 있습니다.**
* [사용량 및 결제](https://aihubmix.com)의 사용 내역에서 실시간 대화별 과금 기록을 하나씩 확인할 수 있습니다.

## 제한과 제약

1. **단일 세션 최대 시간**: WebSocket 연결은 최대 **62분**이며, 도달하면 서버가 먼저 닫습니다(닫힘 코드 `1000`, 사유 `session_duration_limit`). 더 길게 필요하면 구간을 나눠 재연결하십시오.
2. **유휴 연결 해제**: 클라이언트와 모델 양쪽 모두 **5분** 동안 아무 활동이 없으면 서버가 세션을 닫습니다(닫힘 코드 `1008`, 사유 `idle_timeout`). 어느 한쪽에 활동이 있으면 타이머가 초기화되므로, 출력을 계속하는 긴 답변이 중간에 끊기지 않습니다.
3. **잔액 부족**: **세션을 맺을 때** 사용 가능 잔액이 약 1분의 예약 분량을 감당하지 못하면 핸드셰이크가 곧바로 거부되고(HTTP 403) 세션이 맺어지지 않습니다. **세션 진행 중** 잔액이 소진되면 이미 맺어진 연결이 즉시 닫힙니다.
4. **서버 측 전용**: 브라우저 직접 연결은 지원하지 않습니다(`Origin` 헤더 검증). 서버 측에서 통합하십시오.
5. **모델 고정**: 모델은 연결 URL에 고정되며, 세션 중 `session.update`로 모델을 바꾸면 거부되고 세션이 닫힙니다.
6. **형식 고정**: 오디오 입력과 출력은 모두 `audio/pcm@24000` 모노만 지원하며 다른 형식은 거부됩니다.
7. **음색 고정**: `voice`는 첫 답변이 시작된 뒤에는 변경할 수 없습니다. 첫 답변 요청 전에 설정하십시오.
8. **미지원**: 대화 세션 내 인라인 전사, 대화 아이템을 통한 오디오 또는 이미지 콘텐츠 주입, 텍스트 이외의 콘텐츠 아이템 유형.
9. **한 번에 한 답변**: 같은 세션에서 동시에 진행할 수 있는 답변은 하나입니다. 이전 턴이 끝나기 전에 `response.create`를 다시 보내면 거부되고 세션이 닫힙니다(닫힘 코드 `1008`, 사유 `response_already_active`).

## 자주 발생하는 오류

| 상황                                    | 종료 코드 / 상태                                 | 설명                                                                         |
| ------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
| 서비스 미개방                               | HTTP 403 `realtime_disabled`               | 해당 환경에서 실시간 대화가 개방되지 않았습니다                                                 |
| `Origin` 헤더 포함 / 브라우저 직접 연결           | 핸드셰이크 거부                                   | 서버 측 연결로 바꾸십시오                                                             |
| 연결 시 잔액 부족                            | HTTP 403 `insufficient_user_quota`         | 약 1분의 예약 분량을 감당하지 못하는 잔액입니다. 충전 후 재시도하십시오                                  |
| 세션 중 모델 변경                            | `1008` `model_override_forbidden`          | 모델은 연결 URL에서만 지정할 수 있습니다                                                   |
| 오디오 형식이 PCM이 아님                       | `1008` `audio_format_unsupported`          | `audio/pcm@24000`으로 변환하십시오                                                 |
| 첫 답변 후 음색 변경                          | `voice` 필드 무시됨                             | 첫 답변 요청 전에 `voice`를 설정하십시오                                                 |
| 인라인 전사 활성화                            | `1008` `input_transcription_not_supported` | 대화 세션은 이번 릴리스에서 미지원입니다                                                     |
| 대화 아이템에 오디오 주입                        | `1008` `item_audio_not_supported`          | 오디오는 `input_audio_buffer.append`로 보내십시오                                    |
| 대화 아이템에 이미지 주입                        | `1008` `image_input_not_supported`         | 이번 릴리스에서 미지원입니다                                                            |
| 대화 아이템에 텍스트 이외의 콘텐츠 유형 사용             | `1008` `unsupported_content_part`          | 대화 아이템 내용은 텍스트만 지원                                                         |
| `output_modalities` 값 오류              | `error` 이벤트 `invalid_value`                | `["audio"]`와 `["text"]`만 지원하며 세션은 닫히지 않음                                   |
| 세션이 최대 시간에 도달                         | `1000` `session_duration_limit`            | 62분 상한에 도달했으므로 재연결 후 계속                                                    |
| 양쪽이 5분간 유휴                            | `1008` `idle_timeout`                      | 어느 한쪽에 활동이 있으면 타이머가 초기화됨                                                   |
| 이전 답변이 끝나기 전 다시 요청                    | `1008` `response_already_active`           | `response.done` 이후에 `response.create`를 보내십시오                               |
| `event_id` 중복                         | `1008` `duplicate_event_id`                | 클라이언트 이벤트의 `event_id`는 세션 내에서 고유해야 함                                       |
| `response.conversation`에 기본값이 아닌 값 설정 | `1008` `conversation_mode_not_supported`   | 기본 대화 모드만 지원                                                               |
| 클라이언트가 서버 전용 이벤트 전송                   | `1008` `client_forged_lifecycle_event`     | `response.*` 네임스페이스에서 클라이언트는 `response.create`와 `response.cancel`만 보낼 수 있음 |
| 세션 중 잔액 소진                            | 연결이 닫힘                                     | 충전 후 재연결하십시오                                                               |

***

업데이트: 2026-09-21
