feat: implement provider credit tracking and balance management

- Added 'credit_balance' and 'low_credit_threshold' to 'provider_configs' table.
- Updated dashboard backend to support reading and updating provider credits.
- Implemented real-time credit deduction from provider balances on successful requests.
- Added visual balance indicators and configuration modal to the 'Providers' dashboard tab.
This commit is contained in:
2026-02-26 18:25:39 -05:00
parent 9b254d50ea
commit efb50737bf
4 changed files with 64 additions and 10 deletions

View File

@@ -536,7 +536,7 @@ async fn handle_get_providers(State(state): State<DashboardState>) -> Json<ApiRe
let pool = &state.app_state.db_pool;
// Load all overrides from database
let db_configs_result = sqlx::query("SELECT id, enabled, base_url FROM provider_configs")
let db_configs_result = sqlx::query("SELECT id, enabled, base_url, credit_balance, low_credit_threshold FROM provider_configs")
.fetch_all(pool)
.await;
@@ -546,7 +546,9 @@ async fn handle_get_providers(State(state): State<DashboardState>) -> Json<ApiRe
let id: String = row.get("id");
let enabled: bool = row.get("enabled");
let base_url: Option<String> = row.get("base_url");
db_configs.insert(id, (enabled, base_url));
let balance: f64 = row.get("credit_balance");
let threshold: f64 = row.get("low_credit_threshold");
db_configs.insert(id, (enabled, base_url, balance, threshold));
}
}
@@ -566,12 +568,17 @@ async fn handle_get_providers(State(state): State<DashboardState>) -> Json<ApiRe
_ => (false, "".to_string(), "Unknown"),
};
let mut balance = 0.0;
let mut threshold = 5.0;
// Apply database overrides
if let Some((db_enabled, db_url)) = db_configs.get(id) {
if let Some((db_enabled, db_url, db_balance, db_threshold)) = db_configs.get(id) {
enabled = *db_enabled;
if let Some(url) = db_url {
base_url = url.clone();
}
balance = *db_balance;
threshold = *db_threshold;
}
// Find models for this provider in registry
@@ -606,6 +613,8 @@ async fn handle_get_providers(State(state): State<DashboardState>) -> Json<ApiRe
"status": status,
"models": models,
"base_url": base_url,
"credit_balance": balance,
"low_credit_threshold": threshold,
"last_used": None::<String>,
}));
}
@@ -625,6 +634,8 @@ struct UpdateProviderRequest {
enabled: bool,
base_url: Option<String>,
api_key: Option<String>,
credit_balance: Option<f64>,
low_credit_threshold: Option<f64>,
}
async fn handle_update_provider(
@@ -637,12 +648,14 @@ async fn handle_update_provider(
// Update or insert into database
let result = sqlx::query(
r#"
INSERT INTO provider_configs (id, display_name, enabled, base_url, api_key)
VALUES (?, ?, ?, ?, ?)
INSERT INTO provider_configs (id, display_name, enabled, base_url, api_key, credit_balance, low_credit_threshold)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
enabled = excluded.enabled,
base_url = excluded.base_url,
api_key = COALESCE(excluded.api_key, provider_configs.api_key),
credit_balance = COALESCE(excluded.credit_balance, provider_configs.credit_balance),
low_credit_threshold = COALESCE(excluded.low_credit_threshold, provider_configs.low_credit_threshold),
updated_at = CURRENT_TIMESTAMP
"#
)
@@ -651,6 +664,8 @@ async fn handle_update_provider(
.bind(payload.enabled)
.bind(&payload.base_url)
.bind(&payload.api_key)
.bind(payload.credit_balance)
.bind(payload.low_credit_threshold)
.execute(pool)
.await;

View File

@@ -87,6 +87,8 @@ async fn run_migrations(pool: &DbPool) -> Result<()> {
enabled BOOLEAN DEFAULT TRUE,
base_url TEXT,
api_key TEXT,
credit_balance REAL DEFAULT 0.0,
low_credit_threshold REAL DEFAULT 5.0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#

View File

@@ -56,6 +56,8 @@ impl RequestLogger {
/// Insert a log entry into the database
async fn insert_log(pool: &SqlitePool, log: RequestLog) -> Result<(), sqlx::Error> {
let mut tx = pool.begin().await?;
sqlx::query(
r#"
INSERT INTO llm_requests
@@ -65,7 +67,7 @@ impl RequestLogger {
)
.bind(log.timestamp)
.bind(log.client_id)
.bind(log.provider)
.bind(&log.provider)
.bind(log.model)
.bind(log.prompt_tokens as i64)
.bind(log.completion_tokens as i64)
@@ -77,9 +79,22 @@ impl RequestLogger {
.bind(log.duration_ms as i64)
.bind(None::<String>) // request_body - TODO: store serialized request
.bind(None::<String>) // response_body - TODO: store serialized response or error
.execute(pool)
.execute(&mut *tx)
.await?;
// Deduct from provider balance if successful
if log.cost > 0.0 {
sqlx::query(
"UPDATE provider_configs SET credit_balance = credit_balance - ? WHERE id = ?"
)
.bind(log.cost)
.bind(&log.provider)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
}