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 🙏

© 2026 – Pkg Stats / Ryan Hefner

dvgateway-sdk

v1.2.2

Published

DVGateway SDK — Real-time voice AI integration for the DVGateway media gateway

Readme

DVGateway SDK

DVGateway를 통해 AI 플랫폼과 실시간 음성 서비스를 5줄의 코드로 연동합니다. Node.js(TypeScript)Python 두 언어를 모두 지원합니다.


Node.js SDK

설치

npm install dvgateway-sdk dvgateway-adapters

30초 시작 가이드

import { DVGatewayClient } from 'dvgateway-sdk';
import { DeepgramAdapter } from 'dvgateway-adapters/stt';
import { AnthropicAdapter } from 'dvgateway-adapters/llm';
import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';

const gw = new DVGatewayClient({
  baseUrl: 'http://localhost:8080',           // DVGateway API 서버 (:8080)
  auth: { type: 'apiKey', apiKey: 'your_key' },
});

await gw.pipeline()
  .stt(new DeepgramAdapter({ apiKey: '...', language: 'ko' }))
  .llm(new AnthropicAdapter({ apiKey: '...', model: 'claude-sonnet-4-6' }))
  .tts(new ElevenLabsAdapter({ apiKey: '...' }))
  .start();

Python SDK

설치

pip install "dvgateway[adapters]" python-dotenv

30초 시작 가이드

import asyncio
import os
from dotenv import load_dotenv

from dvgateway import DVGatewayClient
from dvgateway.adapters.stt import DeepgramAdapter
from dvgateway.adapters.llm import AnthropicAdapter
from dvgateway.adapters.tts import ElevenLabsAdapter

load_dotenv()

async def main():
    gw = DVGatewayClient(
        base_url="http://localhost:8080",
        auth={"type": "apiKey", "api_key": os.environ["DV_API_KEY"]},
    )

    await (
        gw.pipeline()
        .stt(DeepgramAdapter(api_key=os.environ["DEEPGRAM_API_KEY"], language="ko"))
        .llm(AnthropicAdapter(api_key=os.environ["ANTHROPIC_API_KEY"], model="claude-sonnet-4-6"))
        .tts(ElevenLabsAdapter(api_key=os.environ["ELEVENLABS_API_KEY"]))
        .start()
    )

asyncio.run(main())

Python — 로컬 어댑터 (오프라인)

Python SDK는 로컬 STT/LLM 어댑터를 추가로 지원합니다. 인터넷 없이 완전 오프라인 운영이 가능합니다.

# 로컬 어댑터 추가 패키지
pip install faster-whisper        # Faster-Whisper STT
pip install openai-whisper        # OpenAI Whisper STT (공식)
# Ollama는 별도 설치: https://ollama.com/install.sh
# 완전 오프라인 파이프라인
from dvgateway.adapters.stt import FasterWhisperAdapter
from dvgateway.adapters.llm import OllamaAdapter

await (
    gw.pipeline()
    # Faster-Whisper: 로컬 고속 STT
    .stt(FasterWhisperAdapter(
        model="large-v3",
        device="cpu",           # GPU: "cuda"
        compute_type="int8",    # GPU: "float16"
        language="ko",
        vad_enabled=True,
    ))
    # Qwen via Ollama: 로컬 LLM
    .llm(OllamaAdapter(
        base_url="http://localhost:11434",
        model="qwen3.5:9b",
        system_prompt="친절한 한국어 AI 상담원입니다.",
        options={"think": False},
    ))
    .tts(ElevenLabsAdapter(api_key="..."))  # TTS는 ElevenLabs 권장
    .start()
)

Node.js — OpenAI Realtime (Speech-to-Speech)

For sub-300ms end-to-end latency, use the Realtime adapter to bypass the STT→LLM→TTS chain entirely:

import { DVGatewayClient } from 'dvgateway-sdk';
import { OpenAIRealtimeAdapter } from 'dvgateway-adapters/realtime';

const gw = new DVGatewayClient({
  baseUrl: 'http://localhost:8080',
  auth: { type: 'apiKey', apiKey: 'your_key' },
});

const realtime = new OpenAIRealtimeAdapter({
  apiKey: process.env.OPENAI_API_KEY!,
  model: 'gpt-4o-mini-realtime-preview',  // cost-efficient; use gpt-4o-realtime-preview for best quality
  voice: 'alloy',                          // alloy | echo | nova | shimmer | ash | coral | sage | verse
  instructions: 'You are a helpful voice assistant. Keep answers short and conversational.',
  turnDetection: { mode: 'server_vad', silenceDurationMs: 500 },
  inputTranscription: true,
});

realtime.onAudioOutput((chunk, linkedId) => gw.injectAudio(linkedId, chunk));
realtime.onTranscript((result) => console.log(`[${result.speaker}] ${result.text}`));

gw.onCallEvent(async (event) => {
  if (event.type === 'call:new') {
    const audioStream = gw.streamAudio(event.session.linkedId, { dir: 'in' });
    await realtime.startSession(event.session.linkedId, audioStream);
  }
  if (event.type === 'call:ended') {
    await realtime.stop(event.linkedId);
  }
});

await gw.connect();

Realtime vs cascaded pipeline:

| | Cascaded (STT→LLM→TTS) | Realtime (Speech-to-Speech) | |--|---|---| | Latency | ~500ms | ~300ms | | Cost | per-service billing | single API call | | Control | full per-step control | unified session | | Best for | complex agents, custom logic | low-latency voice bots |



사람다운 음성 최적화 (Human Voice Options)

TTS 어댑터에 사람다운 음성 옵션이 내장되어 있습니다. 기본값은 한국어 대화에 최적화되어 있으며, 자연스러운 쉼, 숨소리, 필러 단어, 감탄사, 감정 표현을 제어할 수 있습니다.

import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';
import { OpenAITtsAdapter } from 'dvgateway-adapters/tts';

// ✅ 기본값: 한국어 최적화 (humanVoice 자동 활성화)
const tts = new ElevenLabsAdapter({ apiKey: '...' });
// → stability: 0.3, style: 0.6, model: eleven_multilingual_v2

// ✅ OpenAI: gpt-4o-mini-tts 자동 선택 + 음성 지시 자동 생성
const openaiTts = new OpenAITtsAdapter({ apiKey: '...' });
// → model: gpt-4o-mini-tts, voiceInstructions 자동 생성

// ✅ 커스텀 설정
const customTts = new ElevenLabsAdapter({
  apiKey: '...',
  humanVoice: {
    naturalPauses: true,      // 문장 사이 자연스러운 쉼
    breathingSounds: true,    // 숨소리 포함
    fillerWords: false,       // 필러 단어 비활성화 (음, 어, 그...)
    exclamations: true,       // 감탄사 (아, 네, 맞아요...)
    emotionalRange: 0.8,      // 감정 표현 범위 (0.0–1.0)
    speechVariation: 0.7,     // 음성 변화도 (0.0–1.0)
  },
});

// ❌ 비활성화: 기존 방식 사용
const flatTts = new ElevenLabsAdapter({
  apiKey: '...',
  humanVoice: false,
  // → stability: 0.5, style: 0.0, model: eleven_flash_v2_5 (기존 기본값)
});

HumanVoiceOptions 인터페이스

| 옵션 | 타입 | 기본값 (KO) | 설명 | |------|------|-------------|------| | naturalPauses | boolean | true | 문장·절 사이 자연스러운 쉼 | | breathingSounds | boolean | true | 긴 문장 사이 숨소리 | | fillerWords | boolean | true | 필러 단어 (음, 어, 그, 저) | | exclamations | boolean | true | 감탄사 (아, 네, 맞아요) | | emotionalRange | number | 0.6 | 감정 표현 범위 (0.0–1.0) | | speechVariation | number | 0.7 | 음성 변화도 (0.0–1.0) |

프로바이더별 매핑

| HumanVoiceOptions | ElevenLabs | OpenAI (gpt-4o-mini-tts) | |---|---|---| | emotionalRange | → style 파라미터 | → voiceInstructions 감정 지시 | | speechVariation | → stability (역비례: 1.0 - variation) | → voiceInstructions 톤 변화 지시 | | naturalPauses | 모델 내장 (multilingual v2) | → voiceInstructions 쉼 지시 | | breathingSounds | 모델 내장 (multilingual v2) | → voiceInstructions 숨소리 지시 | | fillerWords | 모델 내장 | → voiceInstructions 필러 단어 지시 | | exclamations | 모델 내장 | → voiceInstructions 감탄사 지시 |

프리셋

import { HUMAN_VOICE_DEFAULTS_KO, HUMAN_VOICE_DEFAULTS_EN } from 'dvgateway-sdk';

// 한국어 기본값 (기본 적용)
HUMAN_VOICE_DEFAULTS_KO  // { naturalPauses: true, breathingSounds: true, fillerWords: true, ... }

// 영어 기본값
HUMAN_VOICE_DEFAULTS_EN  // { naturalPauses: true, breathingSounds: true, fillerWords: false, ... }

ElevenLabs 한국어 네이티브 보이스

ElevenLabs Voice Library에서 선별한 9개 한국어 네이티브 음성이 내장되어 있습니다:

import { ELEVENLABS_KOREAN_VOICES } from 'dvgateway-adapters';

// 내장 한국어 음성 목록
for (const voice of ELEVENLABS_KOREAN_VOICES) {
  console.log(`${voice.id} — ${voice.label}`);
}
// pjJMvFj0JGWi3mogOkHH — Hyun Bin (남성, 한국어)
// t0jbNlBVZ17f02VDIeMI — 지영 / JiYoung (여성, 한국어)
// zrHiDhphv9ZnVXBqCLjz — Jennie (여성, 한국어)
// ZJCNdOEhQGMOIbMuhBME — Han Aim (남성, 한국어)
// ova4yY2jqnnUdGOmTGbx — KKC HQ (남성, 한국어)
// Xb7hH8MSUJpSbSDYk0k2 — Anna Kim (여성, 한국어)
// XrExE9yKIg1WjnnlVkGX — Yuna (여성, 한국어)
// ThT5KcBeYPX3keUQqHPh — Jina (여성, 한국어)
// Sita5M0jWFxPiECPABjR — jjeong (여성, 한국어)

// 한국어 음성으로 TTS 생성
const tts = new ElevenLabsAdapter({
  apiKey: '...',
  voiceId: ELEVENLABS_KOREAN_VOICES[0].id,  // Hyun Bin
});
# Python
from dvgateway.adapters.tts import ElevenLabsAdapter, KOREAN_VOICES

for voice in KOREAN_VOICES:
    print(f"{voice.id} — {voice.label}")

tts = ElevenLabsAdapter(api_key="...", voice_id=KOREAN_VOICES[0].id)

ElevenLabs Voice Fetch (동적 음성 목록 조회)

ElevenLabs 계정에 등록된 모든 음성(기본, 클론, 라이브러리)을 동적으로 조회합니다:

import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';

// API에서 사용 가능한 모든 음성 조회
const voices = await ElevenLabsAdapter.fetchVoices('your-elevenlabs-api-key');
for (const v of voices) {
  console.log(`${v.id} — ${v.label}`);
  // 클론된 음성: "My Voice (클론) [ko]"
  // 생성된 음성: "Custom Voice (생성됨)"
}
# Python
voices = await ElevenLabsAdapter.fetch_voices("your-elevenlabs-api-key")
for v in voices:
    print(f"{v.id} — {v.label}")

DVGateway REST API:

GET /api/v1/config/apikeys/voices/elevenlabs/fetch

ElevenLabs Voice Cloning (음성 복제)

오디오 파일을 업로드하여 커스텀 음성을 생성합니다:

import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';
import { readFileSync } from 'fs';

// 오디오 파일에서 음성 복제
const audioData = readFileSync('./my-voice-sample.wav');
const newVoice = await ElevenLabsAdapter.cloneVoice(
  'your-elevenlabs-api-key',
  '내 목소리',           // 음성 이름
  audioData,             // 오디오 데이터 (WAV, MP3, OGG)
  'my-voice-sample.wav', // 파일명
  '한국어 남성 음성',     // 설명 (선택)
);

console.log(`클론된 음성 ID: ${newVoice.id}`);  // → 새로운 voice_id

// 클론된 음성으로 TTS 생성
const tts = new ElevenLabsAdapter({
  apiKey: '...',
  voiceId: newVoice.id,
});
# Python
audio_data = open("./my-voice-sample.wav", "rb").read()
new_voice = await ElevenLabsAdapter.clone_voice(
    api_key="your-elevenlabs-api-key",
    name="내 목소리",
    audio_data=audio_data,
    file_name="my-voice-sample.wav",
    description="한국어 남성 음성",
)
print(f"클론된 음성 ID: {new_voice.id}")

DVGateway REST API:

POST /api/v1/config/apikeys/voices/elevenlabs/clone
Content-Type: multipart/form-data
Fields: name, description (optional), file (audio)

TTS 캐싱 (CachedTtsAdapter)

디스크 기반 TTS 오디오 캐싱으로 비용 절감응답 속도 향상을 동시에 달성합니다:

import { CachedTtsAdapter, ElevenLabsAdapter } from 'dvgateway-adapters';

const inner = new ElevenLabsAdapter({ apiKey: '...' });
const tts = new CachedTtsAdapter(inner, {
  provider: 'elevenlabs',
  cacheDir: './tts-cache',
  ttlMs: 7 * 24 * 60 * 60 * 1000,  // 7일
  maxEntries: 1000,                 // LRU 제한
});

// 자주 사용하는 안내 멘트 사전 캐싱
await tts.warmup([
  { text: '안녕하세요, 무엇을 도와드릴까요?' },
  { text: '잠시만 기다려 주세요.' },
  { text: '감사합니다. 좋은 하루 되세요.' },
]);

// 캐시 히트 시 즉시 응답 (API 호출 없음)
const stats = tts.getStats();
console.log(`캐시 히트: ${stats.hits}, 미스: ${stats.misses}`);
# Python
from dvgateway.adapters.tts import CachedTtsAdapter, ElevenLabsAdapter

inner = ElevenLabsAdapter(api_key="...")
tts = CachedTtsAdapter(inner, provider="elevenlabs", cache_dir="./tts-cache")

await tts.warmup([
    {"text": "안녕하세요, 무엇을 도와드릴까요?"},
    {"text": "잠시만 기다려 주세요."},
])

STT 옵션 (SttOptions)

STT 어댑터에 전달 가능한 프로바이더 독립적 옵션입니다.

import type { SttOptions } from 'dvgateway-sdk';

const sttOptions: SttOptions = {
  language: 'ko',              // 언어 코드
  diarize: true,               // 화자 분리
  vadSensitivity: 'medium',    // VAD 감도 (low/medium/high)
  endpointingMs: 300,          // 발화 종료 감지 (ms)
  utteranceEndMs: 800,         // 발화 최종 확정 (ms, 한국어 최적화)
  interimResults: true,        // 중간 결과 수신
  smartFormat: true,           // 스마트 포맷팅 (숫자, 날짜, 구두점)
  keywords: ['DVGateway', 'AI'], // 도메인 키워드 부스트
  punctuate: true,             // 구두점 자동 추가
  profanityFilter: false,      // 비속어 필터
  sentiment: true,             // 감정 분석 (Deepgram Nova-3)
};

감정 분석 (Sentiment Analysis)

Deepgram Nova-3 모델에서 실시간 감정 분석을 지원합니다. 각 발화(transcript segment)를 positive / neutral / negative로 분류하고 신뢰도 점수를 반환합니다.

import { DeepgramAdapter } from 'dvgateway-adapters/stt';

// sentiment: true 로 감정 분석 활성화
const stt = new DeepgramAdapter({
  apiKey: '...',
  language: 'ko',
  sentiment: true,  // 감정 분석 활성화
});

gw.pipeline()
  .stt(stt)
  .onTranscript((result, session) => {
    console.log(`[${result.speaker}] ${result.text}`);
    if (result.sentiment) {
      console.log(`  감정: ${result.sentiment.sentiment} (${result.sentiment.sentimentScore})`);
      // → 감정: positive (0.87)
    }
  })
  .start();
# Python
from dvgateway.adapters.stt import DeepgramAdapter

stt = DeepgramAdapter(api_key="...", language="ko", sentiment=True)

# result.sentiment.sentiment → "positive" | "neutral" | "negative"
# result.sentiment.sentiment_score → 0.0–1.0

SentimentResult 인터페이스

| 필드 | 타입 | 설명 | |------|------|------| | sentiment | 'positive' \| 'neutral' \| 'negative' | 세그먼트 감정 분류 | | sentimentScore | number | 감정 신뢰도 점수 (0.0–1.0) |

대시보드에서도 Deepgram STT 프로바이더 설정에서 "감정 분석" 체크박스로 활성화할 수 있습니다.


Node.js vs Python SDK 비교

| 항목 | Node.js | Python | |------|---------|--------| | 패키지 | dvgateway-sdk | dvgateway | | 어댑터 | dvgateway-adapters | dvgateway-adapters | | 이벤트 핸들러 | .onNewCall(cb) | .on_new_call(cb) | | 비동기 | async/await | asyncio | | Realtime (S2S) | ✅ OpenAIRealtimeAdapter | ✅ OpenAIRealtimeAdapter | | 로컬 STT 지원 | whisper.cpp (외부 서버) | ✅ whisper.cpp, Faster-Whisper, OpenAI Whisper (인프로세스) | | 로컬 LLM 지원 | Ollama (외부 서버) | ✅ Ollama, vLLM (OpenAI 호환) |

DVGateway 포트 구조

| 포트 | 용도 | |------|------| | :8080 | API 서버 — AI SDK가 연결하는 포트 | | :8081 | 대시보드 UI | | :8092 | 미디어 서버 — Dynamic VoIP ExternalMedia WebSocket (GW_MEDIA_ADDR 기본값) | | :8088 | Dynamic VoIP ARI HTTP — DVGateway가 Dynamic VoIP에 연결할 때 사용 |

⚠️ SDK는 항상 :8080 API 서버에 연결합니다. :8092는 Dynamic VoIP가 DVGateway에 연결하는 포트입니다.

아키텍처

Dynamic VoIP PBX
    │ ExternalMedia WebSocket (slin16, 16kHz)
    ↓
DVGateway (:8092 미디어 서버)
    │
    ↓
DVGateway API (:8080)
    │  ├── /api/v1/ws/callinfo    콜 이벤트
    │  ├── /api/v1/ws/stream      오디오 스트림 (AI → 구독)
    │  └── /api/v1/ws/tts/{id}    TTS 주입 (AI → Dynamic VoIP)
    │
    ↓
dvgateway-sdk (이 패키지)
    │
    ├── SttAdapter      (Deepgram, Google Chirp3, whisper.cpp, Faster-Whisper, ...)
    ├── LlmAdapter      (Anthropic Claude, OpenAI GPT, Ollama/Qwen, ...)
    ├── TtsAdapter      (ElevenLabs, OpenAI TTS, Gemini TTS, CosyVoice, ...)
    └── RealtimeAdapter (OpenAI Realtime — speech-to-speech, bypasses STT/LLM/TTS)

오디오 포맷

DVGateway는 slin16 포맷으로 오디오를 전송합니다:

  • 샘플레이트: 16,000 Hz
  • 비트깊이: 16-bit signed integer
  • 채널: Mono (1채널)
  • 엔디안: Little-endian
  • 프레임 크기: 640 bytes = 320 samples = 20ms

SDK는 자동으로 slin16 → Float32Array [-1.0, 1.0]으로 변환합니다.

API 참조

고수준 API (파이프라인 빌더)

gw.pipeline()
  .stt(adapter)                    // STT 어댑터 설정
  .llm(adapter)                    // LLM 어댑터 설정 (선택)
  .tts(adapter)                    // TTS 어댑터 설정 (선택)
  .audioFilter({ dir: 'in' })      // 오디오 방향 필터 (both/in/out)
  .onNewCall(handler)              // 새 콜 이벤트
  .onCallEnded(handler)            // 콜 종료 이벤트
  .onTranscript(handler)           // 전사 결과 이벤트
  .onError(handler)                // 오류 이벤트
  .start()                         // 시작 (Promise — 중단할 때까지 실행)

중간수준 API

// 오디오 스트림 구독
const stream = gw.streamAudio(linkedId, { dir: 'both' });
for await (const chunk of stream) {
  // chunk.samples: Float32Array
}

// TTS 주입
await gw.injectTts(linkedId, tts.synthesize('안녕하세요'));

// 컨퍼런스 TTS 브로드캐스트
await gw.broadcastTts(confId, tts.synthesize('안내말씀'));

// 콜 이벤트 구독
const unsub = gw.onCallEvent((event) => { /* ... */ });

// 세션 관리
const sessions = await gw.listSessions();
await gw.updateSessionMeta(linkedId, { customerId: 'C001' });

// 회의록
await gw.submitTranscript(confId, result);
const minutes = await gw.downloadMinutes(confId, 'txt');

Comfort Noise — AI 처리 중 Dead Air 방지

AI 음성 봇의 STT → LLM → TTS 처리 중 발생하는 무음(dead air)을 방지합니다. 게이트웨이 설정에서 GW_COMFORT_NOISE_ENABLED=true를 설정하면 활성화됩니다.

파이프라인 빌더 사용 시 자동 동작 (별도 코드 불필요):

// 파이프라인 빌더가 내부적으로 thinking:start/stop 시그널을 자동 전송합니다.
// STT 완료 → thinking:start (배경음 시작)
// LLM 완료 → thinking:stop (배경음 중단) → TTS 재생
await gw.pipeline()
  .stt(sttAdapter).llm(llmAdapter).tts(ttsAdapter)
  .start();

수동 제어 — WebSocket 시그널 (저레이턴시):

const audioStream = gw.streamAudio(linkedId);

// STT 완료 시
audioStream.sendThinkingStart();    // comfort noise 시작

// LLM + TTS 준비 완료 시
audioStream.sendThinkingStop();     // comfort noise 중단 (fade-out)
await gw.injectTts(linkedId, tts.synthesize(response));

수동 제어 — REST API:

await gw.startThinking(linkedId);   // POST /api/v1/comfort/{linkedId}/start
await gw.stopThinking(linkedId);    // POST /api/v1/comfort/{linkedId}/stop

Python SDK:

# 수동 WebSocket 시그널
audio_stream = gw.stream_audio(linked_id)
await audio_stream.send_thinking_start()
await audio_stream.send_thinking_stop()

# 수동 REST API
await gw.start_thinking(linked_id)
await gw.stop_thinking(linked_id)

게이트웨이 환경변수:

| 변수 | 기본값 | 설명 | |------|--------|------| | GW_COMFORT_NOISE_ENABLED | false | 기능 활성화 | | GW_COMFORT_NOISE_LEVEL | -50 | 노이즈 레벨 dBFS (-60=미약, -50=미묘, -40=인지 가능) | | GW_COMFORT_NOISE_FILE | (없음) | 커스텀 PCM 파일 경로 (16kHz, 16-bit LE, headerless) |

멀티 테넌트 (Multi-Tenant API)

DVGateway는 멀티 테넌트 격리를 기본 지원합니다. 테넌트별로 세션, TTS, 이벤트가 완전히 분리되며, SDK에서 tenantId 하나만 설정하면 모든 API 호출이 해당 테넌트 범위로 자동 제한됩니다.

서버 설정

# .env 또는 docker-compose 환경 변수
TENANT_CREDENTIALS="acme:password1,contoso:password2"   # 테넌트별 로그인 자격증명
DASHBOARD_PASSWORD=admin-secret                          # 관리자(전체 접근) 패스워드
JWT_SECRET=random-secret-at-least-24-chars               # JWT 서명 시크릿
TENANT_LIMITS="acme:10,contoso:20"                       # 테넌트별 동시 통화 한도
GW_RATE_LIMIT_RPM=300                                    # 테넌트당 분당 요청 한도 (기본: 300)

SDK 테넌트 연결

Node.js

import { DVGatewayClient } from 'dvgateway-sdk';

// 테넌트 ID를 설정하면 모든 API 호출에 X-Tenant-ID 헤더가 자동 추가됩니다.
const gw = new DVGatewayClient({
  baseUrl: 'http://localhost:8080',
  auth: { type: 'apiKey', apiKey: process.env.DV_API_KEY! },
  tenantId: 'acme',  // ← 이 테넌트의 세션만 접근 가능
});

// 이 테넌트 소유 세션만 반환됩니다
const sessions = await gw.listSessions();
console.log(`acme 테넌트 활성 세션: ${sessions.length}건`);

// 다른 테넌트 세션에 TTS 주입 시 403 Forbidden 반환
// await gw.injectTts('other-tenant-linked-id', audio);  // → 403

// 파이프라인도 테넌트 범위로 동작합니다
await gw.pipeline()
  .stt(new DeepgramAdapter({ apiKey: '...' }))
  .llm(new AnthropicAdapter({ apiKey: '...', model: 'claude-sonnet-4-6' }))
  .tts(new ElevenLabsAdapter({ apiKey: '...' }))
  .onNewCall((session) => {
    console.log(`[${session.tenantId}] 새 콜: ${session.linkedId}`);
  })
  .start();

Python

import asyncio
import os
from dotenv import load_dotenv
from dvgateway import DVGatewayClient

load_dotenv()

async def main():
    gw = DVGatewayClient(
        base_url="http://localhost:8080",
        auth={"type": "apiKey", "api_key": os.environ["DV_API_KEY"]},
        tenant_id="acme",  # ← 이 테넌트 범위로 제한
    )

    # 테넌트 소유 세션만 조회
    sessions = await gw.list_sessions()
    print(f"acme 테넌트 활성 세션: {len(sessions)}건")

asyncio.run(main())

테넌트별 세션 조회

// 방법 1: 클라이언트 생성 시 tenantId 설정 (권장)
const gw = new DVGatewayClient({
  baseUrl: 'http://localhost:8080',
  auth: { type: 'apiKey', apiKey: '...' },
  tenantId: 'acme',
});
const sessions = await gw.listSessions();  // acme 세션만 반환

// 방법 2: 관리자 클라이언트에서 특정 테넌트 세션 조회
const adminGw = new DVGatewayClient({
  baseUrl: 'http://localhost:8080',
  auth: { type: 'apiKey', apiKey: '...' },
  // tenantId 미설정 → 관리자 모드 (전체 접근)
});
const acmeSessions = await adminGw.listSessionsByTenant('acme');
const contosoSessions = await adminGw.listSessionsByTenant('contoso');

테넌트 격리 동작

| 동작 | 결과 | |------|------| | listSessions() | JWT의 tenantID로 자동 필터링, 해당 테넌트 세션만 반환 | | listSessionsByTenant(id) | 특정 테넌트 세션 조회 (관리자 또는 본인 테넌트만) | | TTS 주입 (injectTts, say) | 소유 세션만 허용, 타 테넌트 세션 접근 시 403 Forbidden | | 컨퍼런스 TTS (broadcastTts) | 소유 컨퍼런스만 허용, 타 테넌트 접근 시 403 Forbidden | | 콜 이벤트 (onCallEvent) | 전체 이벤트 수신 (이벤트 내 tenantId 필드로 필터링 가능) | | 세션 상세 (GET /sessions/{id}) | 소유 세션만 조회 가능, 타 테넌트 접근 시 403 Forbidden |

콜 이벤트 테넌트 필터링

gw.onCallEvent((event) => {
  if (event.type === 'call:new') {
    // event.tenantId로 테넌트 식별 가능
    console.log(`[${event.tenantId}] 새 콜: ${event.session.linkedId}`);
    console.log(`  발신: ${event.session.caller} → 착신: ${event.session.callee}`);
  }
});

Rate Limiting (요청 제한)

테넌트별 분당 요청 수가 제한됩니다 (기본: 300 RPM).

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"error": "rate limit exceeded", "retryAfter": 60}

SDK는 429 응답 시 자동으로 지수 백오프 재시도합니다.

감사 로그

모든 API 접근은 JSON Lines 형식으로 기록됩니다:

{"ts":"2026-03-17T10:00:00Z","tenantId":"acme","method":"GET","path":"/api/v1/sessions","status":200,"action":"api_access"}
{"ts":"2026-03-17T10:00:05Z","tenantId":"acme","method":"POST","path":"/api/v1/tts/linked-99","status":403,"action":"cross_tenant_attempt"}

기록 이벤트:

  • api_access — 정상 API 요청
  • auth_failed — JWT 없음 또는 검증 실패
  • rate_limited — 요청 한도 초과
  • cross_tenant_attempt — 타 테넌트 리소스 접근 시도 (JWT와 요청 대상 불일치)

인증 모드 요약

| 모드 | 설정 | tenantId | admin | 접근 범위 | |------|------|----------|-------|----------| | 인증 비활성화 | 미설정 | — | true | 전체 (개발용) | | 단일 테넌트 | DASHBOARD_PASSWORD만 | "" | true | 전체 | | 멀티 테넌트 (일반) | TENANT_CREDENTIALS 로그인 | "acme" | false | 해당 테넌트만 | | 멀티 테넌트 (관리자) | DASHBOARD_PASSWORD 로그인 | "" | true | 전체 + 설정 API |


보안

  • API Key → JWT 자동 교환: SDK가 내부적으로 처리, 사용자 코드에서 토큰 관리 불필요
  • PII 자동 마스킹: 로그에서 전화번호 등 개인정보 자동 제거 (기본 활성화)
  • TLS 강제: http://https://로 자동 업그레이드 (프로덕션)
  • mTLS 지원: 기업 환경의 클라이언트 인증서 지원

서비스 지속성

  • 자동 재연결: WebSocket 끊김 시 지수 백오프(Exponential backoff)로 자동 재연결
  • 활성 콜 보호: 재연결 후 진행 중인 콜 자동 재구독
  • 재연결 설정:
    new DVGatewayClient({
      reconnect: {
        maxAttempts: 10,        // 최대 재시도 횟수
        initialDelayMs: 2000,   // 첫 재시도 대기 (2초)
        maxDelayMs: 30_000,     // 최대 대기 (30초)
        backoffMultiplier: 2.0, // 대기 시간 배수
      },
    });

메트릭

// 레이턴시 통계
gw.metrics.logSummary();

// Prometheus 형식으로 내보내기
const promText = gw.metrics.toPrometheus();

수집 메트릭:

  • dvgw_stt_latency_ms — STT 응답 시간
  • dvgw_llm_ttft_ms — LLM 첫 토큰까지 시간
  • dvgw_tts_latency_ms — TTS 첫 오디오까지 시간
  • dvgw_e2e_latency_ms — 전체 E2E 레이턴시
  • dvgw_transcripts_total — 처리된 발화 수
  • dvgw_errors_total — 오류 수

라이선스

MIT