npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

react-native-openai-realtime

v0.8.1

Published

Easy-to-use React Native library for integrating OpenAI Realtime API voice conversations

Readme

react-native-openai-realtime

Библиотека - готовый каркас для голосового/текстового чата с OpenAI Realtime (WebRTC + DataChannel) в React Native.

Содержание

  • Введение
  • Быстрый старт
  • Архитектура
  • Lifecycle и инициализация
  • ⚠️ КРИТИЧЕСКИ ВАЖНО: Правильная инициализация для переключения режимов
  • Компонент RealTimeClient (провайдер)
  • Императивный API через ref (RealTimeClientHandle)
  • Контекст: RealtimeContextValue
  • Хуки
    • useRealtime
    • useSpeechActivity
    • useMicrophoneActivity
    • useSessionOptions
  • События: onEvent и удобные client.on(…)
    • Карта входящих событий → «удобные» события
    • Обработка ошибок парсинга JSON
  • Middleware (incoming/outgoing)
  • Встроенный чат: ChatStore/ChatAdapter/ExtendedChatMsg
    • clearAdded() vs clearChatHistory()
    • Нормализация сообщений
  • Отправка сообщений
    • sendRaw
    • response.create / sendResponse / sendResponseStrict
    • response.cancel
    • session.update
    • function_call_output / sendToolOutput
  • Сессия (SessionConfig)
    • Модель, голос, модальности
    • Встроенная VAD (turn_detection)
    • Транскрипция аудио (input_audio_transcription)
    • Tools (function calling)
    • Instructions
    • Greet
  • Политика «осмысленности»: policy vs chat (isMeaningfulText)
  • Статусы и логирование
  • Низкоуровневый RealtimeClientClass (для продвинутых)
    • Методы, геттеры, события
    • SuccessHandler / SuccessCallbacks (все)
    • ErrorHandler / ErrorStage / Severity
    • Менеджеры (PeerConnection/Media/DataChannel/MessageSender/OpenAI API)
    • Concurrent Guards (защита от конкурентных вызовов)
  • Константы, DEFAULTS и applyDefaults
  • Best Practices
    • Эфемерные токены
    • PTT (push-to-talk) ручной буфер
    • Tools: авто-режим vs ручной
    • VAD: тюнинг и fallback
    • Встроенный чат vs ручной чат
    • Аудио-сессия и эхоподавление (InCallManager)
    • GlobalRealtimeProvider с ref и onToolCall
  • TypeScript Tips
  • Troubleshooting / FAQ

Введение

Библиотека делает за вас всю «тяжелую» работу по Realtime с OpenAI: WebRTC-подключение, SDP-обмен, DataChannel, локальные/удаленные аудио-потоки, DataChannel события, router событий, VAD/речевая активность, инструменты (tools), встроенный адаптируемый чат и простой React-контекст.


Быстрый старт

  1. Поднимите простой сервер для эфемерных токенов (Node/Express). Он будет ходить к OpenAI и создавать Realtime-сессию:
// server/index.ts
app.get('/realtime/session', async (_req, res) => {
  const r = await fetch('https://api.openai.com/v1/realtime/sessions', { ... });
  const j = await r.json();
  res.json(j); // в ответе есть j.client_secret.value
});
  1. В RN-приложении оберните UI провайдером и передайте tokenProvider:
import {
  RealTimeClient,
  createSpeechActivityMiddleware,
  useSpeechActivity,
} from 'react-native-openai-realtime';

const tokenProvider = async () => {
  const r = await fetch('http://localhost:8787/realtime/session');
  const j = await r.json();
  return j.client_secret.value;
};

export default function App() {
  return (
    <RealTimeClient
      tokenProvider={tokenProvider}
      incomingMiddleware={[createSpeechActivityMiddleware()]}
      session={{
        model: 'gpt-4o-realtime-preview-2024-12-17',
        voice: 'alloy',
        modalities: ['audio', 'text'],
        input_audio_transcription: { model: 'whisper-1', language: 'ru' },
        turn_detection: {
          type: 'server_vad',
          silence_duration_ms: 700,
          threshold: 0.5,
          prefix_padding_ms: 300,
        },
        tools: [
          /* ваш tools spec */
        ],
        instructions: 'Краткие и полезные ответы.',
      }}
      greetEnabled
      greetInstructions="Привет! Я на связи."
      greetModalities={['audio', 'text']}
    >
      <YourScreen />
    </RealTimeClient>
  );
}
  1. В любом компоненте используйте хук useRealtime и показывайте статус речи:
import { useRealtime, useSpeechActivity } from 'react-native-openai-realtime';

function YourScreen() {
  const { status, connect, disconnect, chat, sendRaw, sendResponseStrict } =
    useRealtime();
  const { isUserSpeaking, isAssistantSpeaking } = useSpeechActivity();

  return (
    <View>
      {isUserSpeaking && <Text>🎤 Вы говорите...</Text>}
      {isAssistantSpeaking && <Text>🔊 Ассистент отвечает...</Text>}
      {/* ваш UI */}
    </View>
  );
}

Архитектура

  • RealTimeClient — React-провайдер. Создаёт RealtimeClientClass, подписывается на статусы, создаёт встроенный чат (опционально), прокидывает API в контекст.
  • RealtimeClientClass — «ядро»: WebRTC с OpenAI, менеджеры Peer/Media/DataChannel/OpenAIApi, router событий, ChatStore.
  • EventRouter — принимает JSON из DataChannel, обрабатывает middleware и маршрутизирует в «удобные» события (user:, assistant:, tool:*).
  • ChatStore — построение ленты сообщений из дельт/финишей «пользователь/ассистент».
  • Middleware — входящие и исходящие перехватчики событий.
  • Хуки — useRealtime/useSpeechActivity/useMicrophoneActivity.

Lifecycle и инициализация

Ленивая инициализация клиента

Компонент RealTimeClient использует паттерн ленивой инициализации через ensureClient():

  • Клиент создается только при первом вызове connect() или если autoConnect={true}
  • Это позволяет обновлять tokenProvider до создания клиента
  • После создания клиента, tokenProvider можно обновить через client.setTokenProvider()

Порядок инициализации

  1. Монтирование компонента → создание контекста, но клиент еще null
  2. Вызов connect() или autoConnect={true}ensureClient() создает клиент
  3. WebRTC подключение:
    • Получение эфемерного токена через tokenProvider
    • Создание RTCPeerConnection
    • getUserMedia для локального потока
    • Создание DataChannel
    • SDP обмен с OpenAI
    • ICE gathering
  4. DataChannel открыт:
    • Если autoSessionUpdate={true} → отправка session.update
    • Если greetEnabled={true} → отправка приветственного response.create
    • Вызов onOpen(dc)

Статус 'connected' выставляется только тогда, когда и RTCPeerConnection, и DataChannel становятся готовыми. Для строгой проверки используйте client.isFullyConnected() или подписку onConnectionStateChange.

Защита от конкурентных вызовов (Concurrent Guards)

Класс RealtimeClientClass защищен от повторных вызовов:

  • connecting флаг — предотвращает повторный connect() во время подключения
  • disconnecting флаг — предотвращает повторный disconnect() во время отключения
  • При попытке повторного вызова логируется warning

Критически важный порядок операций

В методе connect() проверка микрофона происходит ДО создания PeerConnection:

// ✅ ПРАВИЛЬНЫЙ порядок:
1. Получение токена (tokenProvider)
2. Проверка микрофона (getUserMedia)  // ← ДО создания PC!
3. Создание PeerConnection
4. Добавление треков или recvonly transceiver

Почему это важно:

  • Нельзя добавлять треки в закрытый PeerConnection
  • Если микрофон недоступен, создаем recvonly transceiver сразу
  • Избегаем ошибок "Cannot add tracks: PeerConnection is closed"

Логика:

// 1. Сначала пытаемся получить микрофон
let localStream = null;
let needsRecvOnlyTransceiver = false;

if (shouldTryMic) {
  try {
    localStream = await this.mediaManager.getUserMedia();
  } catch (e) {
    if (this.options.allowConnectWithoutMic === false) {
      throw e; // Критическая ошибка
    }
    needsRecvOnlyTransceiver = true; // Fallback
  }
}

// 2. ТЕПЕРЬ создаем PeerConnection
const pc = this.peerConnectionManager.create();

// 3. Добавляем треки или transceiver
if (localStream) {
  this.mediaManager.addLocalStreamToPeerConnection(pc, localStream);
} else if (needsRecvOnlyTransceiver) {
  pc.addTransceiver('audio', { direction: 'recvonly' });
}

Примечание о reconnect и ChatStore

После disconnect() внутренние подписки EventRouter очищаются (chatWired = false). При новом connect() библиотека восстанавливает подписки ChatStore (если chatWired=false). История чата сохраняется, если deleteChatHistoryOnDisconnect={false}.


⚠️ КРИТИЧЕСКИ ВАЖНО: Правильная инициализация для переключения режимов

Проблема

Если вы планируете динамически переключаться между текстовым и голосовым режимами, нельзя использовать следующую конфигурацию:

// ❌ НЕПРАВИЛЬНО для динамического переключения
<RealTimeClient
  session={{
    model: 'gpt-4o-realtime-preview-2024-12-17',
    modalities: ['text'], // ← Только текст
    turn_detection: null, // ← Отключено
    input_audio_transcription: null, // ← Отключено
  }}
/>

Почему это не работает:

  • Отключение turn_detection и input_audio_transcription на старте требует полной ре-негоциации WebRTC для их включения
  • Библиотека не поддерживает создание нового SDP offer "на лету"
  • Попытка переключить в voice режим приведет к ошибкам обработки аудио на сервере

✅ Правильное решение

Шаг 1: Всегда передавайте полную конфигурацию с VAD и транскрипцией:

<RealTimeClient
  tokenProvider={tokenProvider}
  session={{
    model: 'gpt-4o-realtime-preview-2024-12-17',
    voice: 'shimmer',
    modalities: ['audio', 'text'], // ← Включаем оба режима
    turn_detection: {
      // ← Включаем VAD
      type: 'server_vad',
      threshold: 0.6,
      prefix_padding_ms: 200,
      silence_duration_ms: 1200,
    },
    input_audio_transcription: {
      // ← Включаем транскрипцию
      model: 'whisper-1',
    },
    instructions: 'Твои инструкции',
    tools: tools,
  }}
  initializeMode={{ type: 'text' }}
  autoSessionUpdate={true} // ← Важно: автоматическое применение сессии
>
  <YourScreen />
</RealTimeClient>
  • autoSessionUpdate={true} (по умолчанию): автоматически отправляет конфигурацию из пропа session на сервер после успешного подключения (status='connected'). Это означает, что все параметры сессии (модель, голос, модальности, VAD, транскрипция и т.д.) применяются автоматически.

  • autoSessionUpdate={false}: отключает автоматическую отправку сессии. В этом случае вам нужно будет вручную вызывать updateSession() или sendRaw() для отправки конфигурации сессии на сервер (того что вы передали внутрь session prop). Без этого сессия не будет применена, и VAD не будет работать.

Проп initializeMode

Проп initializeMode позволяет автоматически инициализировать нужный режим сессии после успешного подключения и после удачных переподключений.

Параметры:

  • type (обязательный): 'text' | 'voice' — тип сессии для инициализации
  • options (опциональный): Partial<any> — дополнительные параметры, которые будут переданы в initSession() (принимает те же параметры, что и объект session)

Примеры использования:

Провайдер автоматически вызовет initSession() с указанным типом после первого успешного подключения и после каждого удачного переподключения.


Troubleshooting

Проблема: initSession('voice') вызывает ошибку "DataChannel not open"

Решение: Дождитесь status='connected' перед вызовом

const switchToVoice = async () => {
  if (status !== 'connected') {
    console.warn('Wait for connection');
    return;
  }
  await initSession('voice');
};

Проблема: После переключения в voice режим сервер не обрабатывает голос

Решение: Проверьте, что начальная сессия включает VAD и транскрипцию (см. выше)


Проблема: isModeReady застревает в 'connecting'

Решение: Проверьте, что DataChannel открыт и сессия применилась:

useEffect(() => {
  const dc = client?.getDataChannel();
  console.log('DC state:', dc?.readyState);
  console.log('PC state:', client?.getPeerConnection()?.connectionState);
}, [client]);

Компонент RealTimeClient (провайдер)

Экспорт: RealTimeClient: FC<RealTimeClientProps>

Назначение: создаёт и конфигурирует RealtimeClientClass, обеспечивает контекст(useRealtime), можно сразу автоматически подключаться и/или прикрепить встроенный чат.

Поддерживаемые пропсы (основные):

| Prop | Тип | Default | Назначение | | ------------------------------- | ------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------ | | tokenProvider | () => Promise | required | Возвращает эфемерный токен (client_secret.value). | | webrtc | { iceServers, dataChannelLabel, offerOptions, configuration } | см. DEFAULTS | Настройки WebRTC/ICE/DataChannel. | | media | { getUserMedia?: Constraints } | audio: true | Параметры микрофона/видео для mediaDevices.getUserMedia. | | session | Partial | см. DEFAULTS | Начальная session.update (модель/голос/модальности/VAD/инструкции/tools). | | autoSessionUpdate | boolean | true | При открытии DataChannel автоматически отправлять session.update. | | greetEnabled | boolean | true | Автоприветствие после подключения. | | greetInstructions | string | "Привет! Я на связи и готов помочь." | Текст приветствия. | | greetModalities | Array<'audio' | 'text'> | ['audio', 'text'] | Модальности приветствия. | | onOpen | (dc) => void | - | DataChannel открыт. | | onEvent | (evt) => void | - | Сырые события DataChannel (1:1 с сервером). | | onError | (errorEvent) => void | - | Ошибки в библиотеке/события error от сервера. | | onUserTranscriptionDelta | ({itemId, delta}) => 'consume' | void | - | Нон-стоп дельты транскрипции пользователя. Верните 'consume', чтобы «съесть» событие. | | onUserTranscriptionCompleted | ({itemId, transcript}) => 'consume' | void | - | Финал пользовательской транскрипции. | | onAssistantTextDelta | ({responseId, delta, channel}) => 'consume' | void | - | Дельты ассистента: текст и аудио-транскрипт. | | onAssistantCompleted | ({responseId, status}) => 'consume' | void | - | Конец ответа. | | onToolCall | ({ name, args, call_id }) => Promise | any | - | Если вернёте значение — по умолчанию отправится function_call_output и начнётся новый response.create. | | incomingMiddleware | IncomingMiddleware[] | [] | Перехватчики входящих событий. | | outgoingMiddleware | OutgoingMiddleware[] | [] | Перехватчики исходящих событий. | | policyIsMeaningfulText | (text) => boolean | t => !!t.trim() | Глобальная политика «текст осмысленный?». | | chatEnabled | boolean | true | Включить встроенный ChatStore. | | chatIsMeaningfulText | (text) => boolean | - | Политика «осмысленности» именно для чата (перекрывает policy). | | chatUserAddOnDelta | boolean | true | Создавать юзер-сообщение при первой дельте. | | chatUserPlaceholderOnStart | boolean | false | Плейсхолдер на user:item_started. | | chatAssistantAddOnDelta | boolean | true | Создавать ассистентское сообщение при первой дельте. | | chatAssistantPlaceholderOnStart | boolean | false | Плейсхолдер на response_started. | | chatInverted | boolean | false | Инвертировать порядок сообщений в чате (от старых к новым). | | deleteChatHistoryOnDisconnect | boolean | true | Очищать историю при disconnect() (по умолчанию true в компоненте, не указан в DEFAULTS). | | logger | {debug,info,warn,error} | console.* | Логгер. | | autoConnect | boolean | false | Подключиться сразу. | | attachChat | boolean | true | Прикрепить встроенный чат в контекст. | | allowConnectWithoutMic | boolean | true | Разрешить подключение без микрофона (recvonly transceiver). | | initializeMode | { type: 'text' | 'voice'; options?: Partial } | - | Автоматически вызвать initSession после первого успешного подключения (и после удачного reconnect). | | attemptsToReconnect | number | 1 | Количество автоматических попыток переподключения при статусе 'error'. | | onReconnectAttempt | (attempt: number, max: number) => void | - | Коллбек перед каждой попыткой reconnect (счётчик и максимум). | | onReconnectSuccess | () => void | - | Коллбек после успешного восстановления соединения. | | onReconnectFailed | (error: any) => void | - | Коллбек после исчерпания попыток reconnect или необработанной ошибки. | | children | ReactNode или (ctx) => ReactNode | - | Если функция — получаете RealtimeContextValue. |

Примечания:

Полный список всех доступных callbacks см. в разделе "SuccessHandler / SuccessCallbacks".

  • Компонент RealTimeClient принимает все Success callbacks из RealtimeSuccessCallbacks (onPeerConnectionCreated, onDataChannelOpen и т.д.). См. раздел "SuccessHandler / SuccessCallbacks" для полного списка всех callbacks.
  • chatInverted управляет сортировкой в mergedChat: false = новые сверху, true = старые сверху
  • Компонент поддерживает forwardRef для императивного API (см. раздел "Императивный API через ref")
  • initializeMode использует внутренний хук useSessionOptions: метод initSession() будет вызван один раз после первого успешного подключения и повторно после удачного автоматического переподключения.
  • При attemptsToReconnect > 1 компонент автоматически предпринимает повторные disconnect()connect() при переходе статуса в 'error'. Коллбеки onReconnectAttempt/onReconnectSuccess/onReconnectFailed помогают отслеживать процесс.

attachChat (проп)

Управляет подпиской встроенного ChatStore на контекст:

  • attachChat={true} (по умолчанию) — ctx.chat получает обновления от встроенного ChatStore
  • attachChat={false}ctx.chat остаётся пустым массивом [], но ChatStore продолжает работать внутри клиента

Когда использовать attachChat={false}:

  • Вы строите собственный UI чата через client.onChatUpdate() напрямую
  • Хотите избежать лишних ре-рендеров контекста
  • Используете несколько провайдеров с разными чатами

Пример:

<RealTimeClient attachChat={false} chatEnabled={true}>
  <MyCustomChat /> {/* ctx.chat будет [], но client.getChat() работает */}
</RealTimeClient>

Важно: attachChat не влияет на работу ChatStore — он продолжает накапливать сообщения, доступные через client.getChat().

Автопереподключение

Если указать attemptsToReconnect > 1, компонент начнёт автоматически восстанавливать соединение при переходе статуса в 'error':

  • Перед каждой попыткой вызывается onReconnectAttempt(attempt, maxAttempts).
  • Последовательность: небольшая пауза → disconnect() → пауза → connect().
  • При успехе счётчик попыток сбрасывается, вызывается onReconnectSuccess(), а флаг initializeMode (если задан) будет применён повторно.
  • Если все попытки исчерпаны, вызывается onReconnectFailed(error) — можно отобразить UI для ручного переподключения.

Ручные вызовы connect()/disconnect() не конфликтуют с автоматикой: при успешном ручном подключении счётчик попыток также сбрасывается.


Императивный API через ref (RealTimeClientHandle)

RealTimeClient поддерживает forwardRef и экспортирует императивный API — удобно вызывать методы вне React‑дерева (например, в onToolCall, Portal, глобальных обработчиках).

TypeScript интерфейс

export type RealTimeClientHandle = {
  // Статусы/ссылки
  getClient: () => RealtimeClientClass | null;
  getStatus: () =>
    | 'idle'
    | 'connecting'
    | 'connected'
    | 'disconnected'
    | 'error';
  setTokenProvider: (tp: TokenProvider) => void;
  getCurrentMode: () => 'text' | 'voice';
  getModeStatus: () => 'idle' | 'connecting' | 'connected' | 'disconnected';

  // Соединение
  connect: () => Promise<void>;
  disconnect: () => Promise<void>;

  // Микрофон и медиа
  // НЕ РЕКОМЕНДОВАНО ИСПОЛЬЗОВАТЬ! В разработке
  enableMicrophone: () => Promise<void>;
  disableMicrophone: () => Promise<void>;

  // Отправка
  sendRaw: (e: any) => Promise<void> | void;
  sendResponse: (opts?: any) => void;
  sendResponseStrict: (opts: {
    instructions: string;
    modalities?: Array<'audio' | 'text'>;
    conversation?: 'auto' | 'none';
  }) => void;
  updateSession: (patch: Partial<any>) => void;

  // Переключение режимов сессии
  switchToTextMode: (customParams?: Partial<any>) => Promise<void>;
  switchToVoiceMode: (customParams?: Partial<any>) => Promise<void>;

  // Чат (локальные UI-бабблы)
  addMessage: (m: AddableMessage | AddableMessage[]) => string | string[];
  clearAdded: () => void;
  clearChatHistory: () => void;

  // Утилита: следующий корректный ts
  getNextTs: () => number;
};

Мини‑пример

import React, { useRef } from 'react';
import {
  RealTimeClient,
  type RealTimeClientHandle,
} from 'react-native-openai-realtime';

export default function App() {
  const rtcRef = useRef<RealTimeClientHandle>(null);

  const addUi = () => {
    rtcRef.current?.addMessage({
      type: 'ui',
      role: 'system',
      kind: 'hint',
      payload: { text: 'Подсказка ✨' },
    });
  };

  return (
    <RealTimeClient ref={rtcRef} tokenProvider={async () => 'EPHEMERAL_TOKEN'}>
      {/* ... */}
    </RealTimeClient>
  );
}

Когда использовать ref vs контекст

  • Контекст/хуки: реактивный UI (ctx.chat, ctx.status, sendResponse и т. п.) — используйте useRealtime() / useSpeechActivity() / useMicrophoneActivity()
  • ref: императивные вызовы из onToolCall / эффектов / порталов (добавление UI‑сообщений, быстрый updateSession, массовые операции)

Примечания:

  • ref — дополнение к контексту (хуки useRealtime/useSpeechActivity/useMicrophoneActivity продолжают работать как раньше)
  • addMessage через ref не отправляет событие на сервер — это локальные UI‑пузырьки

Управление микрофоном через ref

Методы enableMicrophone() и disableMicrophone() работают поверх того же RealtimeClientClass, что и хук useSessionOptions, и выполняют полноценную ре-негоциацию WebRTC.

  • enableMicrophone() запрашивает getUserMedia(), подменяет/создаёт аудио-трек в текущем RTCPeerConnection, заново формирует offer и отправляет его в OpenAI. После ответа сервера локальный поток добавляется в соединение, а Success callbacks (onMicrophonePermissionGranted, onLocalStreamSetted и т.д.) вызываются автоматически.
  • disableMicrophone() останавливает локальные аудио-треки, переключает аудио-трансивер в recvonly, повторно сигнализирует серверу и останавливает локальный поток. Success callbacks (onHangUpStarted/onHangUpDone) тоже задействуются.
  • Обе операции возвращают Promise<void> и требуют активного соединения плюс валидный tokenProvider. Отлавливайте исключения, чтобы показывать пользователю ошибки/подсказки.
  • Если микрофон недоступен и allowConnectWithoutMic={false}, connect() завершится ошибкой. При allowConnectWithoutMic={true} можно подключаться в текстовом режиме и включать микрофон позднее через enableMicrophone().

getNextTs() — утилита для ручной сортировки

Возвращает следующий корректный ts для ручного добавления сообщений:

const rtcRef = useRef<RealTimeClientHandle>(null);

// Добавление сообщения с ручным контролем порядка
const addCustomMessage = () => {
  const nextTs = rtcRef.current?.getNextTs() ?? Date.now();

  rtcRef.current?.addMessage({
    type: 'ui',
    kind: 'custom',
    role: 'assistant',
    ts: nextTs, // явный порядок
    payload: { text: 'Custom message' },
  });
};

Когда нужно:

  • Вставка сообщений с гарантированным порядком
  • Синхронизация с внешними системами (например, websocket чат)
  • Дебаггинг и тестирование сортировки

Переключение режимов через ref

Методы switchToTextMode() и switchToVoiceMode() обращаются к тому же механизму, что предоставляет useSessionOptions, поэтому их удобно вызывать из любого места, где доступен ref (push-уведомления, глобальные обработчики, Portal).

  • getCurrentMode() возвращает 'text' или 'voice'.
  • getModeStatus() показывает 'idle' | 'connecting' | 'connected' | 'disconnected' для текущей инициализации режима.
  • Если DataChannel ещё не открыт, метод выбросит исключение. Отлавливайте ошибки и отображайте пользователю подсказку.

Примечание: Подробнее о ts и time см. раздел "Контекст → Нормализация сообщений".


Контекст: RealtimeContextValue

То, что возвращает useRealtime():

| Поле/Метод | Тип | Описание | | -------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | client | RealtimeClientClass | null | Ссылка на ядро (низкоуровневые методы/геттеры). | | status | 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error' | Текущий статус соединения. | | chat | ExtendedChatMsg[] | История чата (встроенный ChatStore + ваши UI-сообщения). | | connect | () => Promise | Подключиться. | | disconnect | () => Promise | Отключиться. | | sendResponse | (opts?) => void | Обёртка над response.create. | | sendResponseStrict | ({ instructions, modalities, conversation? }) => void | Строгая версия response.create. | | updateSession | (patch) => void | Отправить session.update (частичное обновление сессии). | | sendRaw | (event: any) => void | Отправить «сырое» событие в DataChannel (через middleware). | | addMessage | (AddableMessage | AddableMessage[]) => string | string[] | Добавить ваши сообщения в локальную ленту. Возвращает ID созданного сообщения (или массив ID). | | clearAdded | () => void | Очистить локальные UI-сообщения (не трогает ChatStore). | | clearChatHistory | () => void | Очистить встроенный ChatStore (см. раздел "Встроенный чат"). | | getNextTs | () => number | Следующий порядковый ts для ручного добавления сообщений. |

Нормализация сообщений (addMessage)

Внутри RealTimeClient используется метод normalize() для обработки входящих сообщений:

  • id генерируется автоматически (если не указан)
  • ts выставляется автоматически как монотонная последовательность: nextTs = max(ts в текущей ленте) + 1
  • time всегда проставляется как Date.now() для всех типов сообщений (и для text, и для ui)
  • Если type='text', сообщение получает status: 'done' (для совместимости с ChatMsg)

Это гарантирует стабильный порядок при объединении сообщений ChatStore и ваших UI‑сообщений.

Разница между ts и time

  • ts (timestamp sequence) — порядковый номер для сортировки в ленте. Монотонный счётчик, не привязан к реальному времени.
  • timeреальная метка времени (Date.now()) создания сообщения.

Зачем нужны оба:

  • ts — корректная сортировка при объединении ChatStore и addedMessages (независимо от задержек сети)
  • time — отображение времени создания в UI (например, "2 минуты назад")

Пример:

// Сообщения могут приходить не по порядку, но ts гарантирует правильную сортировку
{ id: 'msg1', ts: 100, time: 1704067200000 } // 10:00:00
{ id: 'msg2', ts: 101, time: 1704067199000 } // 09:59:59 (пришло позже, но ts больше)

// После сортировки по ts:
[msg1, msg2] // корректный порядок диалога

В addMessage(): оба поля проставляются автоматически, но можно переопределить ts для ручного управления порядком.

Типы чата:

// Встроенное сообщение чата (от ChatStore)
type ChatMsg = {
  id: string;
  type: 'text' | 'ui'; // в типе указано 'text' | 'ui'
  role: 'user' | 'assistant';
  text?: string;
  ts: number; // порядковый номер для сортировки
  time: number; // unix timestamp создания сообщения
  status: 'streaming' | 'done' | 'canceled';
  responseId?: string;
  itemId?: string;
};

// Ваше UI-сообщение (добавляемое через addMessage)
type UIChatMsg = {
  id: string;
  type: 'ui';
  role: 'assistant' | 'user' | 'system' | 'tool';
  ts: number; // порядковый номер
  time?: number; // может отсутствовать в UI-сообщениях
  kind: string; // тип вашего UI-сообщения
  payload: any; // любые данные для рендера
};

// Объединенный тип
type ExtendedChatMsg = ChatMsg | UIChatMsg;

Примечание: ChatStore создаёт только сообщения с type: 'text'. Значение 'ui' используется для пользовательских UI-сообщений через addMessage().

ExtendedChatMsg объединяет два типа сообщений:

  • ChatMsg (type: 'text') — сообщения из реального диалога (создаются ChatStore)
  • UIChatMsg (type: 'ui') — пользовательские UI-элементы (создаются через addMessage)

Важное различие:

  • ts — порядковый номер для корректной сортировки сообщений
  • time — реальная метка времени создания (Date.now()), всегда проставляется автоматически

Это значит, что для простого UI‑баббла достаточно указать type='ui' / kind / payload: порядок (ts) и время (time) будут корректными без ручной установки.

Пример добавления UI-сообщения:

// Простое UI-уведомление
addMessage({
  type: 'ui',
  kind: 'system_notification',
  role: 'system',
  payload: {
    text: 'Соединение установлено',
    icon: 'checkmark',
    severity: 'success',
  },
});

// Карточка с данными
addMessage({
  type: 'ui',
  kind: 'weather_card',
  role: 'assistant',
  payload: {
    city: 'Киев',
    temp: 22,
    condition: 'Солнечно',
    humidity: 60,
  },
});

// Массовое добавление
const ids = addMessage([
  { type: 'text', text: 'Начинаем...', role: 'system' },
  { type: 'ui', kind: 'loader', payload: { loading: true } },
]);
console.log('Created message IDs:', ids); // ['msg-123', 'msg-124']

Хуки

useRealtime()

Возвращает RealtimeContextValue (см. выше). Используйте в любом компоненте внутри RealTimeClient.

useSpeechActivity()

Отслеживает активность речи (обновляется createSpeechActivityMiddleware):

Возвращает:

| Поле | Тип | Описание | | -------------------- | -------------- | ----------------------------------------------------- | | isUserSpeaking | boolean | Пользователь «говорит» (по событиям буфера/дельтам). | | isAssistantSpeaking | boolean | Ассистент «говорит» (выходной буфер аудио). | | inputBuffered | boolean | Входной аудио буфер в состоянии «коммита»/активности. | | outputBuffered | boolean | Выходной аудио буфер активен. | | lastUserEventAt | number | null | Время последнего пользовательского события. | | lastAssistantEventAt | number | null | Время последнего ассистентского события. |

Зачем: чтобы строить UI-анимации, индикаторы речи, VU-метры и т.д.

Пример использования:

import { useSpeechActivity, createSpeechActivityMiddleware } from 'react-native-openai-realtime';

// В провайдере обязательно добавьте middleware
<RealTimeClient
  incomingMiddleware={[createSpeechActivityMiddleware()]}
  // ...
>

// В компоненте
function SpeechIndicator() {
  const { isUserSpeaking, isAssistantSpeaking } = useSpeechActivity();

  return (
    <View>
      {isUserSpeaking && <Text>🎤 Вы говорите...</Text>}
      {isAssistantSpeaking && <Text>🔊 Ассистент отвечает...</Text>}
    </View>
  );
}

useMicrophoneActivity(options?)

Пробует оценить активность микрофона двумя путями: server-события (дельты) и getStats у локального sender (уровень сигнала). Также отслеживает уровень входящего аудио (голос ассистента).

Опции:

| Опция | Тип | Default | Описание | | -------------- | ----------------------------- | ------------ | ----------------------------------------------------------------- | | client | RealtimeClientClass | из контекста | Ядро (для нестандартных сценариев). | | mode | 'server' | 'stats' | 'auto' | 'auto' | 'server' — по событиям; 'stats' — по audioLevel; 'auto' — гибрид. | | silenceMs | number | 600 | Таймаут молчания. | | levelThreshold | number | 0.02 | Порог для stats. | | pollInterval | number | 250 | Период опроса getStats. |

Возвращает:

| Поле | Тип | Описание | | ----------- | ------------- | ------------------------------------------------------------------------------------- | | isMicActive | boolean | Активность микрофона в текущий момент. | | level | number (0..1) | Оценка уровня исходящего аудио потока (микрофон пользователя). | | remoteLevel | number (0..1) | Оценка уровня входящего аудио потока (голос ассистента). | | isCapturing | boolean | true когда микрофон активен и передаёт данные (есть enabled трек со статусом 'live'). |

Зачем: чтобы рисовать индикатор уровней, подсвечивать PTT, детектировать молчание/речь, отслеживать голос ассистента.

useSessionOptions(client)

Назначение: Управление режимами работы (voice/text) и манипуляции с сессией.

Параметры:

| Параметр | Тип | Описание | | -------- | ----------------------------- | ------------------------------------- | | client | RealtimeClientClass \| null | Экземпляр клиента (может быть null) |

Возвращаемые методы и состояния:

| Метод/Поле | Тип | Описание | | ---------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | initSession(mode, customParams?) | (mode: 'text' \| 'voice', customParams?: Partial<any>) => Promise<void> | Инициализация режима после подключения. customParams — дополнительные параметры сессии, которые будут объединены с дефолтными | | isModeReady | 'idle' \| 'connecting' \| 'connected' \| 'disconnected' | Статус готовности режима | | mode | 'text' \| 'voice' | Текущий активный режим | | clientReady | boolean | Есть валидный клиент (можно дергать initSession/проч.) | | cancelAssistantNow | (onComplete?, onFail?) => Promise<void> | Отмена текущего ответа ассистента | | handleSendMessage | (text: string) => Promise<void> | Отправка текстового сообщения | | subscribeToAssistantEvents | (onAssistantStarted?) => () => void | Подписка на события ассистента (возвращает функцию отписки) |

Важно:

  • Дождитесь clientReady === true — это значит, что хук получил реальный экземпляр клиента и может управлять сессией.
  • Методы асинхронные, обрабатывайте try/catch
  • initSession() требует активного соединения и открытого DataChannel (проверяется автоматически, таймаут 5 секунд)
  • initSession('text') отключает микрофон программно (track.enabled = false), но треки остаются в потоке
  • initSession() не запускает повторную инициализацию, если предыдущая еще выполняется
  • КРИТИЧНО: Инициализируйте режим после подключения (см. раздел "Правильная инициализация для переключения режимов")

События: onEvent и client.on(…)

  • onEvent(evt): проп RealTimeClient — raw JSON из DataChannel. Хорош для логов и троттлинга.
  • client.on('type', handler): «удобные» события, смонтированные на EventRouter.

Поддерживаемые «удобные» типы:

  • user:item_started — { itemId }

  • user:delta — { itemId, delta }

  • user:completed — { itemId, transcript }

  • user:failed — { itemId, error }

  • user:truncated — { itemId }

  • assistant:response_started — { responseId }

  • assistant:delta — { responseId, delta, channel: 'audio_transcript' | 'output_text' }

  • assistant:completed — { responseId, status: 'done' | 'canceled' }

  • tool:call_delta — { call_id, name, delta } — дельты аргументов tool

  • tool:call_done — { call_id, name, args } — собранные аргументы JSON

  • error — { scope, error } — ошибки от сервера (realtime)

События

Для полного списка всех доступных событий которые возвращает onEvent prop и их описания обратитесь к официальной документации OpenAI:

Документация: https://platform.openai.com/docs/api-reference/realtime-client-events/session/update

Начните с раздела о событиях сессии и листайте документацию ниже для просмотра всех доступных событий.

Обработка ошибок парсинга JSON

DataChannelManager обрабатывает ошибки парсинга входящих сообщений:

  • При ошибке парсинга JSON вызывается errorHandler.handle() с severity='warning'
  • В контекст ошибки включается обрезанный raw текст (до 2000 символов)
  • Добавляется hint: 'Failed to JSON.parse DataChannel message'
  • Ошибка recoverable (не критическая), соединение продолжает работать

Примечание: если onToolCall вернёт значение — по умолчанию отправляется function_call_output и тут же делается response.create (follow-up).

⚠️ Важно: Wildcard‑подписки вида 'user:*' не поддерживаются. Подписывайтесь на точные строки:

  • user:item_started | user:delta | user:completed | user:failed | user:truncated
  • assistant:item_started | assistant:response_started | assistant:delta | assistant:completed
  • tool:call_delta | tool:call_done
  • error

Обработка текстовых сообщений (ВАЖНО!)

Библиотека обрабатывает ВСЕ типы текстовых дельт от ассистента:

  • response.text.delta — основной тип для текстовых ответов
  • response.output_item.text.delta — альтернативный формат
  • response.output_text.delta — legacy формат
  • response.audio_transcript.delta — транскрипция голосового ответа

Все они маршрутизируются в одно событие assistant:delta с разными channel:

  • channel: 'output_text' — текстовый ответ
  • channel: 'audio_transcript' — транскрипция аудио

Обработка typed-ввода пользователя:

При отправке текстового сообщения через sendTextMessage():

// Создается conversation item
{
  type: 'conversation.item.created',
  item: {
    role: 'user',
    content: [{ type: 'input_text', text: 'Привет' }]
  }
}

// Router автоматически:
1. Эмитит 'user:item_started' { itemId }
2. Извлекает текст из content[0]
3. Эмитит 'user:completed' { itemId, transcript: 'Привет' }

То есть текстовые сообщения пользователя обрабатываются синхронно (нет дельт).


Middleware

Два вида:

  • incoming(ctx) — на входящих событиях до маршрутизации.
  • outgoing(event) — на исходящих событиях до отправки в DataChannel.

Подписи:

type MiddlewareCtx = {
  event: any;                // входящее сообщение (можно менять)
  send: (e: any) => Promise<void> | void; // можно послать в канал (например, cancel)
  client: RealtimeClientClass; // ядро для низкоуровневого доступа
};

type IncomingMiddleware =
  (ctx: MiddlewareCtx) => any | 'stop' | null | void | Promise<...>;

type OutgoingMiddleware =
  (event: any) => any | null | 'stop' | Promise<...>;

Поведение:

  • Верните 'stop', чтобы прекратить обработку (не маршрутизировать/не отправлять).
  • Верните модифицированный объект — он пойдёт дальше.
  • Ничего не вернёте — событие проходит «как есть».

Типичные кейсы:

  • incoming: дергать setState по audio_buffer событиям, «косметика» дельт, автокоррекция входящих.
  • outgoing: тримминг пустых input_text, добавление метаданных, блокировка cancel.

Порядок обработки входящих событий

  1. Incoming middleware — перехватчики обрабатывают сырое событие
  2. Router (createDefaultRouter) — маршрутизирует в "удобные" события
  3. hooks.onEvent — вызывается внутри router (после middleware, но до emit)

Схема:

DataChannel → incoming middleware (может изменить/остановить) → router → onEvent hook → emit('user:*', 'assistant:*', ...) → ваши подписки on()

Пример влияния middleware на onEvent:

incomingMiddleware={[
  ({ event }) => {
    if (event.type === 'response.audio_transcript.delta' && !event.delta.trim()) {
      return 'stop'; // onEvent НЕ вызовется для этого события
    }
  }
]}

Важно: Если middleware вернёт 'stop', событие не дойдёт до router, onEvent и ваши подписки client.on() не будут вызваны.

Порядок выполнения и доступ к client

Важное обновление: С версии 0.4.2 middleware получают полный доступ к клиенту (RealtimeClientClass):

type MiddlewareCtx = {
  event: any; // входящее сообщение
  send: (e: any) => Promise<void>; // отправка в DataChannel
  client: RealtimeClientClass; // ← ПОЛНЫЙ экземпляр класса
};

Схема выполнения:

DataChannel message
  ↓
Incoming Middleware[0]  ← ctx.client доступен
  ↓
Incoming Middleware[1]  ← ctx.client доступен
  ↓
EventRouter (createDefaultRouter)
  ↓
hooks.onEvent()
  ↓
Emit удобные события
  ↓
Ваши client.on() подписки

Важно: До вызова middleware клиент уже инициализирован, поэтому можно безопасно:

const myMiddleware: IncomingMiddleware = async ({ event, send, client }) => {
  // ✅ Доступны все методы клиента
  const chat = client.getChat();
  const status = client.getStatus();
  const dc = client.getDataChannel();

  // ✅ Можно отправлять события
  if (event.type === 'user:delta' && chat.length > 100) {
    await send({ type: 'conversation.item.truncate' });
  }

  // ✅ Можно модифицировать событие
  if (event.type === 'response.audio_transcript.delta') {
    return {
      ...event,
      delta: event.delta.toUpperCase(), // Капслок для транскриптов
    };
  }
};

Встроенный чат: ChatStore/ChatAdapter/ExtendedChatMsg

  • ChatStore — отслеживает дельты user/assistant, создаёт/обновляет/финализирует сообщения, фильтрует «пустое» по isMeaningfulText.
  • ChatAdapter (attachChatAdapter) — подписывает внешний setChat на обновления встроенного ChatStore. Он не меняет политику isMeaningfulText. Для изменения политики используйте chatIsMeaningfulText или policyIsMeaningfulText в пропсах RealTimeClient.
  • ExtendedChatMsg — объединяет ChatMsg (type='text') и ваши UI-сообщения (type='ui').

clearAdded() vs clearChatHistory()

Важное различие:

  • clearAdded() — удаляет только ваши UI-сообщения, добавленные через addMessage(). Не трогает встроенный ChatStore.
  • clearChatHistory() — очищает встроенный ChatStore (user/assistant сообщения из реального диалога). Не трогает ваши UI-сообщения.
// Пример использования
const { chat, addMessage, clearAdded, clearChatHistory } = useRealtime();

// Добавляем UI-сообщение
addMessage({ type: 'ui', kind: 'hint', payload: { text: 'Подсказка' } });

// chat теперь содержит: [...chatStoreMessages, uiMessage]

clearAdded(); // Удалит только UI-сообщение
clearChatHistory(); // Удалит только chatStore сообщения

Управление порядком сообщений в чате

Проп chatInverted управляет сортировкой сообщений в mergedChat:

  • chatInverted: false (по умолчанию) — новые сообщения сверху (нисходящий порядок по ts)
  • chatInverted: true — старые сообщения сверху (восходящий порядок по ts)
<RealTimeClient
  chatInverted={true} // старые сообщения сверху
  // ...
>
  <YourScreen />
</RealTimeClient>

Примечание: сортировка применяется к объединенному массиву [...chatStoreMessages, ...addedMessages] и влияет на порядок отображения в UI.

Опции ChatStore:

| Опция | Тип | Default | Описание | | --------------------------- | --------------------- | ---------- | ------------------------------------------------- | | isMeaningfulText | (t:string) => boolean | !!t.trim() | Политика «осмысленности». | | userAddOnDelta | boolean | true | Добавлять юзер-сообщение при 1-й дельте. | | userPlaceholderOnStart | boolean | false | Создавать пустышку при user:item_started. | | assistantAddOnDelta | boolean | true | Добавлять ассистентское сообщение при 1-й дельте. | | assistantPlaceholderOnStart | boolean | false | Плейсхолдер при response_started. |


Отправка сообщений

sendRaw(event)

Низкоуровневый отправитель. Используется и внутри обёрток. Проходит через outgoingMiddleware.

Популярные события:

  • Создать пользовательское текстовое сообщение + запросить ответ:
await sendRaw({
  type: 'conversation.item.create',
  item: {
    type: 'message',
    role: 'user',
    content: [{ type: 'input_text', text: 'Привет' }],
  },
});
await sendRaw({ type: 'response.create' });
  • PTT (ручной буфер):
await sendRaw({ type: 'input_audio_buffer.commit' });
await sendRaw({ type: 'response.create' });
await sendRaw({ type: 'input_audio_buffer.clear' });
  • Отменить текущий ответ:
await sendRaw({ type: 'response.cancel' });

sendResponse(opts?)

Обёртка над response.create. Если opts не переданы — отправится пустой объект {} (сервер использует дефолтные настройки сессии).

sendResponse({ instructions: 'Скажи тост', modalities: ['audio', 'text'] });

// Без параметров (сервер использует текущую session)
sendResponse();

Отличие от sendResponseStrict(): instructions НЕ обязателен, можно вызывать без аргументов.

sendResponseStrict({ instructions, modalities, conversation? })

Строгая версия с обязательными инструкциями.

Примеры разных кейсов:

// Простой текстовый ответ
sendResponseStrict({
  instructions: 'Объясни квантовую физику простыми словами',
  modalities: ['text'],
});

// Аудио-ответ с контекстом истории
sendResponseStrict({
  instructions: 'Продолжи разговор и ответь на последний вопрос',
  modalities: ['audio', 'text'],
  conversation: 'default',
});

// One-shot вопрос без сохранения в истории
sendResponseStrict({
  instructions: 'Какая погода в Киеве? Ответь одним предложением',
  modalities: ['text'],
  conversation: 'none',
});

// Смена языка ответа
sendResponseStrict({
  instructions: 'Reply in English from now on',
  modalities: ['audio', 'text'],
});

Параметры:

  • instructions (обязательный) — инструкции для модели
  • modalities — массив модальностей ответа: ['audio'], ['text'] или ['audio', 'text']
  • conversation — 'default' (с историей) или 'none' (без истории)

sendTextMessage(text, options?)

Отправить текстовое сообщение (используется в useSessionOptions):

client.sendTextMessage('Привет', {
  responseModality: 'text', // 'text' или 'audio'
  instructions: 'Ответь кратко',
  conversation: 'auto', // 'auto' или 'none'
});

Что происходит внутри:

  1. Создается conversation.item.create с типом message
  2. Отправляется response.create с указанными параметрами

Параметры:

  • text — текст сообщения
  • options.responseModality — модальность ответа ('text' или 'audio')
  • options.instructions — инструкции для модели
  • options.conversation — 'auto' (с историей) или 'none' (без истории)

Важно: По умолчанию conversation: 'auto' сохраняет контекст разговора.

response.cancel

Отменяет текущую генерацию ответа:

sendRaw({ type: 'response.cancel' });

updateSession(patch)

Частичное обновление session:

updateSession({
  voice: 'ash',
  turn_detection: {
    type: 'server_vad',
    silence_duration_ms: 800,
    threshold: 0.6,
    prefix_padding_ms: 300,
  },
  modalities: ['text', 'audio'],
  tools: [], // отключить инструменты
});

sendToolOutput(call_id, output)

Ручной вывод результата инструмента:

client.sendToolOutput(call_id, { temperature: 22, city: 'Kyiv' });
// ВАЖНО: response.create НЕ вызывается автоматически!
client.sendResponse(); // нужно вызвать вручную

Что происходит внутри:

Метод отправляет событие conversation.item.create с типом function_call_output:

sendToolOutput(call_id: string, output: any) {
  this.sendRaw({
    type: 'conversation.item.create',
    item: {
      type: 'function_call_output',
      call_id,
      output: JSON.stringify(output), // output сериализуется в JSON строку
    },
  });
}

Отличие от onToolCall:

  • onToolCall (если вернёте значение) → автоматически отправляет function_call_output + делает response.create
  • sendToolOutput → только отправляет function_call_output, response.create нужно вызывать вручную

Когда использовать:

  • Ручной контроль — не возвращайте значение в onToolCall, слушайте tool:call_done и используйте sendToolOutput + sendResponse()
  • Автоматический режим — возвращайте значение в onToolCall (библиотека сделает всё сама)

Пример ручного режима:

<RealTimeClient
  onToolCall={async ({ name, args, call_id }) => {
    // Не возвращаем значение — ручной режим
    const result = await callAPI(name, args);
    // Библиотека НЕ отправит function_call_output автоматически
  }}
/>

// Слушаем tool:call_done и отправляем вручную
client.on('tool:call_done', async ({ call_id, name, args }) => {
  const output = await processTool(name, args);
  client.sendToolOutput(call_id, output);
  client.sendResponse(); // вручную инициируем ответ
});

Пример полного компонента:

function MyComponent() {
  const { client } = useRealtime();

  useEffect(() => {
    if (!client) return;

    // Подписываемся на завершение tool call
    const unsubscribe = client.on(
      'tool:call_done',
      async ({ call_id, name, args }) => {
        try {
          // Обрабатываем tool вручную
          const output = await handleTool(name, args);

          // Отправляем результат
          client.sendToolOutput(call_id, output);

          // ВАЖНО: вручную инициируем response.create
          client.sendResponse();
        } catch (error) {
          // Обработка ошибок
          client.sendToolOutput(call_id, { error: error.message });
          client.sendResponse();
        }
      }
    );

    return unsubscribe;
  }, [client]);

  return <View>{/* ваш UI */}</View>;
}

Сессия (SessionConfig)

type SessionConfig = {
  model?: string; // 'gpt-4o-realtime-preview-2024-12-17'
  voice?: VoiceId; // 'alloy' | 'ash' | 'verse' | ...
  modalities?: Array<'audio' | 'text'>; // ['audio','text'] или ['text']
  turn_detection?: {
    type: 'server_vad';
    silence_duration_ms?: number;
    threshold?: number;
    prefix_padding_ms?: number;
  };
  input_audio_transcription?: { model: string; language?: string }; // 'whisper-1' или 'gpt-4o-transcribe'
  tools?: any[]; // Realtime tools spec (проксируется в OpenAI)
  instructions?: string; // системные инструкции
};

Подсказка: библиотека экспортирует VOICE_IDS и тип VoiceId — можно строить picker голосов без «ручных» массивов:

import { VOICE_IDS, type VoiceId } from 'react-native-openai-realtime';

VOICE_IDS.map(v => /* отрисуйте pill и вызовите updateSession({ voice: v as VoiceId }) */);

Формат tools:

tools: [
  {
    type: 'function',
    name: 'get_weather',
    description: 'Return weather by city',
    parameters: {
      type: 'object',
      properties: { city: { type: 'string' } },
      required: ['city'],
      additionalProperties: false,
    },
  },
];

autoSessionUpdate (проп)

Назначение: Автоматически отправляет session.update при открытии DataChannel.

  • autoSessionUpdate={true} (по умолчанию) — библиотека отправляет session.update сразу после подключения
  • autoSessionUpdate={false} — вы управляете сессией вручную через updateSession()

Когда отключать:

  • Динамическая конфигурация сессии (например, выбор модели после подключения)
  • Условное применение настроек (разные tools для разных пользователей)
  • A/B тестирование параметров VAD

Пример ручного управления:

<RealTimeClient
  autoSessionUpdate={false}
  session={undefined} // не передаём начальную сессию
  onOpen={async (dc) => {
    const userPrefs = await fetchUserPreferences();
    client.updateSession({
      voice: userPrefs.voice,
      tools: userPrefs.enabledTools,
      turn_detection: userPrefs.vadConfig
    });
  }}
/>

Важно: Если autoSessionUpdate={false}, но session передана — она НЕ отправится автоматически. Нужно вызвать updateSession() вручную.

Greet (приветствие)

  • greetEnabled (по умолчанию true) — автоприветствие после подключения
  • greetInstructions — текст приветствия
  • greetModalities — ['audio','text'] и т.д.

По умолчанию greetEnabled=true. При открытии DataChannel отправляется response.create с указанным приветствием.

Важно: Если greetEnabled=true, но greetInstructions не заданы, applyDefaults подставит дефолтные instructions и modalities из DEFAULTS.


Политика «осмысленности»: policy vs chat

Предикат isMeaningfulText определяет: считать ли текст «содержательным».

  • policyIsMeaningfulText — глобальный дефолт (используют разные модули).
  • chatIsMeaningfulText — перекрывает policy только для встроенного чата.

Приоритет: chat.isMeaningfulText ?? policy.isMeaningfulText ?? (t => !!t.trim())

Частые сценарии:

  • Жёсткий глобальный фильтр, мягкий в чате.
  • Отключить встроенный чат и применять политику в своих middleware/хуках.

Статусы и логирование

  • status: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'
  • logger: { debug, info, warn, error } — можно прокинуть в RealTimeClient.

Статус 'error' устанавливается при:

  • Критических ошибках (severity='critical') через ErrorHandler
  • Провале WebRTC соединения (pc.connectionState='failed')
  • Ошибке получения токена (fetch_token)
  • Неожиданных исключениях в процессе подключения

⚠️ ВАЖНО: тип RealtimeStatus включает зарезервированные статусы ('user_speaking','assistant_speaking'), но они НЕ АКТИВНЫ в текущей версии. Для отслеживания речи используйте useSpeechActivity() хук.


Низкоуровневый RealtimeClientClass

Конструктор:

new RealtimeClientClass(
  options: RealtimeClientOptionsBeforePrune,
  successHandler?: SuccessHandler,
  errorHandler?: ErrorHandler
)

Методы:

| Метод | Описание | | --------------------------------------------------------------- | ------------------------------------------------------------------------ | | connect() | Установить WebRTC, сделать SDP-обмен, открыть DataChannel. | | disconnect() | Закрыть DataChannel/Media/Peer, почистить подписки и (по настройке) чат. | | enableMicrophone() | Включить микрофон и выполнить повторную SDP-негоциацию. | | disableMicrophone() | Отключить микрофон, остановить треки и обновить сессию без аудио. | | sendRaw(event) | Отправить событие (через outgoingMiddleware). | | sendResponse(opts?) | Обёртка над response.create. | | sendResponseStrict({ instructions, modalities, conversation? }) | Строгая версия response.create. | | updateSession(patch) | Отправить session.update. | | sendToolOutput(call_id, output) | Ручной вывод инструмента. | | setTokenProvider(fn) | Обновить токен-провайдер на лету. | | on(type, handler) | Подписаться на удобные события (user:, assistant:, tool:*, error). | | onConnectionStateChange(fn) | Подписка на смену статуса ('connecting','connected',...). | | onChatUpdate(fn) | Подписка на обновления встроенного чата. | | clearChatHistory() | Очистить историю чата. | | isConnected() | Соединение установлено? | | isFullyConnected() | И PeerConnection, и DataChannel в состоянии 'open'. | | getConnectionState() | Текущий connectionState. | | getStatus() | Текущий connectionState (алиас). | | getPeerConnection() | RTCPeerConnection. | | getDataChannel() | RTCDataChannel. | | getLocalStream() | MediaStream (локальный). | | getRemoteStream() | MediaStream (удалённый). | | getChat() | Текущая история встроенного чат-стора. |

isFullyConnected() отличается от isConnected(): метод возвращает true только если