rig-core Wrapper
Loquent is migrating AI call sites from aisdk to rig-core (v0.37.0, exact-pinned). The shared wrapper at src/mods/ai/rig/ handles client construction, model wrapping with automatic usage logging, and error mapping — so each migrated call site is a one-line swap.
Module Structure
Section titled “Module Structure”src/mods/ai/rig/├── mod.rs # Re-exports├── client.rs # OpenRouter client constructor├── model.rs # LoggedModel / LoggedClient wrappers├── agent.rs # build_rig_agent() convenience function└── error.rs # rig-core → AppError mappingLoggedModel
Section titled “LoggedModel”LoggedModel<M> wraps any CompletionModel and intercepts every completion call to log token usage automatically.
pub struct LoggedModel<M: CompletionModel> { inner: M, organization_id: Uuid, feature: AiUsageFeature, model_id: String,}When you call .completion() on a LoggedModel, it:
- Delegates to the inner model’s
completion() - Extracts
Usagefrom the response (input, output, cached, reasoning tokens) - Spawns
spawn_log_ai_usage()if any tokens were consumed - Returns the response unchanged
Because LoggedModel itself implements CompletionModel, it composes transparently with rig’s Agent — including future tool-calling loops where each turn gets logged automatically.
Building an Agent
Section titled “Building an Agent”Use build_rig_agent() to construct a fully-wired agent in one call:
use crate::mods::ai::rig::build_rig_agent;use crate::mods::ai::{AiArea, AiUsageFeature};
let agent = build_rig_agent( organization_id, AiArea::AssistantTitle, AiUsageFeature::AssistantTitle, "Generate a short conversation title.",).await?;
let result = agent.prompt("User asked about billing").await?;Under the hood, build_rig_agent:
- Calls
openrouter_client()— loads the API key fromcore_conf - Resolves the model ID via
resolve_model(area)(checksai_model_configtable, falls back to the area default) - Wraps the model in
LoggedModelwith the organization ID and feature tag - Builds a rig
Agentwith the system prompt
No manual usage logging needed — LoggedModel handles it.
Error Mapping
Section titled “Error Mapping”error.rs converts rig-core errors into AppError::Internal:
impl From<CompletionError> for AppError { ... }impl From<PromptError> for AppError { ... }Both forward the upstream error message. Be aware that CompletionError::ProviderError can include parts of the provider’s raw response body.
Usage Entry Construction
Section titled “Usage Entry Construction”AiUsageEntry::from_rig_usage() maps rig-core’s Usage struct to Loquent’s usage log format:
AiUsageEntry::from_rig_usage( organization_id, None, // optional call_id AiUsageFeature::AssistantTitle, "deepseek/deepseek-chat", // model string &usage, // rig_core::completion::Usage)The provider name is extracted from the model string prefix (e.g., "deepseek/deepseek-chat" → "deepseek"). Token counts are cast from u64 to i32.
Migrating a Call Site
Section titled “Migrating a Call Site”To migrate a non-streaming, non-tool-calling call site from aisdk:
- Replace the
LanguageModelRequestbuilder withbuild_rig_agent(...).await? - Replace
.generate_text()with.prompt(user_input).await? - Delete the explicit
spawn_log_ai_usagecall —LoggedModelhandles it - Errors map automatically via
From<CompletionError>
Before (aisdk):
let model = build_openrouter_model(&model_name).await?;let response = LanguageModelRequest::builder() .model(model).system(prompt).prompt(&input) .build().generate_text().await?;spawn_log_ai_usage(AiUsageEntry::from_text_generation(...));let text = response.text().unwrap();After (rig-core):
let agent = build_rig_agent(org_id, area, feature, prompt).await?;let text = agent.prompt(input).await?;Current Status
Section titled “Current Status”| Migration state | Call sites |
|---|---|
| ✅ Migrated | generate_title_service |
| ⏳ Blocked (streaming) | assistant_service::stream_assistant_message |
| ⏳ Blocked (tool-calling) | assistant_service (chat), plan_service, text_agent_service |