콘텐츠로 이동
OmniRoute source

open-sse Architecture (한국어)

별도의 워크스페이스 패키지를 사용하는 이유

섹션 제목: “별도의 워크스페이스 패키지를 사용하는 이유”

open-sse/는 다음과 같은 여러 이유로 OmniRoute 모노레포에서 독립형 워크스페이스로 구성됩니다.

  1. 재사용성 — open-sse는 npm에 @omniroute/open-sse로 게시되므로 다른 프로젝트에서도 독립적으로 사용할 수 있습니다
  2. 명확한 경계 — 스트리밍 엔진이 OmniRoute 전용 UI/DB 계층과 분리되어 있습니다
  3. 성능 — 엔진에 Next.js 종속성이 없어 CLI/서버리스 환경에서 더 빠른 콜드 스타트가 가능합니다
  4. 버전 관리 — open-sse는 자체 주기에 따라 릴리스할 수 있습니다
package.json
"workspaces": ["open-sse"]

open-sse/
├── index.ts # 공개 진입점
├── types.d.ts # 공개 타입 내보내기
├── package.json # @omniroute/open-sse
├── config/ # 제공자 설정, 상수, 레지스트리
├── executors/ # 제공자별 HTTP 실행기(67개 + base.ts/index.ts)
├── handlers/ # 요청 핸들러(chatCore, responses 등)
├── lib/ # 내부 유틸리티
├── mcp-server/ # Model Context Protocol 서버
├── services/ # 약 298개의 서비스 모듈
├── transformer/ # Responses API 형식 변환기
├── translator/ # 형식 변환(OpenAI ↔ Claude ↔ Gemini)
└── utils/ # 공유 유틸리티(로깅, 오류, 스트림 등)

| 디렉터리 | 파일 수 | 용도 | | executors/ | 167 | 제공자별 HTTP 실행기(DefaultExecutor 팩토리를 통해 통합) | | handlers/ | 157 | 요청 진입점(chatCore, responses, embeddings) | | services/ | ~536 | 라우팅, 캐싱, 속도 제한, 갱신 등 | | translator/ | 56 | 형식 변환(OpenAI ↔ Claude ↔ Gemini) | | mcp-server/ | 44 | MCP 도구 및 전송 방식 | | utils/ | ~108 | 공통 유틸리티(로깅, 오류, 스트림) | | config/ | ~339 | 제공자 설정, 상수, 레지스트리 |


모든 LLM 요청은 5단계 파이프라인을 거칩니다.

┌──────────────┐
HTTP 요청 │ 1. 라우팅 │ 콤보 해석, 모델 선택
(Next.js 라우트) └──────┬───────┘
│
▼
┌──────────────┐
│ 2. 변환 │ 형식 변환(OpenAI ↔ Claude ↔ Gemini)
└──────┬───────┘
│
▼
┌──────────────┐
│ 3. 실행 │ 제공자 실행기, HTTP, 재시도, 차단기
└──────┬───────┘
│
▼
┌──────────────┐
│ 4. 스트리밍 │ SSE 변환, 백프레셔
└──────┬───────┘
│
▼
┌──────────────┐
│ 5. 기록 │ 사용량 추적, 호출 로그, 오류 분류
└──────┬───────┘
│
▼
HTTP 응답(SSE 또는 JSON)

진입점: services/combo.ts의 handleComboChat()

요청을 구체적인 (provider, model, account, credentials) 튜플로 해석합니다.

  • ID로 콤보를 조회하거나 auto/* 모델을 위한 가상 콤보 생성
  • 라우팅 전략 적용(우선순위, 가중치, 라운드 로빈 등)
  • 비정상 제공자 제외(서킷 브레이커)
  • 사용 가능한 다음 대상 선택

auto/* 모델의 경우 이 단계에서 다음 작업도 수행합니다.

  • 16개 요소 기반 점수 산정 알고리즘 실행(services/autoCombo/)
  • 상태, 비용, 지연 시간 등을 기준으로 provider+model 쌍 선택

소스 형식(예: OpenAI)이 대상 형식(예: Claude)과 다르면 요청이 변환됩니다.

  • 시스템 프롬프트 → 시스템 메시지
  • 도구 정의 → 제공자별 도구 형식
  • 추론/사고 매개변수 → 제공자별 대응 항목
  • 메시지 역할 정규화(OpenAI 이외의 경우 developer → system)

translator/index.ts는 다음을 노출합니다.

translateRequest(body, sourceFormat, targetFormat): TranslatedRequest
needsTranslation(source, target): boolean

진입점: getExecutor(providerId).execute(request, options)

모든 제공자는 getExecutor() 팩토리의 폴백을 통해 DefaultExecutor(executors/default.ts)를 사용합니다. 실행기는 다음 작업을 수행합니다.

  • 업스트림 URL 구성(buildUrl())
  • 제공자별 헤더 추가(buildHeaders())
  • 요청 본문 변환(transformRequest())
  • 재시도 및 지수 백오프를 적용하여 HTTP 요청 전송
  • 필요한 경우 인증 갱신 처리(OAuth 제공자)

모든 실행기는 BaseExecutor(executors/base.ts, 1170 LOC)를 확장하며, 다음 기능을 제공받습니다.

  • 공통 재시도 로직
  • 프록시 통합
  • 서킷 브레이커 통합
  • 사용량 기록 훅

스트리밍 응답의 경우 실행기는 ReadableStream을 반환합니다. 핸들러는 다음 작업을 수행합니다.

  • SSE 변환을 통해 파이프 처리(createSSETransformStreamWithLogger)
  • 비활성 연결을 감지하기 위한 하트비트 핑 적용
  • 클라이언트 연결 해제를 정상적으로 처리(pipeWithDisconnect)
  • 비스트리밍 클라이언트를 위해 SSE → JSON 변환

비스트리밍 응답의 경우 실행기는 파싱된 JSON 객체를 반환하며, 이 객체는 변경 없이 그대로 전달됩니다.

응답 후(성공 또는 실패), 사용량이 기록됩니다:

  • 응답의 prompt_tokens, completion_tokens, cached_tokens
  • 가격 데이터를 기반으로 계산된 cost_usd
  • 실패한 경우 latency_ms, status, error_class
  • usage_history 테이블에 저장됨

호출 로그 아티팩트가 활성화된 경우 ${DATA_DIR}/call_logs/에 기록됩니다.


메인 요청 핸들러입니다. 크기는 크지만 구조는 명확합니다.

// chatCore.ts의 의사 구조
export async function handleChat(request: NextRequest) {
// 1. 인증 + CORS
await authenticateRequest(request);
applyCorsHeaders(response);
// 2. 본문 유효성 검사
const body = await parseRequestBody(request);
// 3. 형식 감지 + 변환
const sourceFormat = detectFormat(request);
const targetFormat = getTargetFormat(providerId);
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}
// 4. 콤보 라우팅
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
const result = await executeOnTarget(target, body);
await recordUsage(result);
return result;
} catch (err) {
// 다음 대상으로 계속 진행
}
}
// 5. 비상 폴백
return await emergencyFallback(body);
}

하나의 거대한 함수이지만, 5단계 파이프라인에 대응하는 주석이 달린 섹션으로 구성되어 있습니다.

콤보를 순서가 지정된 대상 목록으로 해석하는 라우팅 엔진입니다.

services/combo.ts
export async function handleComboChat(body, comboId): Promise<ChatResult> {
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
return await handleSingleModel(target, body);
} catch (err) {
log.warn("대상 실패, 다음 대상 시도", { target, err });
}
}
throw new ComboExhaustedError("모든 대상이 실패했습니다");
}

19가지 라우팅 전략을 지원합니다(src/shared/constants/routingStrategies.ts 참조).

전략 동작
priority 첫 번째 대상 우선의 순서 지정 목록
weighted 대상별 가중치에 따른 확률적 선택
round-robin 대상들을 순서대로 순환
context-relay 대상 간에 컨텍스트 전달
fill-first 다음 대상으로 이동하기 전에 할당량을 먼저 채움
p2c 두 개 선택지 중 최적 선택
random 균등 무작위 선택
least-used 최근 사용 횟수가 가장 적은 대상 선택
cost-optimized 정상 상태인 대상 중 가장 저렴한 대상을 우선 선택
reset-aware 제공자의 재설정 시간대를 고려
reset-window 재설정 시간대 기반 라우팅
headroom 남은 할당량 여유가 가장 큰 대상을 우선 선택
strict-random 완전 균등 선택(품질 가중치 없음)
auto 16개 요소 기반 점수 산정 사용(autoCombo/)
lkgp 마지막으로 정상 작동이 확인된 제공자를 우선 선택
context-optimized 긴 컨텍스트 요청에 가장 적합한 대상 선택
fusion 패널에 병렬로 요청한 후 판정자를 통해 종합(fusion.ts)

107개의 모든 실행기가 확장하는 추상 실행기입니다. 다음 항목을 포함합니다.

  • buildUrl() — 기본 URL 구성(사용자 정의가 필요한 경우 하위 클래스에서 재정의)
  • buildHeaders() — 기본 헤더(인증, 콘텐츠 유형)
  • transformRequest() — 기본적으로 그대로 전달
  • execute() — 재시도/백오프/차단기를 포함하는 메인 HTTP 루프
open-sse/executors/default.ts
export class DefaultExecutor extends BaseExecutor {
// 모든 OpenAI/Anthropic 호환 제공자 처리
// 제공자는 구성(URL, 인증, 헤더)을 등록하지만 실행기 로직은 공유함
}

제공자별 동작(인증 헤더, 기본 URL, 버전 헤더)은 별도의 실행기 클래스가 아니라 제공자 레지스트리를 통해 구성됩니다.

---
## 서비스 (117개 모듈)
서비스는 핸들러가 조합하여 사용하는 **집중화된 단일 목적 모듈**입니다. 주요 범주는 다음과 같습니다.
### 라우팅 및 콤보
- `combo.ts` — 콤보 라우팅 요청의 진입점
- `services/autoCombo/` — 16개 요소 기반 점수 산정, 8가지 자동 라우팅 전략
- `wildcardRouter.ts` — 와일드카드 라우트(`gpt-*`) 일치 처리
- `modelFamilyFallback.ts` — T5 패밀리 내 폴백
### 속도 제한 및 할당량
- `rateLimitManager.ts` — 키+제공자별 토큰 버킷
- `usage.ts` — 사용량 기록
- `quotaCache.ts` — 인메모리 할당량 스냅샷
### 계정 및 토큰
- `tokenRefresh.ts` — 401 응답 시 OAuth 갱신
- `accountFallback.ts` — 대체 계정으로 전환
- `sessionManager.ts` — 멀티턴 세션 상태
### 인텔리전스
- `intentClassifier.ts` — 요청 의도 분류
- `taskAwareRouter.ts` — 작업 유형별 라우팅
- `thinkingBudget.ts` — 사고 토큰 할당
- `contextManager.ts` — 라우팅 컨텍스트 주입
### 복원력
- `resilience.ts` — 재시도, 백오프, 서킷 브레이커 오케스트레이션
- `emergencyFallback.ts` — 최후 수단 폴백
- `modelDeprecation.ts` — 후속 모델로 자동 라우팅
### 상태
- `signatureCache.ts` — 요청 시그니처 기반 중복 제거
- `volumeDetector.ts` — 부하 분산
- `contextHandoff.ts` — 세션 직렬화
### 압축
- `compression/` (하위 디렉터리) — 전체 압축 파이프라인
- 엔진, 규칙 팩, 어댑터를 포함하는 39개 파일
### 스킬
- ([SKILLS.md](./SKILLS.md)에서 다룸)
### 메모리
- ([MEMORY.md](./MEMORY.md)에서 다룸)
---
## 실행기 (75개 이상의 파일)
제공자마다 하나의 파일이 있습니다. 모두 `BaseExecutor`를 확장하고 서로 다른 부분을 오버라이드합니다.
### 공통 패턴
제공자는 구성된 실행기를 반환하는 `getExecutor(providerId)`를 통해 확인됩니다. OpenAI/Anthropic 호환 제공자는 `DefaultExecutor`(`executors/default.ts`)를 사용합니다. 제공자별 동작(기본 URL, 인증 헤더, API 버전)은 `open-sse/config/providers/`에서 구성되며, 요청 본문 변환은 `open-sse/translator/`에서 처리됩니다.
**사용자 지정 URL**은 제공자 구성을 통해 설정됩니다.
```ts
// open-sse/config/providers/의 제공자 구성
export default {
id: "together",
baseURL: "https://api.together.xyz/v1/chat/completions",
}

사용자 지정 인증은 제공자 레지스트리의 인증 구성(API 키, OAuth, 헤더 프로필)을 통해 처리됩니다.

사용자 지정 요청 본문 변환(예: Anthropic에서 system을 messages와 분리)은 open-sse/translator/에서 제공자별로 등록됩니다.

### 실행기 팩토리
`executors/index.ts`는 `getExecutor(providerId)`를 내보냅니다.
```ts
import { getExecutor } from "@omniroute/open-sse/executors";
const executor = getExecutor("anthropic");
const result = await executor.execute({
model: "claude-sonnet-4-5",
messages: [...],
});

확인은 ExecutorRegistry(executors/registry.ts)를 통해 이루어집니다. 모든 특수 실행기는 executors/index.ts의 기본 제공 테이블에 선언되며, 모듈 로드 시 registerExecutor(alias, instance)를 통해 등록됩니다. getExecutor()는 레지스트리를 조회하고, 특수 항목이 없는 제공자에 대해서는 메모이제이션된 DefaultExecutor로 폴백합니다. 전체 별칭 → 실행기 매핑은 골든 테스트 tests/unit/executor-map-golden.test.ts로 명세됩니다.


3가지 형식인 OpenAI, Anthropic, Gemini와 새로운 Responses API 간에 변환합니다.

import { needsTranslation, translateRequest } from "@omniroute/open-sse/translator";
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}

일반적인 변환:

  • OpenAI → Anthropic: 별도의 system 필드, x-api-key 헤더
  • OpenAI → Gemini: messages 대신 contents, systemInstruction
  • OpenAI → Responses API: input 배열, previous_response_id 상태
  • developer 역할 → OpenAI 이외의 경우 system
  • system 역할 → GLM/ERNIE의 경우 첫 번째 사용자 메시지에 병합
  • json_schema → Gemini의 responseMimeType + responseSchema
  • tools → 제공자별 도구 형식
  • 사고 매개변수(o1, Claude) → 제공자별 대응 항목

open-sse/mcp-server/는 Model Context Protocol 서버를 구현합니다.

  • 110개의 도구(제공자 관리, 조합, 메모리, 캐시, 압축, 프록시, 스킬, 게임화, 플러그인, Notion, Obsidian, 로컬 코퍼스)
  • 3가지 전송 방식: stdio, SSE, Streamable HTTP
  • 세분화된 권한 부여를 위한 33개의 범위

도구는 open-sse/mcp-server/tools/에 독립형 파일로 등록되며, 각 파일은 이름, 스키마, 핸들러 및 범위를 내보냅니다.

open-sse/mcp-server/tools/getHealth.ts
import { z } from "zod";
export default {
name: "omniroute_get_health",
description: "Get system health snapshot",
scope: "read:health",
inputSchema: z.object({}),
handler: async (_args, ctx) => {
return await getSystemHealth();
},
};
// stdio(CLI 사용)
startMcpStdio(server);
// SSE(HTTP 기반 스트리밍)
startMcpSse(server, port);
// Streamable HTTP(최신 MCP)
startMcpStreamable(server, port);

모든 도구 호출은 범위 검사(open-sse/mcp-server/auth/)를 거칩니다.

if (!hasScope(apiKey, "providers:read")) {
throw new Error("Insufficient scope");
}

open-sse/transformer/는 Chat Completions와 Responses API 형식 간에 변환합니다.

Responses API는 상태 유지 대화(previous_response_id)를 지원하는 OpenAI의 새로운 형식입니다. 클라이언트가 Responses 요청을 보내면 OmniRoute는 다음을 수행합니다.

  1. 내부적으로 Responses → Chat Completions로 변환
  2. 제공자(Chat Completions를 지원하는 모든 제공자)에게 전송
  3. 응답을 다시 Responses 형식으로 변환
  4. 변환된 응답을 클라이언트에 스트리밍

변환기(transformer/responsesTransformer.ts)는 다음을 제공합니다.

createResponsesApiTransformStream(): TransformStream

다음을 처리합니다.

  • response.output_item.added 이벤트
  • response.output_text.delta 이벤트
  • response.completed 이벤트
  • 도구 호출 매핑(function_call ↔ tool_calls)

open-sse/config/에는 구성 계층이 포함되어 있습니다.

파일 용도
providerRegistry.ts 352개 제공자 카탈로그 기반의 채팅 모델 레지스트리
providerModels.ts 모델 별칭, 형식 매핑
constants.ts 시간 제한, 한도, 상태 코드
defaultThinkingSignature.ts 기본 Claude 사고 시그니처
modelStrip.ts (서비스 내) 제공자별 필드 제거
interface ProviderConfig {
id: string;
name: string;
baseUrl: string;
authType: "bearer" | "api-key" | "oauth" | "cookie";
executorClass: string;
defaultModel: string;
capabilities: ProviderCapabilities;
models: ModelDefinition[];
}

모듈 로드 시 Zod 검증을 수행하여 모든 제공자 구성이 유효한지 확인합니다.


라우팅 엔진에는 엄격한 성능 예산이 적용됩니다.

작업 목표 측정 기준
콤보 결정 <10ms 대상 50개 기준
속도 제한 확인 <1ms 인메모리 토큰 버킷
모델 패밀리 폴백 <5ms 캐시된 패밀리 정의
요청 라우팅 디스패치 <2ms 핫 패스
라우팅 핫 패스에서 블로킹 I/O 금지 — 모두 비동기

❌ combo.ts에서 동기식 DB 호출 — 미리 계산하고 캐시할 것 ❌ 핸들러 내 재시도 로직 — 복원력 서비스의 retry()를 사용할 것 ❌ 프로바이더 설정에 직접 접근 — providerRegistry getter를 사용할 것 ❌ 하드코딩된 폴백 체인 — modelFamilyFallback.ts에 정의할 것 ❌ 동시 요청 간 상태 변경 — 요청 범위 컨텍스트만 사용할 것


  1. 명확한 단일 책임을 갖는 open-sse/services/[serviceName].ts 생성
  2. 기본 핸들러 함수와 필요한 상수 내보내기
  3. tests/unit/services/[serviceName].test.mjs에 단위 테스트 추가
  4. 라우팅과 관련된 경우 handlers/chatCore.ts의 요청 파이프라인에 통합
  5. 서비스가 대상 선택에 영향을 주는 경우 combo.ts의 라우팅 로직 업데이트
  6. 이 파일에 문서화
  1. BaseExecutor를 확장하는 open-sse/executors/[provider].ts 생성
  2. config/providerRegistry.ts에 등록
  3. executors/index.ts 팩토리에 추가
  4. 실행기 단위 테스트 추가
  5. docs/architecture/ARCHITECTURE.md에 문서화
  1. open-sse/mcp-server/tools/[category]Tools.ts 생성 또는 업데이트
  2. 입력용 Zod 스키마 정의
  3. mcp-server/index.ts에 도구 등록
  4. mcp-server/auth/의 범위 매트릭스에 추가
  5. 단위 테스트 추가


OmniRoute 소스 코드 (a58000c7685f)

HagiCode

HagiCode는 구조화된 워크플로, 다중 에이전트 실행, Hero Dungeon 뷰를 갖춘 에이전트 코딩 작업 공간입니다.

더 스마트하고 빠르며 즐거운 에이전트 워크플로로 유용한 소프트웨어를 만드세요.

HagiCode 라이트 테마 메인 화면
  • Smart구조화된 워크플로는 의도를 아이디어부터 배포까지 실행 가능한 경로로 바꿉니다.
  • Efficient다중 에이전트 워크플로로 조사, 구현, 검토를 병렬로 진행합니다.
  • FunHero Dungeon은 긴 코딩 세션을 시각적이고 협업적인 경험으로 만듭니다.
HagiCode 방문