Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,22 @@ provider/model routing, OpenAI-compatible non-streaming and streaming chat,
usage metering, credit ledger updates, Redis runtime limits, and Prometheus
gateway metrics.

The long-term contract is:

- Keep API-facing provider output in an OpenAI-compatible shape.
- Support both `/v1/chat/completions` and `/v1/responses` through the same
normalization path.
- Add non-API provider adapters later (CLI/browser session types) without changing
client contract.

This is not a stable/full release yet. The remaining tracked work before a
broader MVP is the RTK-backed CLI proxy baseline and durable request/admin
audit log foundations.

## MVP Scope

- OpenAI-compatible gateway for `/v1/chat/completions` and `/v1/models`
- OpenAI-compatible gateway for `/v1/chat/completions`, `/v1/responses`, and
`/v1/models`
- Admin-managed upstream connections for API providers and local models
- User registration, virtual API keys, model access rules, and usage history
- Credit accounting based on input/output token prices per 1M tokens
Expand Down
1 change: 1 addition & 0 deletions crates/mizan-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ futures-util.workspace = true
redis.workspace = true
sha2.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tokio.workspace = true
tower-http.workspace = true
Expand Down
22 changes: 22 additions & 0 deletions crates/mizan-api/src/billing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ use mizan_metering::UsageChargeInput;
use mizan_providers::{ChatMessage, TokenUsage};
use mizan_wallet::{RoutePrice, calculate_usage_charge};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{Any, AnyPool, FromRow, Transaction, query, query_as};
use tracing::warn;
use uuid::Uuid;

use crate::AppState;
use crate::auth::ApiKeyIdentity;
use crate::logging::{AdminAuditInput, record_admin_audit, serialize_payload};
use crate::utils::{from_app_error, prepare_sql, unix_timestamp_string};

const AUDIT_ACTION_CREDIT_GRANT: &str = "credit_granted";
const AUDIT_ENTITY_USER: &str = "user";

const DEFAULT_USAGE_LIST_LIMIT: i64 = 100;
const MAX_USAGE_LIST_LIMIT: i64 = 500;
const CREDIT_GRANT_REASON: &str = "credit_grant";
Expand Down Expand Up @@ -163,6 +169,7 @@ pub async fn list_usage_admin(
pub async fn grant_credits(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
Extension(identity): Extension<ApiKeyIdentity>,
Json(payload): Json<GrantRequest>,
) -> BillingHttpResult<Json<GrantResponse>> {
if payload.amount_microcredits <= 0 {
Expand Down Expand Up @@ -240,6 +247,21 @@ pub async fn grant_credits(
)))
})?;

let audit = AdminAuditInput {
actor_user_id: Some(identity.user_id),
action: AUDIT_ACTION_CREDIT_GRANT.to_owned(),
entity_type: AUDIT_ENTITY_USER.to_owned(),
entity_id: Some(user_id.to_string()),
payload_json: serialize_payload(json!({
"amount_microcredits": payload.amount_microcredits,
"reason": reason,
})),
};
if let Err(error) = record_admin_audit(&state.database, state.database_backend(), &audit).await
{
warn!(error = %error, "failed to record credit grant audit");
Comment on lines +260 to +262
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make admin audit writes atomic with mutations

When the admin_audit_logs insert fails here (for example because migrations were disabled or the audit table/schema is unavailable), the credit grant has already committed and this handler still returns success, leaving a balance-changing admin action with no audit row. Since the audit log is being introduced as durable admin audit coverage, record it in the same transaction as the credit ledger/wallet update or fail the mutation when the audit write cannot be persisted.

Useful? React with 👍 / 👎.

}

Ok(Json(GrantResponse {
user_id,
wallet_id: wallet.id,
Expand Down
Loading