Files
2nd/10_Wiki/Topic_Programming/Subsystems/LLM_프로바이더_추상화.md
T
Antigravity Agent e2c5471046 wiki: Topic_Blog 신규 문서 일괄 추가 + ASTRA 성장 자산 동기화
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:55:38 +09:00

8.1 KiB

id, title, category, status, verification_status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, created_at, updated_at, review_reason, merge_history, tags, raw_sources, applied_in, github_commit
id title category status verification_status canonical_id aliases duplicate_of source_trust_level confidence_score created_at updated_at review_reason merge_history tags raw_sources applied_in github_commit
llm-provider-abstraction LLM 프로바이더 추상화 AI_and_ML draft applied
provider abstraction
adapter pattern
LLM 라우팅
prefix routing
SSE
스트리밍
엔진 폴백
A 0.92 2026-06-13 2026-06-13
llm
provider
adapter
streaming
sse
astraai
AstraAI/src/features/providers/types.ts
AstraAI/src/features/providers/index.ts
AstraAI/src/features/providers/anthropic.ts
AstraAI/src/core/services.ts
AstraAI

LLM 프로바이더 추상화

🎯 한 줄 통찰 (One-line insight)

여러 LLM 공급자(로컬 LM Studio/Ollama, 클라우드 OpenRouter/Anthropic/Gemini)를 한 코드에서 쓰려면 "model id prefix 로 라우팅 + 공급자별 어댑터가 차이를 흡수 + 모두 같은 SSE 포맷으로 정규화"가 핵심이며, AstraAI 는 이 어댑터 패턴으로 호출부를 공급자 무관하게 유지한다 [S1][S2].

🧠 핵심 개념 (Core concepts)

  1. Prefix 라우팅: anthropic:claude-..., gemini:..., openrouter:... 처럼 model id 의 접두사로 공급자를 결정. 접두사 없으면 로컬 엔진 [S1].
  2. 어댑터 패턴: 공급자마다 streamX(context, params) 함수가 그 API 의 차이(인증·바디·스트림 형식)를 흡수하고 표준 인터페이스(StreamParams)를 받는다 [S2][S3].
  3. 출력 정규화(SSE): 각 어댑터가 응답 스트림을 OpenAI 호환 SSE 로 변환해 반환 → 기존 SSE 파서 하나로 모두 소비 [S2][S3].
  4. 로컬 엔진 폴백: 로컬은 LM Studio↔Ollama 간 자동 폴백(전송 오류/5xx/빈 응답 시) [S4].
  5. 에러 응답 passthrough: 인증 실패·4xx·5xx 는 .ok=false Response 로 그대로 반환, 호출부가 .text() 로 메시지 추출 [S2][S3].

🧩 추출된 패턴 (Extracted patterns)

  • 양방향 prefix 변환: parseModelPrefix(id)(분해) ↔ makeModelId(provider, model)(조립) — UI/config 저장과 라우팅이 1:1 [S1].
  • switch dispatch: streamCloudCompletionswitch (hit.provider) 로 어댑터 선택 — 공급자 추가는 case 하나 [S2].
  • 입력 정규화(공급자 제약 흡수): Anthropic 어댑터는 (1) system 을 top-level 로 분리, (2) 연속 같은 role 병합(교대 강제), (3) 첫 메시지 user 강제(dummy 삽입) [S3].
  • 활성 공급자만 병렬 조회: listAllCloudModels 가 enabled+apiKey 인 공급자만 Promise.all 로 모델 목록 수집 [S2].
  • 하드코딩 fallback 목록: 모델 list API 가 없는 공급자(Anthropic)는 알려진 모델 목록을 반환하되 사용자 직접 입력도 허용(validate 안 함) [S3].

📖 세부 내용 (Details)

prefix 를 왜 쓰는가

같은 model name 이 OpenRouter 와 직통에 동시 존재할 수 있어 출처를 명시해야 라우팅이 모호하지 않다. 또 UI 드롭다운 그룹화('OpenRouter · ...')와, 접두사 없는 옛 설정('gemma4:e2b')의 자동 로컬 경로 처리에 유리하다 [S1].

어댑터가 흡수하는 차이 (Anthropic 예)

  • base URL https://api.anthropic.com/v1, 인증 x-api-key + anthropic-version 헤더.
  • system 은 messages 가 아니라 top-level system 필드 → 어댑터가 분리·병합.
  • role 교대 강제 → 연속 같은 role 병합.
  • 응답 스트림이 OpenAI 와 다른 event 형식 → transformAnthropicStream 으로 변환 후 새 Response 로 wrap [S3].

이 정규화 덕분에 상위 agent.ts 의 스트림 파서는 공급자를 전혀 모른다 — "차이는 가장자리(어댑터)에서 흡수, 중심은 단일 포맷".

로컬 엔진 폴백 (services.ts)

AIService.chat 은 사용자 설정 엔진을 먼저 시도하고, 전송 오류/HTTP 실패/빈 응답이면 다른 엔진으로 폴백한다. 빈 응답은 soft failure 로 취급해 재시도, 두 엔진 모두 빈 응답이면 empty: true 결과를 반환해 호출부가 사용자에게 안내하게 한다(예외 삼키지 않음) [S4].

⚖️ 비교 및 선택 기준 (Comparison & decision criteria)

접근 장점 단점 언제
prefix 라우팅 모호성 없음, UI 그룹화 id 규칙 학습 필요 다중 공급자/중복 모델명
어댑터별 함수 차이 격리, 추가 쉬움 공급자마다 구현 공급자 API 가 제각각일 때
공통 SSE 정규화 파서 1개로 통일 변환 레이어 비용 스트리밍 다공급자
로컬↔로컬 폴백 가용성↑ 지연 증가 가능 로컬 엔진 불안정 환경

⚖️ 모순 및 업데이트 (Contradictions & updates)

  • 하드코딩 모델 목록의 노후화: Anthropic 어댑터의 모델 목록은 작성 시점 기준이라 시간이 지나면 낡는다. 사용자 직접 입력을 허용해 완화하지만, 최신 모델 사용 시 id 를 직접 넣어야 한다.
  • prompt caching/tool use 미구현: 어댑터 주석이 "향후 확장 여지(prompt caching, tool use)"를 명시 — 현재는 단순 streaming 만. 비용 최적화·구조화 호출은 후속 과제.

🛠️ 적용 사례 (Applied in summary)

  • AstraAI/src/features/providers/index.ts — streamCloudCompletion switch dispatch, listAllCloudModels 병렬 [S2].
  • AstraAI/src/features/providers/anthropic.ts — 입력 정규화 + SSE 변환 + 에러 passthrough [S3].
  • AstraAI/src/features/providers/types.ts — parseModelPrefix/makeModelId [S1].
  • AstraAI/src/core/services.ts — 로컬 엔진 폴백 [S4].

💻 코드 패턴 (Code patterns)

// 1) prefix 라우팅 (src/features/providers/types.ts)
export function parseModelPrefix(id: string): { provider: ProviderId; model: string } | null {
    for (const { prefix, id: pid } of PROVIDER_PREFIXES)
        if (id.startsWith(prefix)) return { provider: pid, model: id.slice(prefix.length) };
    return null;   // null = 로컬 엔진 경로
}

// 2) switch dispatch — 공급자 추가는 case 하나 (src/features/providers/index.ts)
switch (hit.provider) {
    case 'openrouter': return streamOpenRouter(context, fullParams);
    case 'anthropic':  return streamAnthropic(context, fullParams);
    case 'gemini':     return streamGemini(context, fullParams);
}

// 3) 입력 정규화로 공급자 제약 흡수 (src/features/providers/anthropic.ts)
for (const m of params.messages) {
    if (m.role === 'system') systemPrompt += (systemPrompt ? '\n\n' : '') + m.content; // top-level 분리
    else { const last = messages.at(-1);
        if (last?.role === m.role) last.content += '\n\n' + m.content;                  // 같은 role 병합
        else messages.push({ role: m.role, content: m.content }); }
}
if (messages[0]?.role !== 'user') messages.unshift({ role: 'user', content: '(continue)' }); // 첫 user 강제

// 4) 응답 스트림을 공통 SSE 로 정규화
const transformed = transformAnthropicStream(upstream.body);
return new Response(transformed, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });

검증 상태 및 신뢰도

  • 상태: draft
  • 검증 단계: applied
  • 출처 신뢰도: A
  • 신뢰 점수: 0.92
  • 중복 검사 결과: 신규 생성 (New discovery)

🔗 지식 그래프 (Knowledge Graph)

📚 출처 (Sources)

  • [S1] AstraAI/src/features/providers/types.ts — prefix 라우팅, makeModelId
  • [S2] AstraAI/src/features/providers/index.ts — switch dispatch, 병렬 모델 조회
  • [S3] AstraAI/src/features/providers/anthropic.ts — 입력 정규화, SSE 변환, 에러 passthrough
  • [S4] AstraAI/src/core/services.ts — 로컬 엔진 폴백

📝 변경 이력 (Change history)

  • 2026-06-13: AstraAI 코드 분석 기반 초안 생성.