BoxAgnts 소개 (7) — OpenAI API 및 Anthropic API
요약
BoxAgnts가 다양한 AI 모델 제공자의 API 형식을 추상화하여 단일 인터페이스로 통합하는 설계 방식을 소개합니다. OpenAI, Anthropic, Google Gemini 간의 메시지 형식 및 시스템 프롬프트 차이를 분석하고 이를 정규화하는 엔지니어링 기법을 다룹니다.
핵심 포인트
- LlmProvider 트레이트를 통한 제공자 중립적 인터페이스 설계
- ProviderRequest/Response를 활용한 데이터 정규화 계층 구현
- Anthropic, OpenAI, Gemini 간의 시스템 프롬프트 및 도구 정의 차이 분석
- 단일 파라미터 변경만으로 모델을 전환할 수 있는 추상화 구조
2025년 AI 모델 시장은 만개하고 있습니다. 하지만 각 제공업체(Provider)는 저마다의 API 형식, 인증 방식, 스트리밍 프로토콜을 가지고 있습니다. BoxAgnts의 설계 목표는 다음과 같습니다: 사용자가 단 하나의 파라미터만 변경함으로써 내부 로직의 변경 없이 모델을 전환하는 것입니다.
이 글에서는 네 가지 수준에 걸쳐 이러한 추상화(Abstraction)를 분석합니다:
- 통합 인터페이스 (Unified Interface):
LlmProvider트레이트(Trait)가 "모델 제공자"를 정의하는 방식 - 3대 주요 API 형식 비교: Anthropic, OpenAI, Google Gemini 간의 형식 차이
- 형식 변환 (Format Conversion): 완전히 다른 세 가지 메시지 형식 간의 변환 방법
- 엔지니어링 실무 (Engineering Practices): 설정, 에러 핸들링(Error handling), ProviderQuirks, API 키 관리 등
통합 인터페이스: LlmProvider 트레이트
모든 것은 인터페이스 정의에서 시작됩니다:
// boxagnts-api/src/provider.rs
#[async_trait]
pub trait LlmProvider: Send + Sync {
...
입력과 출력 모두 제공자 중립적인(Provider-agnostic) 통합 타입을 사용합니다:
pub struct ProviderRequest {
pub model: String,
pub messages: Vec<Message>, // 통합 대화 형식
...
정규화 계층(Normalization layer)의 핵심 가치: 기저에 있는 모델이 Claude, GPT, 또는 Gemini인지에 관계없이, 상위 계층의 코드는 오직 ProviderRequest와 ProviderResponse만을 바라봅니다.
ProviderRegistry: 40개 이상의 모델을 위한 통합 엔트리
// boxagnts-api/src/registry.rs
pub struct ProviderRegistry {
providers: HashMap<ProviderId, Arc<dyn LlmProvider>>,
...
세 가지 구현 전략:
| 유형 | 대표 사례 | 변환 전략 | 개수 |
|---|---|---|---|
| Native Anthropic | claude-sonnet-4-5 | 변환 거의 없음 (내부 형식 = Anthropic 형식) | 1 |
| ... |
3대 주요 API 형식 간의 차이점
Anthropic, OpenAI, Google Gemini — 메시지 형식에서 방대한 차이를 보이는 세 가지 API입니다. 이러한 차이를 이해하는 것은 변환 계층(Conversion layer)의 가치를 이해하는 데 필수적입니다.
3.1 시스템 프롬프트 (System Prompt)
| 기능 | Anthropic | OpenAI | Google Gemini |
|---|---|---|---|
| 위치 | 최상위 "system" 필드 | messages[0], role:"system" | 최상위 "systemInstruction" 필드 |
| 유형 | 문자열(string) 또는 ContentBlock 배열 | 문자열(string)만 가능 | content parts 배열만 가능 |
// Anthropic — 최상위 독립 필드
{"model": "claude-sonnet-4-5", "system": "You are helpful.", "messages": [...]}
...
3.2 도구 정의 (Tool Definitions)
| 기능 | Anthropic | OpenAI | |
|---|---|---|---|
| 필드 | "tools": [{name, description, input_schema}] | "tools": [{type:"function", function:{...}}] | "tools": [{functionDeclarations: [{name, description, parameters}]}] |
| 래핑 계층 (Wrapping Layers) | 0 | 1 | 1, 서로 다른 중첩 이름 사용 |
3.3 도구 호출 응답 (Tool Call Responses)
// Anthropic — content 배열 내의 네이티브 블록
{"content": [{"type":"tool_use", "id":"toolu_01A", "name":"read", "input": {...}}]}
...
3.4 도구 결과 형식 (Tool Result Format)
// Anthropic — tool_result는 user 메시지 content 배열 내의 블록임
{"role":"user", "content": [{"type":"tool_result", "tool_use_id":"toolu_01A", "content":"..."}]}
...
3.5 역할 명명 (Role Naming)
| Anthropic | OpenAI | |
|---|---|---|
user | user | user |
assistant | assistant | model |
Google은 assistant 대신 model을 사용합니다 — 이는 가장 쉽게 간과하기 쉽지만 가장 오류가 발생하기 쉬운 차이점입니다.
변환 계층 구현: OpenAI Provider 예시
OpenAiProvider는 변환 계층의 가장 완전한 예시입니다:
// boxagnts-api/src/providers/openai.rs
impl OpenAiProvider {
fn to_openai_messages(
...
가장 복잡한 부분은 tool_use_id 정제(sanitization)입니다 — Anthropic의 도구 ID(예: toolu_01Bx...)에는 OpenAI가 허용하지 않는 문자가 포함될 수 있습니다.
Google Gemini Provider: 세 번째 형식의 완전한 적응
GoogleProvider는 Anthropic 및 OpenAI와 모두 다른 API 형식을 처리하는 방법을 보여줍니다:
// boxagnts-api/src/providers/google.rs
// URL 패턴이 OpenAI의 /v1/chat/completions와 완전히 다름
fn generate_url(&self, model: &str) -> String {
...
OpenAI와의 주요 차이점:
| 차이점 | Google Gemini | OpenAI |
|---|---|---|
| API Key 위치 | URL 쿼리 파라미터 ?key= | HTTP 헤더 Authorization: Bearer |
| ... |
사고 설정 (Thinking Configuration): 심층 추론 (Deep Reasoning)에서의 모델 차이
ThinkingConfig는 정규화된 심층 사고 (Deep Thinking) 설정입니다. 하지만 각 제공자(Provider)마다 이를 처리하는 방식이 완전히 다릅니다:
// 정규화된 설정 (Normalized configuration)
pub struct ThinkingConfig {
pub budget_tokens: u32, // 사고 토큰 예산 (Thinking token budget)
...
| 제공자 (Provider) | 사고 지원 여부 (Thinking Support) | 전달 방식 |
|---|---|---|
| Anthropic (Claude 3.5+) | ✓ | "thinking": {"type": "enabled", "budget_tokens": N} |
| ... |
요청 생성 시점에 ProviderCapabilities는 각 제공자의 기능을 선언합니다:
pub struct ProviderCapabilities {
pub thinking: bool, // 심층 사고 (Deep thinking) 지원 여부
pub prompt_caching: bool, // 프롬프트 캐싱 (Prompt caching) 지원 여부
...
ProviderQuirks: 각 제공자의 "사소한 특이점 (Little Quirks)"
OpenAI 호환 제공자들의 API는 대략적으로 호환되지만, 모두 미세한 차이점을 가지고 있습니다. ProviderQuirks는 이러한 차이점을 처리합니다:
pub struct ProviderQuirks {
/// 컨텍스트 오버플로 (Context overflow)에 대한 특정 에러 메시지 패턴
pub overflow_patterns: Vec<String>,
...
예를 들어, DeepSeek의 스트리밍 응답은 OpenAI와 다른 필드 이름으로 추론 내용 (Reasoning content)을 반환하며, 이는 reasoning_field를 통해 조정됩니다. Ollama의 컨텍스트 오버플로 에러 메시지는 "exceeds the available context size"인 반면, LM Studio는 "greater than the context length"이며, 이는 overflow_patterns를 통해 조정됩니다.
통합 스트리밍 처리 (Unified Streaming Processing)
스트리밍 응답 또한 세 가지 API 간에 완전히 다릅니다:
| 기능 | Anthropic (SSE) | OpenAI (SSE) | Google (SSE) |
|---|---|---|---|
| 이벤트 세분성 (Event Granularity) | 높음: 6가지 이벤트 유형 (start/delta/stop × 2) | 낮음: 각 청크(chunk)가 하나의 완전한 델타(delta)임 | 중간: 청크 단위로 푸시되지만 구조가 평면적임 |
| ... |
세 가지 형식 모두 동일한 StreamEvent 열거형 (enum)으로 정규화됩니다:
pub enum StreamEvent {
MessageStart { id, model, usage },
ContentBlockStart { index, content_block },
...
에러 처리 (Error Handling): 제공자 간의 차이에서 통합된 의미론으로
각 제공자의 에러 형식 또한 서로 다릅니다:
// 통합된 에러 유형 (Unified error types)
pub enum ProviderError {
Auth { ... }, // 인증 실패 (Authentication failure)
...
쿼리 루프 (query loop) 내에서 특정 에러는 특정 복구 전략을 트리거합니다:
RateLimited / Overloaded → fallback_model로 전환
ContextOverflow → auto_compact 트리거
StreamError (stall) → 재시도 (최대 2회, 45초 타임아웃)
...
계층적 API 키 관리 (Tiered API Key Management)
BoxAgnts는 각 제공자에 대한 환경 변수 이름 매핑을 정의합니다:
// boxagnts-workspace/src/config.rs
pub fn api_key_env_vars_for_provider(provider_id: &str) -> &'static [&'static str] {
match provider_id {
...
3단계 우선순위: 환경 변수 (Environment Variables) > 사용자 설정 JSON (User Config JSON) > 기본값 없음 (No Default). 이 설계는 멀티 테넌시 (multi-tenancy), CI/CD, 로컬 개발과 같은 다양한 시나리오를 지원합니다.
요약
BoxAgnts의 모델 추상화 계층은 "하나의 코드 세트로 모든 API에 적응해야 하는" 핵심적인 문제를 해결합니다:
┌──────────────────────────────────────────────┐
│ boxagnts-query (에이전트 추론 루프) │
│ ProviderRequest / ProviderResponse만 사용 │
...
세 가지 핵심 역량:
- 사용자 자유도:
--model파라미터만 변경하여 모델을 전환할 수 있음 - 코드 영향 없음:
run_query_loop()는 내부에서 무엇이 작동하는지 알지 못함 - 극도로 낮은 확장 비용: 새로운 OpenAI 호환 제공자를 추가하는 데 약 3줄의 코드만 필요함
이것은 단순한 '어댑터 패턴(adapter pattern)'이 아닙니다. 40개 이상의 실제 API를 통해 검증된 프로덕션급 추상화 계층입니다.
관련 자료 (Related Resources)
- Boxagnts: https://github.com/guyoung/boxagnts
- Anthropic API: https://docs.anthropic.com/en/api/messages
- OpenAI API: https://platform.openai.com/docs/api-reference/chat
- Google Gemini API: https://ai.google.dev/gemini-api/docs
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기