Executando verificação de segurança...
1
skel
5 min de leitura ·

Ararajuba: um AI SDK em Rust inspirado no Vercel AI SDK

Na última semana, portei o Vercel AI SDK para Rust e, no processo, redesenhei a interface de providers para ser idiomática em Rust. O resultado é o Ararajuba — um SDK unificado para trabalhar com LLMs (OpenAI, Anthropic, Google, DeepSeek) diretamente em Rust.

Neste post, vou explicar por que fiz isso, o que mudou no processo e como a versão Rust diverge do original em TypeScript.

Por que Rust para AI?

A maioria dos SDKs de IA são em Python ou TypeScript. Mas se você está construindo:

  • Backends de alta performance com Axum/Actix
  • Agentes que rodam em produção com controle fino de memória
  • Ferramentas CLI que precisam de binários pequenos e rápidos
  • Sistemas embarcados ou edge computing

...você provavelmente não quer adicionar um runtime Node.js ou Python só para chamar uma API de LLM.

O Vercel AI SDK tem uma abstração excelente — generate_text, stream_text, generate_object, tools, middleware, streaming — mas é TypeScript-only. O Ararajuba traz essa mesma abstração para Rust.

O que foi portado

A API de alto nível é praticamente idêntica:

use ararajuba::{generate_text, GenerateTextOptions, Prompt};
use ararajuba::openai::openai;

let result = generate_text(GenerateTextOptions {
    model: &openai("gpt-4o"),
    prompt: Prompt::simple("Explique computação quântica em uma frase."),
    ..Default::default()
}).await?;

println!("{}", result.text);

Streaming:

use ararajuba::{stream_text, GenerateTextOptions, Prompt};
use ararajuba::anthropic::anthropic;

let result = stream_text(GenerateTextOptions {
    model: &anthropic("claude-sonnet-4-6"),
    prompt: Prompt::simple("Escreva um poema sobre Rust."),
    ..Default::default()
}).await?;

let mut stream = result.text_stream;
while let Some(chunk) = stream.next().await {
    print!("{}", chunk);
}

Tudo que você espera do Vercel AI SDK está aqui: generate_object, stream_object, embed, embed_many, generate_image, generate_speech, transcribe, rerank, tools com multi-step, middleware, MCP client, UI streaming.

Onde divergimos: a interface v4

Aqui é onde fica interessante. A versão inicial (v3) era uma tradução direta do TypeScript. Funcionava, mas não era idiomática em Rust. Na v4, redesenhamos a interface de providers para aproveitar o sistema de tipos do Rust.

1. Async nativo em vez de BoxFuture

v3 (port do TypeScript):

fn call(&self, options: &CallOptions) -> BoxFuture<'_, Result<GenerateResult, Error>>

v4 (Rust-nativo):

#[async_trait]
pub trait LanguageModelV4: Send + Sync {
    async fn do_generate(&self, options: &CallOptions) -> Result<GenerateResult, Error>;
    async fn do_stream(&self, options: &CallOptions) -> Result<StreamResult, Error>;
}

Sem indireção. O compilador pode inlinar agressivamente.

2. Streams tipados em vez de um enum com 15+ variantes

No TypeScript, o stream retorna eventos misturados em um único canal. Na v4, separamos em 3 streams tipados:

pub struct StreamResult {
    pub content: BoxStream<'static, ContentDelta>,      // texto, reasoning, arquivos
    pub tool_calls: BoxStream<'static, ToolCallDelta>,   // chamadas de ferramentas
    pub metadata: BoxStream<'static, MetadataDelta>,     // usage, finish_reason
    pub abort_handle: AbortHandle,
}

O consumidor assina só o que precisa. O compilador garante type safety.

3. Capability traits em vez de flags runtime

No TypeScript, você checa features com model.supportsReasoning. Em Rust, usamos traits:

pub trait SupportsReasoning: LanguageModelV4 {
    fn reasoning_config(&self) -> ReasoningConfig;
}

pub trait SupportsCaching: LanguageModelV4 {
    fn cache_config(&self) -> CacheConfig;
}

pub trait SupportsToolCalling: LanguageModelV4 {
    fn max_tools(&self) -> Option<usize>;
    fn supports_parallel_calls(&self) -> bool;
}

Se o modelo não implementa a trait, o código nem compila. Zero overhead em runtime.

4. Options tipadas por provider

Em vez de serde_json::Value genérico:

options.set_provider_options("anthropic.chat", &AnthropicOptions {
    thinking: Some(ThinkingConfig {
        type_: "enabled",
        budget_tokens: 10000,
    }),
    ..Default::default()
})?;

IDE autocomplete funciona. Sem typos em string keys em runtime.

5. Cancelamento via Drop

Quando o stream sai de escopo, a request HTTP é cancelada automaticamente:

{
    let result = model.do_stream(&options).await?;
    // Se este bloco terminar ou der panic, a request é cancelada
    // Sem precisar lembrar de chamar .cancel()
}
// Request já foi cancelada aqui

Rust garante cleanup de recursos via ownership. Você literalmente não consegue esquecer de cancelar.

Sistema de Tools

O sistema de ferramentas suporta execução multi-step com aprovação humana:

let tools = ToolSet::from([
    tool("weather")
        .description("Pega o clima atual de uma cidade")
        .schema(json_schema!({
            "type": "object",
            "properties": { "location": { "type": "string" } },
            "required": ["location"]
        }))
        .execute(|input| async move {
            Ok(format!("25°C e ensolarado em {}", input["location"]))
        })
        .build(),
]);

let result = generate_text(GenerateTextOptions {
    model: &openai("gpt-4o"),
    prompt: Prompt::simple("Qual o clima em São Paulo?"),
    tools,
    max_steps: 5, // permite loops de tool calling
    ..Default::default()
}).await?;

Para ferramentas perigosas, tem workflow de aprovação:

tool("delete_file")
    .needs_approval(|input| {
        // Exige aprovação para paths sensíveis
        input["path"].as_str()
            .map(|p| p.contains("/etc") || p.contains("system"))
            .unwrap_or(false)
    })
    .execute(|input| async move { /* ... */ })
    .build()

Middleware

Middleware intercepta e transforma chamadas ao modelo:

use ararajuba::{wrap_language_model, extract_reasoning_middleware};

let model = wrap_language_model(
    anthropic("claude-opus-4-6"),
    extract_reasoning_middleware(),
);
// Agora <thinking>...</thinking> é extraído automaticamente

A inovação: middleware pode chamar tanto do_generate quanto do_stream, permitindo cenários como "simular streaming a partir de uma API não-streaming" ou "cachear resultados de streaming".

MCP (Model Context Protocol)

Cliente MCP integrado com 3 transportes:

use ararajuba::mcp::{create_mcp_client, StdioTransportConfig};

let mcp = create_mcp_client(StdioTransportConfig {
    command: "npx".into(),
    args: vec!["@anthropic/mcp-server-github".into()],
    ..Default::default()
}).await?;

let tools = mcp.tools().await?;
// Todas as ferramentas MCP viram ToolSet do Ararajuba

Arquitetura

ararajuba/                  # Facade crate (o que o usuário importa)
core/
├── provider                # Traits: LanguageModel, EmbeddingModel, ImageModel, etc.
├── provider-utils          # HTTP, SSE parsing, retry, API key loading
├── ai                      # High-level API: generate_text, tools, middleware, agent
└── mcp                     # MCP client com stdio/HTTP/SSE

providers/
├── openai-compatible       # Base para APIs compatíveis com OpenAI
├── openai                  # OpenAI (GPT-4o, o1/o3/o4, GPT-5, DALL-E, TTS, Whisper)
├── anthropic               # Anthropic (Claude 3.5/4/4.5/4.6, thinking, caching)
├── google                  # Google (Gemini, Imagen, Veo)
└── deepseek                # DeepSeek (chat, reasoner)

tools/
└── coding                  # Tools prontas: file system, git, shell, diagnostics

11 crates, 307 testes, edição Rust 2024.

Compatibilidade com Frontend

O protocolo de UI streaming (UIMessageStream, formato SSE) é compatível com os hooks do @ai-sdk/react (useChat, useCompletion, useObject). Se seu frontend já consome o protocolo SSE padrão do Vercel AI SDK, funciona com Ararajuba como backend.

Instalação

[dependencies]
ararajuba = { version = "0.1", features = ["openai", "anthropic"] }

Features disponíveis: openai, anthropic, google, deepseek, mcp, coding-tools, full.


O Ararajuba é open source (Apache-2.0). Contribuições são bem-vindas — especialmente se você quer adicionar novos providers ou melhorar os existentes.

Se você trabalha com Rust e LLMs, experimenta e me conta o que achou.

Carregando publicação patrocinada...