dvgateway-sdk
v1.2.2
Published
DVGateway SDK — Real-time voice AI integration for the DVGateway media gateway
Maintainers
Readme
DVGateway SDK
DVGateway를 통해 AI 플랫폼과 실시간 음성 서비스를 5줄의 코드로 연동합니다. Node.js(TypeScript) 와 Python 두 언어를 모두 지원합니다.
Node.js SDK
설치
npm install dvgateway-sdk dvgateway-adapters30초 시작 가이드
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-dotenv30초 시작 가이드
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/fetchElevenLabs 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.0SentimentResult 인터페이스
| 필드 | 타입 | 설명 |
|------|------|------|
| 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}/stopPython 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
