ZeroClaw — Provider 구현체
Layer 1 학습. 실제 소스 코드 기반. 파일:
src/providers/compatible.rs,src/providers/reliable.rs
전체 구조
Provider trait 위에 두 개의 핵심 구현체가 있다.
Box<dyn Provider>
├── OpenAiCompatibleProvider ← 실제 LLM API 호출 (compatible.rs)
└── ReliableProvider ← 안정성 래퍼 (reliable.rs)
내부에 OpenAiCompatibleProvider를 품고 있음
agent loop는 항상 ReliableProvider를 통해 LLM을 호출한다.
OpenAiCompatibleProvider — 실제 HTTP 클라이언트
핵심 아이디어
OpenAI /v1/chat/completions 형식을 따르는 모든 API를 단일 구현으로 처리한다. llama.cpp, Ollama, Claude, Qwen, Groq, Mistral 등 22개 이상의 provider가 이 하나의 struct로 연결된다.
// 생성 예시
let llama_local = OpenAiCompatibleProvider::new(
"llama.cpp",
"http://localhost:8080/v1", // 내 서버
None, // API key 없음
AuthStyle::Bearer,
);
let claude = OpenAiCompatibleProvider::new(
"anthropic",
"https://api.anthropic.com/v1",
Some("sk-ant-..."),
AuthStyle::Bearer,
);
AuthStyle — 인증 방식 추상화
pub enum AuthStyle {
Bearer, // Authorization: Bearer <key> → 대부분
XApiKey, // x-api-key: <key> → 일부 중국 provider
Custom(String), // 커스텀 헤더명 → 특수 provider
}
native_tool_calling 플래그
가장 중요한 필드. tool call 방식을 결정한다.
native_tool_calling = true
→ tools 필드를 API 요청 JSON에 포함
→ LLM이 API 레벨에서 tool_calls 반환
→ 파싱: response.choices[0].message.tool_calls
native_tool_calling = false (로컬 LLM 기본값)
→ system prompt 안에 도구 목록을 텍스트로 주입
→ LLM이 <tool_call>{"name":...}</tool_call> 형식으로 텍스트 출력
→ 파싱: 텍스트에서 XML 태그 찾아 파싱
로컬 llama.cpp는 기본적으로 false. 명시적으로 without_native_tools()로 끌 수도 있다.
Responses API 폴백
llama.cpp는 /v1/chat/completions 대신 /v1/responses 형식도 지원한다. 404가 오면 자동으로 폴백한다.
// 404 수신 → responses_url()로 재시도
// GET https://localhost:8080/v1/chat/completions → 404
// ↓
// POST https://localhost:8080/v1/responses → 성공
메시지 전처리 — flatten_system_messages
일부 provider(MiniMax 등)는 role: system을 거부한다. 이럴 때 system 메시지 내용을 첫 번째 user 메시지 앞에 붙인다.
// 입력
[system("정책"), user("질문")]
// merge_system_into_user = true 일 때 출력
[user("정책\n\n질문")]
reasoning_content 폴백
DeepSeek-R1, Qwen3 등 thinking 모델은 content가 비어있고 reasoning_content에 결과를 넣는다.
// effective_content() 로직
if content가 비어있으면:
reasoning_content를 반환
else:
content 반환 (<think>...</think> 태그 제거 후)
ReliableProvider — 3단계 failover 래퍼
구조
pub struct ReliableProvider {
providers: Vec<(String, Box<dyn Provider>)>, // 우선순위 순서
max_retries: u32,
base_backoff_ms: u64,
api_keys: Vec<String>, // Rate limit 시 key 순환
model_fallbacks: HashMap<String, Vec<String>>, // 모델 폴백 체인
}
3중 중첩 루프 — failover 전략
for current_model in model_chain: // 외부: 모델 폴백
for provider in providers: // 중간: provider 우선순위
for attempt in 0..max_retries: // 내부: 재시도 + 지수 백오프
match provider.chat(...):
Ok(resp) → 즉시 반환 ✅
Err(e) →
non_retryable? → break (다음 provider로)
retryable? → sleep + retry
에러 분류
// non_retryable (즉시 다음 provider로)
- 4xx 에러 (400, 401, 403, 404) → 재시도해도 의미 없음
- "invalid api key" 류 텍스트
- "model not found" 류 텍스트
- 단, 429(rate limit), 408(timeout)는 retryable
// retryable (백오프 후 재시도)
- 5xx 에러 (500, 502, 503)
- 네트워크 타임아웃
- 429 rate limit (단, plan 제한은 non-retryable)
// 특수: context_window_exceeded
- 히스토리 잘라서 재시도 (truncate_for_context)
- 자를 게 없으면 즉시 실패
지수 백오프
backoff_ms = base_backoff_ms // 초기값 (기본 50ms)
재시도마다: backoff_ms *= 2 // 지수 증가
최대: 10,000ms (10초)
// Retry-After 헤더가 있으면 그 값을 우선 (최대 30초)
compute_backoff(base, err) → Retry-After가 있으면 그 값, 없으면 base
모델 폴백 체인
// 설정
model_fallbacks: {
"claude-opus": ["claude-sonnet", "claude-haiku"]
}
// 호출 순서
1. claude-opus → 실패
2. claude-sonnet → 실패
3. claude-haiku → 성공 ✅
context window 초과 처리
// chat_with_history에서만 동작 (히스토리가 있을 때)
fn truncate_for_context(messages) → dropped_count {
non_system 메시지의 절반을 앞에서 제거
system 메시지와 마지막 user 메시지는 보존
}
실제 agent가 provider를 사용하는 방식
// 설정 파일에서 생성 (create_resilient_provider)
let provider = ReliableProvider::new(
vec![
("local".into(), Box::new(
OpenAiCompatibleProvider::new("llama.cpp", "http://localhost:8080/v1", None, AuthStyle::Bearer)
)),
("claude".into(), Box::new(
OpenAiCompatibleProvider::new("anthropic", "https://api.anthropic.com/v1", Some(&api_key), AuthStyle::Bearer)
)),
],
3, // max_retries
500, // base_backoff_ms
)
.with_model_fallbacks(fallbacks);
// agent loop에서 호출
let response = provider.chat(ChatRequest { messages, tools }, model, temperature).await?;
내 프로젝트 적용
로컬 llama.cpp 연결
let local = OpenAiCompatibleProvider::new(
"qwen-local",
"http://localhost:8080/v1",
None,
AuthStyle::Bearer,
)
.without_native_tools() // 로컬 LLM은 prompt-guided 방식
.with_timeout_secs(60); // 로컬이라 여유 있게
Claude API 폴백
let reliable = ReliableProvider::new(
vec![
("local".into(), Box::new(local)), // 1순위: 로컬
("claude".into(), Box::new(claude)), // 2순위: Claude API
],
3, // 3회 재시도
200,
)
.with_model_fallbacks({
let mut m = HashMap::new();
m.insert("qwen3-7b".into(), vec!["claude-sonnet-4-5".into()]);
m
});
이렇게 하면 로컬 LLM 실패 시 자동으로 Claude API로 폴백하고, Claude도 실패하면 재시도한다.
핵심 요약
| 컴포넌트 | 역할 |
|---------|------|
| OpenAiCompatibleProvider | /v1/chat/completions HTTP 호출. native vs prompt-guided 도구 처리 |
| AuthStyle | Bearer / XApiKey / Custom 인증 방식 추상화 |
| ReliableProvider | 3중 루프 failover: 모델 체인 → provider 우선순위 → 재시도 |
| is_non_retryable() | 4xx 에러 분류 — 즉시 다음 provider로 |
| truncate_for_context() | context 초과 시 히스토리 절반 제거 후 재시도 |
다음: Agent Loop
src/agent/loop_.rs— provider + tool + memory가 어떻게 연결되는지run_tool_call_loop()— tool call 파싱과 실행
관련
- trait-system — Provider trait 원형
- overview — ZeroClaw 학습 지도
Config → ReliableProvider 연결 (providers/mod.rs)
호출 경로
Config::load_or_init()
→ create_resilient_provider(primary, api_key, api_url, &reliability)
→ create_provider_with_url_and_options(primary, key, url, options)
→ Box<dyn Provider> (AnthropicProvider / OpenAiCompatibleProvider / ...)
→ fallback_providers 순회
→ create_provider_with_options(fallback, None, options)
→ invalid provider는 warn 후 무시
→ ReliableProvider::new(providers, retries, backoff)
.with_api_keys(...)
.with_model_fallbacks(...)
핵심: fallback은 key를 상속받지 않는다
// fallback에는 None 전달 → 각 provider가 자기 전용 env var 사용
create_provider_with_options(provider_name, None, &fallback_options)
primary api_key = "zai-key"여도 fallback "deepseek"은 DEEPSEEK_API_KEY에서 가져옴.
llama.cpp 연결
"llamacpp" | "llama.cpp" => {
let base_url = api_url.unwrap_or("http://localhost:8080/v1");
OpenAiCompatibleProvider::new_with_vision("llama.cpp", base_url, key, ...)
}
default_provider = "llamacpp" + api_url = "http://localhost:8080/v1" 으로 바로 연결.
custom: 접두사로 임의 endpoint 추가
[reliability]
fallback_providers = ["anthropic", "custom:http://localhost:8080/v1"]
내 프로젝트 최소 설정
api_key = "sk-ant-..."
default_provider = "anthropic"
default_model = "claude-sonnet-4-6"
[reliability]
provider_retries = 3
provider_backoff_ms = 500
fallback_providers = ["llamacpp"]
[reliability.model_fallbacks]
"claude-sonnet-4-6" = ["claude-haiku-4-5"]