Skip to content

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.

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 mapping

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:

  1. Delegates to the inner model’s completion()
  2. Extracts Usage from the response (input, output, cached, reasoning tokens)
  3. Spawns spawn_log_ai_usage() if any tokens were consumed
  4. 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.

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:

  1. Calls openrouter_client() — loads the API key from core_conf
  2. Resolves the model ID via resolve_model(area) (checks ai_model_config table, falls back to the area default)
  3. Wraps the model in LoggedModel with the organization ID and feature tag
  4. Builds a rig Agent with the system prompt

No manual usage logging needed — LoggedModel handles it.

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.

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.

To migrate a non-streaming, non-tool-calling call site from aisdk:

  1. Replace the LanguageModelRequest builder with build_rig_agent(...).await?
  2. Replace .generate_text() with .prompt(user_input).await?
  3. Delete the explicit spawn_log_ai_usage call — LoggedModel handles it
  4. 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?;
Migration stateCall sites
✅ Migratedgenerate_title_service
⏳ Blocked (streaming)assistant_service::stream_assistant_message
⏳ Blocked (tool-calling)assistant_service (chat), plan_service, text_agent_service