Open Agent SDK (Rust)
OpenAI chat completions 또는 Anthropic messages를 통해 Rust에서 AI 에이전트를 구축하세요.
구축 가능한 것들:
- 원고를 분석하고 작문 패턴을 추적하는 카피 에디터(Copy editors)
- 의미 있는 커밋 메시지를 작성하는 Git 커밋 생성기(Git commit generators)
- 경쟁사를 조사하고 분석 결과를 요약하는 시장 분석기(Market analyzers)
- 코드 리뷰어, 데이터 분석가, 리서치 어시스턴트 등
이 SDK를 사용해야 하는 이유?
- 두 가지 프로토콜: 엔드포인트별로 OpenAI chat completions 또는 Anthropic messages 중 선택 가능한 두 개의 API.
- 로컬 또는 호스팅: API 비용 없이 데이터를 기기 외부로 전송하지 않고 자체 하드웨어에서 실행하거나, 특정 공급업체(vendor)를 지정할 수 있습니다.
- 제어력: 원하는 모델(Qwen, Llama, Mistral, Claude 등)을 선택할 수 있습니다.
Crates.io
Documentation
License: MIT
개요 (Overview)
Open Agent SDK (Rust)는 두 가지 전송 프로토콜, 즉 OpenAI chat completions과 Anthropic messages를 통해 로컬 및 클라우드 모델 서버와 작업할 수 있는 깔끔하고 스트리밍 가능한 API를 제공합니다. 이 프로토콜은 엔드포인트별로 선택됩니다. 또한 SSE 스트리밍을 전송 경계(transport boundaries)에 걸쳐 지원하며, 도구 호출 집계(tool call aggregation), 라이프사이클 훅(lifecycle hooks), 자동 도구 실행 등을 지원합니다.
스트리밍은 실제 환경의 서버에서도 허용됩니다. SSE 이벤트는 임의의 HTTP 전송 청크 경계를 넘어 버퍼링되며, 스트림이 끝날 때까지 보유된 모든 내용은 플러시됩니다. 여기에는 llama.cpp, vLLM 및 여러 로컬 게이트웨이가 finish_reason을 설정하지 않고 연결을 닫거나 data: [DONE]을 전송하는 경우도 포함됩니다. 콘텐츠가 조용히 손실되는 일은 없습니다.
모든 스트림은 종료 이유를 보고합니다. query()에서 발생하는 스트림은 항상 하나의 StreamEvent::Finish와 함께 FinishReason을 전달하며, 따라서 토큰 제한(Length)으로 잘린 응답과 모델이 스스로 종료하기로 선택한 경우(Stop), 그리고 서버가 아무런 메시지를 보내지 않은 경우(Unspecified)는 내용만으로는 구별할 수 없습니다. 클라이언트에서는 client.finish_reason()을 통해 동일한 정보를 얻을 수 있습니다.
지원되는 제공업체 (Supported Providers)
프로토콜은 엔드포인트의 속성(property)이며, .protocol(..)으로 설정하고 기본값은 ApiProtocol::OpenAiChat입니다.
ApiProtocol::OpenAiChat — POST {base_url}/chat/completions, bearer auth
- LM Studio -
http://localhost:1234/v1
- Ollama -
http://localhost:11434/v1
- llama.cpp server - OpenAI 호환 모드
- vLLM - OpenAI 호환 API
- Text Generation WebUI - OpenAI 확장 기능
- 모든 OpenAI 호환 로컬 엔드포인트
- 클라우드 공급업체 - OpenAI, OpenRouter, Azure OpenAI, z.ai (
https://api.z.ai/api/coding/paas/v4)
- 클라우드 모델을 프록시하는 로컬 게이트웨이 - 예: Ollama 또는 클라우드 공급업체로 라우팅되는 사용자 지정 게이트웨이
LM Studio에 대한 참고: LM Studio는 이 SDK와 특히 잘 테스트되었으며 신뢰할 수 있는 OpenAI 호환 API 지원을 제공합니다. 사용하기 쉬운 로컬 모델 서버를 찾고 있으며 우수한 호환성을 원한다면, LM Studio가 강력히 권장됩니다.
ApiProtocol::Anthropic — POST {base_url}/messages, x-api-key + anthropic-version
- Anthropic
- Moonshot Kimi for Coding -
https://api.kimi.com/coding/v1
- MiniMax -
https://api.minimax.io/anthropic/v1
확장된 사고(Extended thinking)는 기존 추론 채널(StreamEvent::Reasoning, .include_reasoning(true)로 선택적 활성화)을 통해 도착하며, 도구 호출은 일반적인 ContentBlock::ToolUse 블록으로 처리됩니다. 도구 연속 실행(자동 실행 또는 add_tool_result 후 send(""))은 Anthropic의 형식에 맞춰 재현됩니다. Messages API 스키마는 빈 텍스트를 허용하지 않으므로, 빈 텍스트나 내용이 없는 턴은 요청에서 제외되며, 히스토리는 이를 유지합니다.
제3자 Anthropic 호환 엔드포인트들은 통일되어 있지 않으며, SDK가 임의로 기능을 추가하지 않습니다. 예를 들어, api.kimi.com/coding/v1는 max_tokens를 요구하며, 이것이 없으면 invalid_request_error 400을 반환하고, temperature는 허용하지 않지만 1만 받습니다. 따라서 엔드포인트가 요청하는 경우 둘 다 명시적으로 설정해야 합니다.
지원되지 않는 기능 (공식 SDK 사용 권장)
- 클라우드 제공업체 전용 SDK API - Bedrock, Vertex 등 (호환 가능한 게이트웨이를 통해 프록시되는 경우는 가능합니다)
빠른 시작(Quick Start)
설치(Installation)
[dependencies]
open-agent-sdk = "0.11.7"
tokio = { version = "1", features = ["full"] }
...
개발용:
git clone https://github.com/slb350/open-agent-sdk-rust.git
cd open-agent-sdk-rust
cargo build
호출자 헤더는 문자열로 저장되며 query() 및 Client 요청 모두에 적용됩니다. 이들은 동일한 이름의 케이스에 상관없이 SDK 기본값을 대체하며, 제공되지 않은 이름은 프로토콜 기본값을 유지합니다.
OpenRouter 사용 예시:
let options = AgentOptions::builder()
.model(std::env::var("MODEL")?)
.base_url("https://openrouter.ai/api/v1")
...
Azure OpenAI의 api-key 인증은 빈 api_key를 사용하여 SDK bearer 헤더를 억제한 다음, 제공업체별 특정 헤더를 직접 공급합니다:
let options = AgentOptions::builder()
.model(std::env::var("MODEL")?)
.base_url(std::env::var("AZURE_OPENAI_BASE_URL")?)
...
동일한 이름으로 .header()를 다시 호출하면 이전 값이 대체됩니다. 유효하지 않은 이름이나 값은 요청이 전송되기 전에 .build()에 의해 거부됩니다.
커스텀 헤더는 구성된 모델 오리진으로만 전송됩니다. 모델 요청은 동일 출처 리다이렉트를 포함하여 HTTP 리다이렉트를 의도적으로 따르지 않으며, 모든 30x 응답은 일반 API 상태 오류 경로를 통해 노출됩니다. base_url을 요청 및 자격 증명을 받아야 하는 정확한 목적지로 설정하십시오.
0.10.x에서 업그레이드하기
v0.11.0은 추가적인 변경 사항만 포함합니다. 기존 구성은 현재의 프로토콜 헤더를 유지하며, 새로운 .header(name, value) 메서드를 사용하는 호출자는 기본값을 대소문자를 구분하지 않고 대체할 수 있습니다. 유일하게 명시적으로 옵트인하는 인증 동작은 .api_key("")인데, 이는 이제 SDK 인증 헤더를 생략하여 인증이 전적으로 호출자 헤더에서 오도록 합니다.
0.9.x에서 업그레이드하기
v0.10.0에는 하나의 하위 호환성 깨지는 변경 사항(breaking change)이 있으며, 컴파일러가 이를 감지하지 못합니다: 타입은 변하지 않았지만, 동일한 텍스트를 담는 이벤트의 개수만 바뀝니다.
텍스트와 추론 결과가 조각조각 도착합니다. 스트림의 끝에서 하나의 ContentBlock::Text로 도착하던 응답이 이제 스트림이 열려 있는 동안, 각 델타(delta)마다 순서대로 하나의 블록으로 도착합니다. 이것이 바로 SDK가 항상 스트리밍을 의미한다고 주장했던 바입니다. 수신한 내용을 연결하는 코드는 변경할 필요가 없습니다. 첫 번째 텍스트 블록을 전체 답변으로 읽던 코드는 이제 그 접두사(prefix)를 읽게 됩니다:
if let Some(StreamEvent::Block(ContentBlock::Text(text))) = stream.next().await.transpose()? {
println!("{}", text.text);
...
이미 블록들을 수집하고 이전 형태를 되찾고 싶다면, coalesce_text_blocks가 SDK가 내부적으로 적용하는 병합(join) 기능이며, 이제 외부로 노출됩니다:
use open_agent::coalesce_text_blocks;
let whole = coalesce_text_blocks(&collected);
대화 기록은 영향을 받지 않습니다. 프래그먼트(fragments)는 어시스턴트 턴(assistant turn)이 작성되기 전에 합쳐지므로, 다음 요청은 0.9.x가 재현했던 내용을 정확히 재생합니다. 도구 호출(Tool calls)도 영향을 받지 않습니다. 해당 인자들은 마지막 프래그먼트가 도착한 후에만 유효한 JSON이기 때문에, 여전히 스트림의 끝에서 오름차순 인덱스 순서로 온전하게 방출됩니다.
0.8.x에서 업그레이드하기
v0.9.0에는 하나의 하위 호환성 깨지는 변경 사항(breaking change)이 있으며, 컴파일러가 이를 감지합니다.
AgentOptions::temperature()는 Option<f32>를 반환하며, 설정되지 않은 경우(unset)는 아예 설정을 하지 않음을 의미합니다. 이전에는 기본값으로 0.7을 사용했으며 항상 전송되었습니다. 점점 더 많은 모델들이 이 매개변수를 완전히 거부하고 있습니다. Anthropic의 범위는 1.0에서 끝나고, Moonshot의 k3는 400 에러와 함께 이 모델에 대해서는 온도(temperature) 1만 허용됩니다라고 응답합니다. 따라서 클라이언트가 임의로 설정한 기본값이 작동하던 요청을 심각한 오류로 만듭니다. None은 해당 필드를 생략하고 서버가 결정하도록 맡기며, 이는 max_tokens가 0.7.0 이후로 보여왔던 방식과 정확히 같습니다. 빌더는 변경되지 않았습니다: .temperature(0.2)를 호출하면 여전히 값을 설정합니다. 이전 기본값에 의존했던 호출자들은 이제 이를 명시적으로 전달해야 합니다.
Anthropic 메시지 엔드포인트에 도달하는 것은 누적적입니다:
let options = AgentOptions::builder()
.model("k3")
.base_url("https://api.kimi.com/coding/v1")
...
0.7.x에서 업그레이드하기
v0.8.0에는 하나의 하위 호환성 깨지는 변경 사항이 있었고, 컴파일러가 이를 감지했습니다.
query()는 ContentBlock 대신 StreamEvent를 반환합니다. 스트림은 콘텐츠가 아닌 무언가를 위한 공간이 필요했는데, 바로 생성(generation)이 중단된 이유였습니다. 이제 모든 스트림은 정확히 하나의 StreamEvent::Finish로 끝납니다.
while let Some(block) = stream.next().await {
match block? {
...
구조화된 출력을 파싱하는 경우, 업그레이드한 이유가 바로 Finish 이벤트입니다. 이를 무시하기보다는 매치해야 합니다:
use open_agent::{ContentBlock, FinishReason, StreamEvent};
let mut answer = String::new();
...
`ContentStream`이 `EventStream`으로 이름이 변경되었습니다. `ToolCallAggregator`는 `StreamAccumulator`로 이름이 변경되었습니다.
— 만약 이들을 직접 가져와 사용했다면 임포트 구문을 업데이트해야 합니다. API 자체는 변동 없습니다. `ContentBlock`은 변경되지 않았습니다 — 새로운 변형(variant)도 없고 와이어 형태(wire-shape) 변화도 없으므로, 여전히 이를 이용한 완전 탐색 매치(exhaustive matches)와 그 `serde` 표현을 통한 처리가 가능합니다.
**`Client`는 영향을 받지 않습니다.** `client.receive()`는 여전히 `ContentBlock`을 반환하며, 종료 이유는 클라이언트 측에 기록됩니다:
```rust
client.send("Reply with JSON.").await?;
while let Some(block) = client.receive().await? { }
...
0.8.0 버전에서 새로 추가되었으며, 별도의 조치가 필요하지 않습니다: 추론 모델 측 채널(DeepSeek의 reasoning_content, OpenRouter의 reasoning)이 이제 알 수 없는 필드로 버려지는 대신 명시적으로 파싱되어 어시스턴트 텍스트로부터 분리 및 라우팅됩니다. 따라서 숙고 과정의 산문(deliberation prose)이 JSON으로 파싱되는 응답에 끼어드는 일은 절대 없을 것입니다. 이를 확인하려면 .include_reasoning(true) 옵션을 사용하세요. 이 옵션은 해당 내용을 StreamEvent::Reasoning과 client.reasoning()을 통해 노출합니다.
0.6.x 버전에서 업그레이드하기
v0.7.0에는 두 가지 하위 호환성 파괴 변경(breaking changes)이 있습니다. 대부분의 프로젝트는 수정할 필요가 없습니다. 컴파일러가 첫 번째 문제를 감지하며, 두 번째 문제는 컴파일 오류 없이 발생하는 동작 변화입니다.
1. Error::Api에 HTTP 상태 코드가 포함됩니다. 이 필드는 튜플 변형(tuple variant)에서 구조체 변형(struct variant)으로 변경되었으므로, 모든 패턴 매치(pattern match)를 업데이트해야 합니다:
if let Error::Api(msg) = &err { eprintln!("{msg}"); }
...
에러를 생성하는 방식은 변동이 없습니다 — Error::api(msg)는 여전히 작동하며 status: None을 반환합니다. 상태 코드를 가지고 있다면 새로운 Error::api_status(status, msg)를 사용하세요. 왜냐하면 is_retryable_error가 상태 코드를 기준으로 분류하고, 상태가 없는 API 에러는 영구적인 것으로 처리하기 때문입니다.
2. max_tokens의 기본값은 더 이상 4096이 아닙니다. .max_tokens()를 설정하지 않으면 이제 해당 필드가 요청에서 제외되어 서버가 자체 제한을 적용합니다. 이것은 조용한 동작 변화(silent behaviour change)입니다: 만약 암묵적인 상한선에 의존했다면, 명시적으로 설정해야 합니다.
let options = AgentOptions::builder()
.model("qwen2.5-32b-instruct")
.base_url("http://localhost:1234/v1")
...
장문 컨텍스트 및 추론 모델의 경우, 4096 토큰 클라이언트 측 제한으로 인해 응답 중간에 잘리는 것을 방지하기 위해 이 설정을 비워두는 것이 권장됩니다.
또한 0.7.0 버전에서 수정되었으며 별도의 조치가 필요하지 않습니다: 서버가 finish_reason을 전송하지 않고 스트림을 종료할 때 더 이상 스트리밍된 콘텐츠가 폐기되지 않도록(llama.cpp, vLLM 및 여러 로컬 게이트웨이가 이 방식을 사용함), 그리고 429 상태 코드가 이제 재시도 가능한 것으로 올바르게 처리됩니다.
간단한 질의 (LM Studio)
use open_agent::{query, AgentOptions, ContentBlock, FinishReason, StreamEvent};
use futures::StreamExt;
...
다중 턴 대화 (Ollama)
use open_agent::{Client, AgentOptions, ContentBlock};
#[tokio::main]
...
도구 사용을 통한 함수 호출
깔끔하고 타입 안전한 함수 호출을 위해 빌더 패턴을 사용하여 도구를 정의합니다:
use open_agent::{tool, Client, AgentOptions, ContentBlock};
use serde_json::json;
...
고급: 수동 도구 실행
사용자 지정 실행 로직 또는 결과 가로채기를 위해:
let options = AgentOptions::builder()
.system_prompt("당신은 도구에 접근할 수 있는 유용한 어시스턴트입니다.")
...
주요 기능: