어댑터는 하나의 (provider, source) 조합을 실제 호출로 바꾸는 계층입니다. PAYG API 키 호출이든, OAuth 구독 호출이든, 설치된 외부 어댑터든 전부 같은 프로토콜을 따릅니다. 새 백엔드를 붙이는 작업은 어댑터 작성, 레지스트리 등록, 라우팅 연결, 호출 계약 문서화의 네 단계입니다.
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,
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,
)스트리밍과 introspection은 필수가 아닙니다. 지원하는 표면만 StreamingCapable, EnvironmentDiagnosticCapable, ModelListingCapable, QuotaInspectionCapable, CredentialDetectionCapable 구조를 만족시키면 됩니다. 지원하지 않는 메서드를 빈 값이나 None stub으로 만들지 마십시오.
2. 레지스트리에 등록합니다
어댑터는 core/llm/adapters/registry.py가 발행한 불변 generation snapshot으로 조회됩니다. 내장 어댑터는 명시적 factory 목록에서 생성합니다. 외부 패키지는 전역 dict를 직접 수정하지 않고 geode.llm_adapters 패키지 진입점에 factory를 선언합니다. 진입점 이름은 factory가 반환한 canonical adapter name과 같아야 합니다. resolve_for(provider, source)는 (provider, source) 쌍이 정확히 하나의 어댑터에 매칭되도록 강제하므로, 같은 쌍을 둘 등록하면 invariant 위반으로 곧바로 실패합니다.
# acme-geode-adapter/pyproject.toml
[project.entry-points."geode.llm_adapters"]
acme-payg = "acme_geode:create_adapter"
# acme_geode/__init__.py
def create_adapter():
return AcmePaygAdapter()진입점 이름과 배포 패키지 metadata는 실행 없이 먼저 열거됩니다. GEODE는 이름 충돌을 해결한 다음확장 신뢰 정책의llm-adapter:acme-payg 승인을 확인하고 나서야entry_point.load()를 호출합니다. 승인이 없으면 validation report에 REJECTED로 남고 factory는 import되지 않습니다. factory는 기존처럼 인자 없이 만들거나, 정확히 하나의context 인자를 받아 불변 확장 ID와 승인된 포트를 확인할 수 있습니다. 다른 signature는 session 시작 전에 실패합니다.
기존 factory는 보수적인 호환 composition을 자동으로 얻습니다. 실제 인증 선택과 API shape를 선언하려면 반환 객체에 불변ProviderSpec을 추가하십시오. 이 값은ProviderProfile, CredentialRoute,TransportSpec으로 나뉘며 secret이나 SDK client를 담지 않습니다. 선언한 provider/source/billing/capability가 adapter 호환 속성과 다르면 session 시작 전에 등록이 실패합니다.
# acme_geode/__init__.py
from core.llm.registry import (
AdapterBillingType, CredentialRoute, ProviderProfile,
ProviderSpec, TransportSpec,
)
ACME_SPEC = ProviderSpec(
profile=ProviderProfile("acme", "acme", "Acme", "acme"),
credential=CredentialRoute(
source="payg", account_provider="acme", selector="plugin",
auth_type="bearer", billing_type=AdapterBillingType.API,
),
transport=TransportSpec(
id="acme-responses", api="acme-responses",
default_base_url="https://api.acme.example/v1",
),
)
class AcmeComposedAdapter(AcmePaygAdapter):
provider_spec = ACME_SPEC
def create_adapter():
return AcmeComposedAdapter()서브프로세스(워커·audit)는 부모의 wiring 컨테이너를 거치지 않으므로 bootstrap_builtins()를 명시 호출해야 합니다. 안 그러면 레지스트리가 비어 AdapterNotFoundError가 납니다. 이 호출은 내장 factory와 지원 진입점을 함께 검색하며, generation과 validation report가 붙은 snapshot을 반환합니다. 새 세션은 현재 snapshot을 캡처하고, 이미 실행 중인 세션은 reload 뒤에도 기존 generation을 유지합니다. canonical ID 충돌은 기본적으로 실패합니다. 의도적인 교체만 AdapterOverride로 승자 origin, priority, trust decision을 명시해 reload_adapters()에 전달합니다.
3. 라우팅과 폴백 체인을 연결합니다
core.config._resolve_provider(model)이 모델 이름을 프로바이더로 해석하고, adapter dispatch가 credential metadata로 source를 결정합니다. 등록된 Plan은 별도의 routing target으로 endpoint와 credential을 선택합니다. 모델 접두사와 프로바이더의 매핑은 core/config/routing.toml의 [routing.prefixes]가 SoT이고, 사용자 override는 ~/.geode/routing.toml입니다. 새 프로바이더의 모델 접두사를 여기에 추가합니다. 다중 모델 폴백은 core/llm/router/calls/_failover.py의 call_with_failover(models, call_fn)이 처리합니다. 모델 체인을 순서대로 시도하며, 재시도 가능한 오류(rate-limit, timeout, connection, server)는 백오프 후 다음 모델로 넘어가고, 인증 오류 같은 비재시도 오류는 즉시 전파됩니다. 단, 폴백 체인은 기본 출하값이 전부 빈 리스트입니다([model.fallbacks]). 기본 경로는 실패를 그대로 드러냅니다. 새 어댑터의 모델을 폴백 후보로 쓰려면 ~/.geode/routing.toml에서 직접 체인을 켜야 합니다.
4. 호출 계약을 문서화합니다
adapter가 등록됐다는 사실과 agentic 기능이 보장된다는 주장은 다릅니다. 새 경로의 실제 request builder를 확인한 뒤 도구 호출과 구조화 출력 표에 provider/source/adapter 경계를 추가합니다.
| 항목 | 기록할 내용 |
|---|---|
| 도구 호출 | ToolSpec encoding, tool_choice 변환, 복수 호출, call id와 result replay |
| 구조화 출력 | response_schema wire field, strict 판정, local validation과 retry 범위 |
| 미지원 경계 | 필드를 무시하는 경로와 모델별 확인이 필요한 부분을 지원으로 뭉개지 않고 명시 |
| 근거 | 공식 provider 문서 또는 source, local request builder, request-shape test, 남은 live test |
SDK type에 필드가 있다는 사실만으로 지원을 선언하지 않습니다. adapter가 값을 실제 wire payload에 싣는지와, GEODE가 결과를 어떻게 정규화·검증하는지를 함께 적습니다.
5. 확인합니다
(provider, source) 쌍이 정확히 어댑터로 해석되는지 확인합니다.
uv run python -c "
from core.llm.adapters.registry import bootstrap_builtins
from core.llm.adapters import EnvironmentDiagnosticCapable
snapshot = bootstrap_builtins()
a = snapshot.resolve_for('acme', 'payg')
print(snapshot.generation, snapshot.report.origins)
print(a.name, a.provider, a.source)
if isinstance(a, EnvironmentDiagnosticCapable):
print(a.test_environment().ok)
"어댑터 이름이 출력되면 라우팅이 그 쌍을 찾을 수 있습니다. 환경 진단 capability를 구현했다면 test_environment().ok도 자격증명 상태를 정직하게 보고합니다.
참조: Providers, Tool calling, Structured output, Pick a path.