e2c5471046
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.1 KiB
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 |
|
A | 0.92 | 2026-06-13 | 2026-06-13 |
|
|
|
LLM 프로바이더 추상화
🎯 한 줄 통찰 (One-line insight)
여러 LLM 공급자(로컬 LM Studio/Ollama, 클라우드 OpenRouter/Anthropic/Gemini)를 한 코드에서 쓰려면 "model id prefix 로 라우팅 + 공급자별 어댑터가 차이를 흡수 + 모두 같은 SSE 포맷으로 정규화"가 핵심이며, AstraAI 는 이 어댑터 패턴으로 호출부를 공급자 무관하게 유지한다 [S1][S2].
🧠 핵심 개념 (Core concepts)
- Prefix 라우팅:
anthropic:claude-...,gemini:...,openrouter:...처럼 model id 의 접두사로 공급자를 결정. 접두사 없으면 로컬 엔진 [S1]. - 어댑터 패턴: 공급자마다
streamX(context, params)함수가 그 API 의 차이(인증·바디·스트림 형식)를 흡수하고 표준 인터페이스(StreamParams)를 받는다 [S2][S3]. - 출력 정규화(SSE): 각 어댑터가 응답 스트림을 OpenAI 호환 SSE 로 변환해 반환 → 기존 SSE 파서 하나로 모두 소비 [S2][S3].
- 로컬 엔진 폴백: 로컬은 LM Studio↔Ollama 간 자동 폴백(전송 오류/5xx/빈 응답 시) [S4].
- 에러 응답 passthrough: 인증 실패·4xx·5xx 는
.ok=falseResponse 로 그대로 반환, 호출부가.text()로 메시지 추출 [S2][S3].
🧩 추출된 패턴 (Extracted patterns)
- 양방향 prefix 변환:
parseModelPrefix(id)(분해) ↔makeModelId(provider, model)(조립) — UI/config 저장과 라우팅이 1:1 [S1]. - switch dispatch:
streamCloudCompletion이switch (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)
- 상위/루트: AstraAI 아키텍처 개요
- 관련 개념: 비동기 프로그래밍 Promise async await, 의존성 주입과 서비스 인터페이스, Agent 오케스트레이터 분해
- 참조 맥락: 로컬 LLM 이 다중 LLM 공급자/외부 API 를 어댑터로 통합하는 코드를 작성할 때 참조.
📚 출처 (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 코드 분석 기반 초안 생성.