feat: create llm_call_log table + DB module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 88c16c5d67
commit b2b0b286c0

@ -117,7 +117,7 @@ cd frontend && npx tsc --noEmit
- `GET /api/v1/admin/users` — user list - `GET /api/v1/admin/users` — user list
- `PUT /api/v1/admin/users/:id/role` — role management - `PUT /api/v1/admin/users/:id/role` — role management
## Database (16 migrations) ## Database (17 migrations)
Tables: `users`, `sessions`, `magic_link_tokens`, `user_settings`, `sources`, `syntheses`, `admin_providers`, `admin_rate_limits`, `user_api_keys`, `audit_log` Tables: `users`, `sessions`, `magic_link_tokens`, `user_settings`, `sources`, `syntheses`, `admin_providers`, `admin_rate_limits`, `user_api_keys`, `audit_log`
## Environment Variables ## Environment Variables

@ -0,0 +1,14 @@
CREATE TABLE llm_call_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
job_id UUID NOT NULL,
call_type TEXT NOT NULL,
model TEXT NOT NULL,
system_prompt TEXT NOT NULL DEFAULT '',
user_prompt TEXT NOT NULL DEFAULT '',
response_body TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_llm_call_log_job_id ON llm_call_log(job_id);
CREATE INDEX idx_llm_call_log_user_id ON llm_call_log(user_id, created_at);

@ -0,0 +1,97 @@
//! LLM call logging: tracks every LLM interaction during synthesis generation.
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::PgPool;
use uuid::Uuid;
use crate::errors::AppError;
/// Row returned from llm_call_log queries.
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct LlmCallLogRow {
pub id: Uuid,
pub call_type: String,
pub model: String,
pub system_prompt: String,
pub user_prompt: String,
pub response_body: String,
pub duration_ms: i32,
pub created_at: DateTime<Utc>,
}
/// Insert a single LLM call log entry.
pub async fn insert(
pool: &PgPool,
user_id: Uuid,
job_id: Uuid,
call_type: &str,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_body: &str,
duration_ms: i32,
) -> Result<(), AppError> {
sqlx::query(
r#"
INSERT INTO llm_call_log (user_id, job_id, call_type, model, system_prompt, user_prompt, response_body, duration_ms)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
)
.bind(user_id)
.bind(job_id)
.bind(call_type)
.bind(model)
.bind(system_prompt)
.bind(user_prompt)
.bind(response_body)
.bind(duration_ms)
.execute(pool)
.await?;
Ok(())
}
/// List all LLM call log entries for a generation job.
pub async fn list_by_job_id(
pool: &PgPool,
user_id: Uuid,
job_id: Uuid,
) -> Result<Vec<LlmCallLogRow>, AppError> {
let rows = sqlx::query_as::<_, LlmCallLogRow>(
r#"
SELECT id, call_type, model, system_prompt, user_prompt, response_body, duration_ms, created_at
FROM llm_call_log
WHERE user_id = $1 AND job_id = $2
ORDER BY created_at ASC
"#,
)
.bind(user_id)
.bind(job_id)
.fetch_all(pool)
.await?;
Ok(rows)
}
/// Truncate old LLM call log entries: replace prompt/response with first 500 chars.
pub async fn truncate_old(
pool: &PgPool,
user_id: Uuid,
days: i32,
) -> Result<u64, AppError> {
let result = sqlx::query(
r#"
UPDATE llm_call_log
SET system_prompt = LEFT(system_prompt, 500) || E'\n[truncated]',
user_prompt = LEFT(user_prompt, 500) || E'\n[truncated]',
response_body = LEFT(response_body, 500) || E'\n[truncated]'
WHERE user_id = $1
AND created_at < now() - make_interval(days => $2)
AND LENGTH(system_prompt) > 510
"#,
)
.bind(user_id)
.bind(days)
.execute(pool)
.await?;
Ok(result.rows_affected())
}

@ -1,5 +1,6 @@
pub mod article_history; pub mod article_history;
pub mod api_keys; pub mod api_keys;
pub mod llm_call_log;
pub mod audit; pub mod audit;
pub mod magic_links; pub mod magic_links;
pub mod providers; pub mod providers;

Loading…
Cancel
Save