← /geode/portfolioGEODE . 문서
GitHub
가이드
How-to

LLM 어댑터 추가

라우터와 어댑터 레이어에 프로바이더를 추가하고 폴백 항목을 거는 방법입니다.

어댑터는 하나의 (provider, source) 조합을 실제 호출로 바꾸는 계층입니다. PAYG API 키 호출이든, OAuth 구독 호출이든, 로컬 CLI 서브프로세스든 전부 같은 프로토콜을 따릅니다. 새 백엔드를 붙이는 작업은 어댑터 작성, 레지스트리 등록, 폴백 체인 편입 세 단계입니다.

1. 어댑터를 작성합니다

어댑터는 core/llm/adapters/base.py LLMAdapter 프로토콜을 만족하면 됩니다. 최소 요건은 네 정체성 속성(name, provider, source, billing_type)과 비동기 호출 메서드 acomplete()입니다. source CONCRETE_SOURCES(payg / subscription / adapter) 중 하나여야 하고, "auto"는 picker 전용 sentinel이라 어댑터에 박을 수 없습니다. 요청·응답 셰이핑은 프로토콜이 정의한 provider-agnostic 타입(AdapterCallRequest, AdapterCallResult)을 어댑터 내부에서 SDK 페이로드로 번역하는 일입니다. AnthropicPaygAdapter(core/llm/adapters/anthropic_payg.py)가 PAYG 경로의 참조 구현입니다.

# core/llm/adapters/acme_payg.py
from dataclasses import dataclass, field
from typing import Any
from core.llm.adapters.base import (
    SOURCE_PAYG, AdapterBillingType,
    AdapterCallRequest, AdapterCallResult,
    EnvironmentReport, UsageSummary,
)

@dataclass
class AcmePaygAdapter:
    name: str = "acme-payg"
    provider: str = "acme"
    source: str = SOURCE_PAYG
    billing_type: AdapterBillingType = AdapterBillingType.API
    _client: Any = field(default=None, init=False, repr=False)

    async def acomplete(self, req: AdapterCallRequest) -> AdapterCallResult:
        client = self._get_client()
        raw = await client.create(...)  # translate req -> SDK payload
        return AdapterCallResult(
            text=raw.text,
            usage=UsageSummary(input_tokens=..., output_tokens=...),
            stop_reason=raw.stop_reason,
        )

    def test_environment(self) -> EnvironmentReport:
        from core.config import settings
        if not settings.acme_api_key:
            return EnvironmentReport(ok=False, hints=("set ACME_API_KEY",))
        return EnvironmentReport(ok=True)

스트리밍·introspection 메서드(astream, list_models, get_quota_windows, detect_credential)는 프로토콜이 요구하지만, 해당 표면을 지원하지 않으면 빈 값이나 None을 돌려줘도 됩니다. 단, test_environment는 항상 정직해야 합니다.

2. 레지스트리에 등록합니다

어댑터는 프로세스 전역 core/llm/adapters/registry.py 레지스트리로 조회됩니다. 내장 어댑터는 bootstrap_builtins()에서 등록되므로, 새 내장 어댑터라면 그 함수의 클래스 튜플에 추가합니다. 외부 플러그인이면 진입점에서 register_adapter(AcmePaygAdapter())를 직접 호출합니다. resolve_for(provider, source) (provider, source) 쌍이 정확히 하나의 어댑터에 매칭되도록 강제하므로, 같은 쌍을 둘 등록하면 invariant 위반으로 loud하게 실패합니다.

# core/llm/adapters/registry.py — bootstrap_builtins()
from core.llm.adapters.acme_payg import AcmePaygAdapter

for adapter_cls in (..., AcmePaygAdapter):
    instance = adapter_cls()
    if instance.name in _REGISTRY:
        continue
    register_adapter(instance)

서브프로세스(워커·audit)는 부모의 wiring 컨테이너를 거치지 않으므로 bootstrap_builtins()를 명시 호출해야 합니다. 안 그러면 레지스트리가 비어 AdapterNotFoundError가 납니다.

3. 라우팅과 폴백 체인을 연결합니다

core/llm/router/calls/_route.py _route_provider(model)이 모델 이름을 프로바이더로 해석합니다. 등록된 Plan이 있으면 그것을 따르고, 없으면 core.config의 정적 _resolve_provider로 떨어집니다. 새 프로바이더가 모델 이름으로 해석되도록 그 매핑을 확인하세요. 다중 모델 폴백은 core/llm/router/calls/_failover.py call_with_failover(models, call_fn)이 처리합니다. 모델 체인을 순서대로 시도하며, 재시도 가능한 오류(rate-limit, timeout, connection, server)는 백오프 후 다음 모델로 넘어가고, 인증 오류 같은 비재시도 오류는 즉시 전파됩니다. 새 어댑터의 모델을 체인에 넣으면 다른 모델이 소진됐을 때 폴백 후보가 됩니다.

확인

(provider, source) 쌍이 정확히 어댑터로 해석되는지 확인합니다.

uv run python -c "
from core.llm.adapters.registry import bootstrap_builtins, resolve_for
bootstrap_builtins()
a = resolve_for('acme', 'payg')
print(a.name, a.provider, a.source)
print(a.test_environment().ok)
"

어댑터 이름이 출력되면 라우팅이 그 쌍을 찾을 수 있습니다. test_environment().ok는 자격증명 상태를 정직하게 보고합니다.

참조: Providers, Pick a path.