하나의 SSE 스트림, 7개의 LLM 제공자
7개의 서로 다른 LLM 제공자를 지원하는 Next.js 앱을 만들었습니다.
OpenAI, Claude, Gemini, Ollama, Mistral, Groq, 그리고 Azure.
한 가지 엄격한 규칙을 세웠습니다. 브라우저는 모든 제공자에 대해 정확히 동일한 코드 경로를 사용해야 한다는 것입니다.
이 작업이 어려운 이유는 이 API들이 서로 다르기 때문입니다. 전송 방식이 다르고, 데이터 형태도 다릅니다. 어떤 것은 SSE를 사용하고, 어떤 것은 NDJSON을 사용합니다.
이러한 차이점이 UI까지 전달되게 두면, 코드는 "if" 문으로 뒤엉킨 난장판이 됩니다. 제공자를 추가할 때마다 프론트엔드는 점점 더 복잡해집니다.
저는 단일 규약(contract)을 만들어 이 문제를 해결했습니다. 모든 제공자는 브라우저에 다음과 같은 형식을 전달해야 합니다.
• data: {"delta":"
브라우저는 delta, error, [DONE] 세 가지만 이해하면 됩니다.
구현 방법은 다음과 같습니다.
Async Generator 사용 각 제공자를 일반 텍스트를 생성(yield)하는 제너레이터로 취급합니다. 이를 통해 API의 복잡성을 숨길 수 있습니다.
Wrapper 패턴
createSSEStream이라는 래퍼 함수를 만들었습니다. 이 래퍼는 와이어 포맷(wire format)을 관리합니다. 또한 스트림이 항상 종료되도록 보장합니다. 제공자가 중간에 실패하더라도 래퍼가 에러와 [DONE] 신호를 보냅니다. 이를 통해 클라이언트가 멈춰 있는(hanging) 현상을 방지합니다.유사한 API 그룹화 OpenAI, Mistral, Groq, Azure는 모두 동일한 방식을 사용합니다. 이들 모두를 위해 하나의 구현체를 작성했습니다. 이제 호환되는 새로운 제공자를 추가하는 데는 단 한 줄의 코드만 필요합니다.
예외 케이스 처리 Anthropic과 Ollama는 작동 방식이 다릅니다. Anthropic은 특정 타입의 이벤트를 사용하고, Ollama는 NDJSON을 사용합니다. 이를 위해 커스텀 파서를 작성했지만, 두 경우 모두 동일한 래퍼로 텍스트를 전달합니다. 브라우저는 그 차이를 전혀 알 필요가 없습니다.
개인정보 보호 및 단순성 이 앱은 "Bring Your Own Key" 모델을 사용합니다.
• 사용자가 자신의 API 키를 직접 입력합니다. • 키는 로컬 스토리지에 저장됩니다. • 서버는 순수 프록시 역할만 수행합니다. • 키는 데이터베이스에 절대 저장되지 않습니다.
이 방식은 복잡한 인증이나 비밀 키 관리의 필요성을 없애줍니다. 덕분에 앱을 셀프 호스팅하기가 매우 쉽습니다.
교훈은 간단합니다. 각 통합 과정을 여러분이 실제로 원하는 결과물을 생성하는 제너레이터로 모델링하세요. 그리고 한 번만 래핑하세요. 변동 사항은 제너레이터 내부에 머물게 하여 메인 로직을 깔끔하게 유지하는 것입니다.
선택 사항 학습 커뮤니티: https://t.me/GyaanSetuAi