DEV Community

Mattias chaw
Mattias chaw

Posted on • Originally published at aiwave.live

GDPR-Aware AI Usage Ledgers for Multi-Model Applications

Most teams discover multi-model AI billing the hard way: a raw request log is useful for debugging one 500 error, but it is a weak source of truth for cost, retries, fallback behavior, and data retention.

If your application can call DeepSeek, Kimi, Qwen, GLM, MiniMax, or ERNIE behind an OpenAI-compatible gateway, the operational question is not only "did the request succeed?" It is also:

  • Which model did we choose, and why?
  • How many uncached input tokens, cached input tokens, and output tokens were billed?
  • Did a retry or fallback change the price?
  • Which price sheet did we use when estimating cost?
  • Can we debug production behavior without storing personal data in prompts?

That is what I mean by a usage ledger. It is a small append-only record of each billable AI decision. It should be boring, queryable, and designed for audits.

This post uses AIWave as the gateway example because it exposes Chinese AI models through an OpenAI-compatible API at https://aiwave.live/v1. The same ledger pattern also works if you call providers directly. AIWave's public pricing page was checked on 2026-08-05 before writing this article; the pricing endpoint returned 62 model entries. The examples below use USD per 1M tokens and keep both input and output rates explicit.

Why a ledger beats raw logs

Raw logs tend to grow around whatever failed last week. They capture request bodies, stack traces, HTTP status codes, and sometimes complete prompts. That is convenient during development, but it becomes awkward in production.

For European customers or any enterprise buyer with a privacy review, prompt logging is often the first uncomfortable conversation. You may not need the prompt text to answer a billing question. You usually need a hashed tenant ID, a model name, token counts, region, retry count, validation result, and a price source date.

A ledger gives engineering and finance the same neutral object. It lets you ask:

  • "Which tenants crossed their daily model budget?"
  • "How much did fallback from one model to another change spend?"
  • "Which task classes generate the most output tokens?"
  • "Are we storing personal data when aggregated telemetry would be enough?"

That is especially important when you route across Chinese model families. Provider docs and model catalogs change often. DeepSeek documents an OpenAI-compatible API surface, Qwen publishes model and deployment information through Alibaba Cloud and Qwen docs, Zhipu documents GLM APIs through BigModel, and Moonshot documents Kimi APIs separately. A gateway reduces integration friction, but it does not remove your need for cost evidence.

Useful references:

Current price examples

The table below is not a benchmark and not a promise of uptime. It is a dated pricing snapshot from AIWave's public pricing source on 2026-08-05.

Model Input, USD / 1M tokens Output, USD / 1M tokens Cached input, USD / 1M tokens
deepseek-v4-flash 0.10 0.21 0.02
deepseek-v4-pro 0.54 1.09 0.27
kimi-k2.6 0.55 2.30 0.09
glm-5-turbo 0.90 2.70 0.24
qwen3.5-122b-a10b 0.25 1.86 n/a
minimax-m2.5 0.25 1.00 n/a

The practical lesson is simple: a request with a long cached prefix, a short uncached user message, and a small answer has a very different cost profile from a request that generates thousands of output tokens. Your ledger needs separate columns for cached input, uncached input, and output. A single tokens_total column hides the part of the bill you need to control.

A minimal ledger schema

Start with fields that survive provider changes:

Field Why it matters
request_id Join app traces, gateway calls, and customer support tickets
tenant_hash Track account-level cost without storing direct identifiers
task_class Separate coding, extraction, chat, search, and batch jobs
model Make routing and fallback visible
region Useful for latency review and data residency notes
cached_input_tokens Required for cache-aware pricing
uncached_input_tokens Required for normal input pricing
output_tokens Often the biggest cost driver
retry_count Retries can quietly inflate bills
fallback_from Shows whether a cheaper model failed validation
validation_result Helps connect quality gates to spend
estimated_usd The calculated cost at request time
price_source_url Where the rate came from
price_checked_at When the rate was verified
retention_class Example: aggregate_only, debug_7d, audit_90d

Do not store API keys in the ledger. In code examples, use YOUR_API_KEY_HERE and keep real keys in environment variables or a secret manager. Do not commit them to git.

Python: call AIWave and append a privacy-safe ledger row

This script is intentionally small. It estimates tokens with tiktoken when available and falls back to a rough word-based estimate if you do not have that dependency installed. In a real service, use the token counts returned by your gateway or SDK response whenever available.

import csv
import hashlib
import os
import time
from pathlib import Path

from openai import OpenAI

AIWAVE_API_KEY = os.getenv("AIWAVE_API_KEY", "YOUR_API_KEY_HERE")
LEDGER_PATH = Path("ai_usage_ledger.csv")

client = OpenAI(
    api_key=AIWAVE_API_KEY,
    base_url="https://aiwave.live/v1",
)

PRICES = {
    "deepseek-v4-flash": {
        "input_per_1m": 0.10,
        "output_per_1m": 0.21,
        "cached_input_per_1m": 0.02,
        "source": "https://aiwave.live/pricing",
        "checked_at": "2026-08-05",
    }
}


def estimate_tokens(text: str) -> int:
    try:
        import tiktoken

        enc = tiktoken.get_encoding("cl100k_base")
        return len(enc.encode(text))
    except Exception:
        return max(1, int(len(text.split()) * 1.35))


def tenant_hash(tenant_id: str) -> str:
    return hashlib.sha256(tenant_id.encode("utf-8")).hexdigest()[:16]


def estimate_cost(model: str, uncached_input: int, cached_input: int, output: int) -> float:
    rate = PRICES[model]
    return (
        uncached_input * rate["input_per_1m"]
        + cached_input * rate["cached_input_per_1m"]
        + output * rate["output_per_1m"]
    ) / 1_000_000


def append_ledger(row: dict) -> None:
    write_header = not LEDGER_PATH.exists()
    with LEDGER_PATH.open("a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=row.keys())
        if write_header:
            writer.writeheader()
        writer.writerow(row)


prompt = "Summarize three operational risks of raw AI prompt logging."
model = "deepseek-v4-flash"

response = client.chat.completions.create(
    model=model,
    messages=[{"role": "user", "content": prompt}],
    temperature=0.2,
)

answer = response.choices[0].message.content or ""
uncached_input_tokens = estimate_tokens(prompt)
output_tokens = estimate_tokens(answer)
estimated_usd = estimate_cost(model, uncached_input_tokens, 0, output_tokens)

append_ledger(
    {
        "request_id": f"req_{int(time.time() * 1000)}",
        "tenant_hash": tenant_hash("customer-123"),
        "task_class": "privacy_review_summary",
        "model": model,
        "region": "Singapore",
        "cached_input_tokens": 0,
        "uncached_input_tokens": uncached_input_tokens,
        "output_tokens": output_tokens,
        "retry_count": 0,
        "fallback_from": "",
        "validation_result": "accepted",
        "estimated_usd": f"{estimated_usd:.8f}",
        "price_source_url": PRICES[model]["source"],
        "price_checked_at": PRICES[model]["checked_at"],
        "retention_class": "aggregate_only",
    }
)

print(answer)
print(f"Estimated request cost: ${estimated_usd:.8f}")
Enter fullscreen mode Exit fullscreen mode

Install dependencies with pip install openai tiktoken.

JavaScript: enforce a tenant budget before calling the model

The ledger becomes more useful when it blocks bad behavior before it reaches the API. This Node.js example checks a simple in-memory daily budget, calls AIWave through the OpenAI SDK, and records the estimated spend.

import OpenAI from "openai";
import crypto from "node:crypto";
import fs from "node:fs";

const client = new OpenAI({
  apiKey: process.env.AIWAVE_API_KEY || "YOUR_API_KEY_HERE",
  baseURL: "https://aiwave.live/v1",
});

const prices = {
  "minimax-m2.5": {
    inputPer1M: 0.25,
    outputPer1M: 1.0,
    source: "https://aiwave.live/pricing",
    checkedAt: "2026-08-05",
  },
};

const dailyBudgetUsd = new Map([["tenant-a", 0.05]]);
const spentUsd = new Map([["tenant-a", 0]]);

function roughTokens(text) {
  return Math.max(1, Math.ceil(text.split(/\s+/).length * 1.35));
}

function hashTenant(tenantId) {
  return crypto.createHash("sha256").update(tenantId).digest("hex").slice(0, 16);
}

function estimateCost(model, inputTokens, outputTokens) {
  const rate = prices[model];
  return (inputTokens * rate.inputPer1M + outputTokens * rate.outputPer1M) / 1_000_000;
}

function appendLedger(row) {
  fs.appendFileSync("ai_usage_ledger.jsonl", `${JSON.stringify(row)}\n`, "utf8");
}

async function run() {
  const tenantId = "tenant-a";
  const model = "minimax-m2.5";
  const prompt = "Write a compact checklist for validating AI API cost telemetry.";
  const inputTokens = roughTokens(prompt);
  const preflightCost = estimateCost(model, inputTokens, 800);

  if ((spentUsd.get(tenantId) || 0) + preflightCost > dailyBudgetUsd.get(tenantId)) {
    throw new Error("Daily AI budget would be exceeded");
  }

  const completion = await client.chat.completions.create({
    model,
    messages: [{ role: "user", content: prompt }],
    temperature: 0.2,
  });

  const output = completion.choices[0]?.message?.content || "";
  const outputTokens = roughTokens(output);
  const finalCost = estimateCost(model, inputTokens, outputTokens);

  spentUsd.set(tenantId, (spentUsd.get(tenantId) || 0) + finalCost);

  appendLedger({
    request_id: `req_${Date.now()}`,
    tenant_hash: hashTenant(tenantId),
    task_class: "cost_telemetry_checklist",
    model,
    region: "Singapore",
    cached_input_tokens: 0,
    uncached_input_tokens: inputTokens,
    output_tokens: outputTokens,
    retry_count: 0,
    fallback_from: null,
    validation_result: "accepted",
    estimated_usd: Number(finalCost.toFixed(8)),
    price_source_url: prices[model].source,
    price_checked_at: prices[model].checkedAt,
    retention_class: "aggregate_only",
  });

  console.log(output);
  console.log(`Estimated request cost: $${finalCost.toFixed(8)}`);
}

run().catch((error) => {
  console.error(error.message);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Install dependencies with npm install openai.

What to redact

For most teams, the default ledger should not contain prompt text, completion text, email addresses, direct customer names, or payment identifiers. Keep those in a short-lived debug store only when you have a clear product and legal reason.

The ledger can still be useful without sensitive content. A row like "German tenant, invoice extraction, Kimi model, validation failed, fallback to DeepSeek, 1 retry, 0.00042 USD estimated" is enough to debug routing and billing. It does not need the invoice itself.

What AIWave does and does not solve

AIWave helps if your application already speaks the OpenAI Chat Completions API and you want access to Chinese model families through one base URL. That reduces SDK churn. It also avoids asking an overseas developer to open multiple domestic provider accounts before testing DeepSeek, Kimi, Qwen, GLM, MiniMax, or ERNIE.

It does not remove your responsibility for privacy design, tenant budgets, model evaluation, or audit logging. It also does not mean every Chinese model is interchangeable. Some are better suited to long-context writing, some to coding, some to extraction, and some to low-cost background jobs. Treat model selection as an engineering decision, not a brand decision.

FAQ

Should I store prompts in the usage ledger?

Usually no. Store operational metadata first. If you need prompt captures for debugging, put them behind a short retention period, access controls, and explicit redaction.

Can I rely on estimated token counts?

Use estimates only for preflight checks. For final billing and reporting, prefer token counts returned by the API response or gateway logs when available.

Why include the price source date?

AI model pricing changes. A dated source lets finance and engineering understand why an old request was estimated at a different rate from today's pricing page.

Is this only for GDPR?

No. GDPR is a useful forcing function, but the same ledger helps with SOC 2 evidence, customer support, spend limits, and model-quality reviews.

Top comments (0)