DEV Community

Mattias chaw
Mattias chaw

Posted on

Build a Billing Reconciliation Harness for Chinese AI Model Routers

Build a Billing Reconciliation Harness for Chinese AI Model Routers

Chinese AI APIs are moving fast enough that a static cost spreadsheet is now a liability. DeepSeek has versioned V4 Flash and V4 Pro pricing, Kimi K3 exposes separate cache-hit and cache-miss input rates for 1M-token contexts, Z.AI publishes GLM cached-input rates and tool fees, and QwenCloud uses context tiers for long prompts. If your application routes requests across these models, the invoice risk is not only "which model did we call?" It is "which dated price sheet did the router believe, which cache state did the provider apply, and which tokens actually landed on the bill?"

That is why production teams should treat pricing as a reconciled data feed rather than a blog-post constant. A router can choose a model in milliseconds, but finance, SRE, and product teams need a daily way to prove that request-level cost estimates match provider billing within a tolerable margin. This article shows a practical pattern: build a billing reconciliation harness that records dated pricing snapshots, estimates each request before dispatch, imports provider usage later, and flags drift before it becomes a customer-facing surprise.

The examples use a neutral OpenAI-compatible gateway shape, so you can run the same pattern with direct provider APIs or a unified endpoint such as AIWave. Code samples use environment variables and placeholders only.

Why Billing Reconciliation Belongs in the Router

Most teams start with a simple route table:

Workload Primary model Fallback model Cost control
Coding agent planning DeepSeek V4 Pro GLM-5.1 Cap output tokens
Coding agent execution DeepSeek V4 Flash qwen3.7-plus Prefer cached context
Long repository analysis Kimi K3 Qwen long-context model Track cache-hit rate
Structured extraction GLM-5.1 DeepSeek V4 Flash Validate JSON retries

That table is useful, but it hides three billing realities.

First, cache-hit input and cache-miss input are different products. Kimi K3, DeepSeek, and GLM pricing all make repeated context materially different from fresh context. A single "input tokens" column cannot explain the bill.

Second, some price sheets now need effective dates. DeepSeek's official documentation and recent market reporting point to dated V4 pricing changes, including peak and off-peak behavior. If you overwrite yesterday's price row, you can no longer explain yesterday's invoice.

Third, long-context tiers can change the marginal rate. QwenCloud documents per-model text pricing where long input windows can move into higher tiers. A route that is economical at 80K input tokens might not remain economical at 600K.

The router is the right place to capture the expected billing dimensions because it already knows the model, route, tenant, prompt size, completion cap, cache key, and fallback path. Reconciliation then becomes a deterministic join instead of an incident review.

Pricing Snapshot Shape

Do not store pricing as one mutable JSON object per provider. Store append-only snapshots with effective_from, source_url, and a version hash. As of August 15, 2026, the public pages worth checking before a production rollout include:

Provider Dated pricing fields to snapshot Operational concern
DeepSeek API docs V4 Flash and V4 Pro cache-hit input, cache-miss input, output, context length, effective pricing windows Peak/off-peak changes and model-version drift
Kimi K3 pricing Cache-hit input, cache-miss input, output, 1M-token context Long-context cache economics
Z.AI pricing GLM input, cached input, output, storage and tool fees Tool-call and cached-input attribution
QwenCloud pricing Model-specific input tiers, output rates, marketplace references Context-tier transitions

Your snapshot does not need to mirror every provider page. It needs enough structure to reproduce the estimate that your router gave a request at the time it was sent.

{
  "provider": "deepseek",
  "model": "deepseek-v4-pro",
  "effective_from": "2026-08-15T00:00:00Z",
  "effective_until": "2026-08-16T16:00:00Z",
  "currency": "USD",
  "unit": "1M_tokens",
  "input_cache_hit": 0.003625,
  "input_cache_miss": 0.435,
  "output": 0.87,
  "source_url": "https://api-docs.deepseek.com/quick_start/pricing/",
  "checked_at": "2026-08-15T12:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

For Qwen-style context tiers, model the input side as bands rather than a scalar.

{
  "provider": "qwencloud",
  "model": "qwen3.7-plus",
  "effective_from": "2026-08-15T00:00:00Z",
  "currency": "USD",
  "unit": "1M_tokens",
  "input_tiers": [
    {"max_input_tokens": 256000, "rate": 0.40},
    {"max_input_tokens": 1000000, "rate": 1.20}
  ],
  "output": 1.60,
  "source_url": "https://docs.qwencloud.com/developer-guides/getting-started/pricing",
  "checked_at": "2026-08-15T12:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

The point is not to claim that these values will remain stable. The point is to make instability auditable.

Request Ledger

Every routed request should write a compact billing ledger row before returning to the caller. Keep it small enough for hot-path writes, but rich enough for reconciliation.

import os
import time
import uuid
from dataclasses import dataclass, asdict

@dataclass
class BillingLedgerRow:
    request_id: str
    tenant_id: str
    route_name: str
    provider: str
    model: str
    pricing_snapshot_id: str
    prompt_tokens: int
    cached_prompt_tokens: int
    completion_tokens: int
    estimated_cost_usd: float
    cache_key_version: str
    fallback_from: str | None
    created_at_ms: int

def make_ledger_row(
    *,
    tenant_id: str,
    route_name: str,
    provider: str,
    model: str,
    pricing_snapshot_id: str,
    prompt_tokens: int,
    cached_prompt_tokens: int,
    completion_tokens: int,
    estimated_cost_usd: float,
    cache_key_version: str,
    fallback_from: str | None = None,
) -> dict:
    return asdict(BillingLedgerRow(
        request_id=str(uuid.uuid4()),
        tenant_id=tenant_id,
        route_name=route_name,
        provider=provider,
        model=model,
        pricing_snapshot_id=pricing_snapshot_id,
        prompt_tokens=prompt_tokens,
        cached_prompt_tokens=cached_prompt_tokens,
        completion_tokens=completion_tokens,
        estimated_cost_usd=round(estimated_cost_usd, 8),
        cache_key_version=cache_key_version,
        fallback_from=fallback_from,
        created_at_ms=int(time.time() * 1000),
    ))

AIWAVE_API_KEY = os.environ.get("AIWAVE_API_KEY", "YOUR_API_KEY_HERE")
Enter fullscreen mode Exit fullscreen mode

Do not log prompts in this ledger. Reconciliation needs token counts, provider identifiers, route decisions, and price snapshot identifiers. It does not need raw customer content.

Estimation Function

A reconciler should use the same estimator that the router used, or at least the same rules. That prevents "the dashboard says one thing, the router says another" drift.

def estimate_token_cost(snapshot: dict, prompt_tokens: int, cached_tokens: int, output_tokens: int) -> float:
    cached_tokens = min(max(cached_tokens, 0), prompt_tokens)
    uncached_tokens = prompt_tokens - cached_tokens

    if "input_tiers" in snapshot:
        input_rate = rate_for_tier(snapshot["input_tiers"], prompt_tokens)
        input_cost = (prompt_tokens / 1_000_000) * input_rate
    else:
        input_cost = (
            (cached_tokens / 1_000_000) * snapshot.get("input_cache_hit", 0)
            + (uncached_tokens / 1_000_000) * snapshot["input_cache_miss"]
        )

    output_cost = (output_tokens / 1_000_000) * snapshot["output"]
    return input_cost + output_cost

def rate_for_tier(tiers: list[dict], prompt_tokens: int) -> float:
    for tier in tiers:
        if prompt_tokens <= tier["max_input_tokens"]:
            return tier["rate"]
    return tiers[-1]["rate"]
Enter fullscreen mode Exit fullscreen mode

This is intentionally boring code. Billing logic should be clear enough for engineering, finance, and support to read together.

Reconciliation Job

Run reconciliation after provider usage data is available. Some providers expose usage exports by API key; others require invoice exports, dashboard downloads, or gateway-level aggregation. Normalize them into provider usage rows.

from collections import defaultdict

def reconcile(ledger_rows: list[dict], provider_rows: list[dict], tolerance: float = 0.03) -> list[dict]:
    expected = defaultdict(float)
    observed = defaultdict(float)

    for row in ledger_rows:
        key = (row["provider"], row["model"], row["route_name"])
        expected[key] += row["estimated_cost_usd"]

    for row in provider_rows:
        key = (row["provider"], row["model"], row["route_name"])
        observed[key] += row["billed_cost_usd"]

    findings = []
    for key, expected_cost in expected.items():
        billed_cost = observed.get(key, 0.0)
        if expected_cost == 0:
            continue
        delta_ratio = (billed_cost - expected_cost) / expected_cost
        if abs(delta_ratio) > tolerance:
            findings.append({
                "provider": key[0],
                "model": key[1],
                "route_name": key[2],
                "expected_cost_usd": round(expected_cost, 4),
                "billed_cost_usd": round(billed_cost, 4),
                "delta_ratio": round(delta_ratio, 4),
            })
    return findings
Enter fullscreen mode Exit fullscreen mode

Start with a 3 percent tolerance, then tune by provider. Small differences can come from tokenization updates, rounding, retries, streaming cutoffs, and provider-side cache attribution. Large differences usually mean one of five things:

  1. The router used a stale pricing snapshot.
  2. The provider counted fewer cached tokens than expected.
  3. A fallback route changed the model but not the ledger row.
  4. Long-context tiering was missed.
  5. Tool-call or web-search charges were not included.

That list is more useful than a single blended "cost went up" alert.

Alert Routing

Do not page a human every time a provider bill differs by pennies. Route reconciliation findings by blast radius.

Finding Owner Action
One tenant, one route, small delta Support or growth engineer Add explanation to customer usage view
One model, many tenants Platform engineer Check pricing snapshot and route table
One provider, all routes SRE and finance Compare provider export with billing dashboard
Cache-hit collapse after deploy Application owner Review prompt template and cache key version
Output-token spike Product owner Inspect max token caps and task type

For Tier 1 and Tier 2 customers, the valuable experience is not a promise that prices never move. It is a system that can explain changes with dates, provider sources, and request-level evidence.

Dashboard Metrics That Matter

Once the harness exists, expose a small set of operational metrics:

  • Estimated vs billed cost by provider, model, route, and tenant.
  • Cache-hit token share by route and prompt-template version.
  • Effective price snapshot age.
  • Percentage of requests with unknown provider usage.
  • Fallback cost multiplier.
  • Output-token share of total spend.
  • Long-context tier distribution.

The two best early warning metrics are snapshot age and cache-hit token share. Snapshot age catches pricing drift before a bill arrives. Cache-hit share catches prompt-template changes that quietly turn repeated context into fresh input.

Production Rollout Checklist

Before turning reconciliation into customer-facing reporting, run it privately for at least one billing cycle.

  • Store append-only pricing snapshots with source URLs and checked dates.
  • Record pricing_snapshot_id on every routed request.
  • Keep prompt content out of billing rows.
  • Track cached and uncached input tokens separately.
  • Preserve fallback origin and final model.
  • Normalize provider exports into a stable internal schema.
  • Alert on route-level deltas, not only account-level totals.
  • Review official pricing pages before planned route-table changes.

This is also a good place to document how your gateway handles failed calls. Some providers do not bill failed requests; others may have provider-specific edge cases around timeouts, streaming interruptions, or tool calls. Your ledger should capture enough status data to separate "request estimated" from "request completed."

Closing

Chinese AI model access is becoming a routing problem, not a single-provider integration. DeepSeek, Kimi, GLM, and Qwen each expose different economics around cache, context, output, tools, and effective dates. A billing reconciliation harness turns that volatility into an engineering control: every route decision gets tied to a dated price snapshot, every invoice gets compared against request-level estimates, and every drift alert points to a fixable cause.

If you are building a multi-model API layer for production agents, start with this harness before the first customer asks why this week's usage looks different from last week's. It is easier to explain a dated ledger than to reverse-engineer a bill from logs after the route table has already changed.

Top comments (0)