n4nAI

Typing token usage and cost fields in TypeScript

Learn how to model TypeScript token usage cost types for LLM API clients with strict interfaces, runtime validation, and accurate billing math.

n4n Team3 min read682 words

Audio narration

Coming soon — every post will get a voice note here.

Most LLM API clients ship loose any types for usage fields, which turns billing into a guessing game. Strong typescript token usage cost types let you catch missing prompt_tokens at compile time and compute spend without string parsing. This guide walks through building a typed client layer that survives real provider responses and keeps your finance team off your back.

Step 1: Define the core usage and cost types

Start by modeling what providers actually return. OpenAI-compatible APIs report prompt_tokens, completion_tokens, and total_tokens. Newer models add cache accounting. Capture those as optional fields so you don’t break on older responses.

export interface TokenUsage {
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  cacheReadTokens?: number;
  cacheWriteTokens?: number;
}

export interface CostBreakdown {
  inputCost: number;
  outputCost: number;
  cacheReadCost: number;
  totalCost: number;
  currency: 'USD';
}

Keep the token counts as raw integers. Store cost as floating point dollars, not cents, to avoid multiplying everywhere. If you later support non-USD billing, make currency a union rather than a literal.

The typescript token usage cost types above are the contract for every downstream function. No function should accept a bare number for tokens when it means promptTokens.

Step 2: Map provider responses to your types

Providers nest usage under usage. The field names are snake_case, not camelCase. Write a pure mapper that converts and backfills defaults.

interface RawUsage {
  prompt_tokens?: number;
  completion_tokens?: number;
  total_tokens?: number;
  prompt_tokens_details?: {
    cached_tokens?: number;
    cache_creation_tokens?: number;
  };
}

export function toTokenUsage(raw: RawUsage | undefined): TokenUsage {
  if (!raw) {
    throw new Error('usage field missing from response');
  }
  const prompt = raw.prompt_tokens ?? 0;
  const completion = raw.completion_tokens ?? 0;
  const total = raw.total_tokens ?? prompt + completion;
  return {
    promptTokens: prompt,
    completionTokens: completion,
    totalTokens: total,
    cacheReadTokens: raw.prompt_tokens_details?.cached_tokens,
    cacheWriteTokens: raw.prompt_tokens_details?.cache_creation_tokens,
  };
}

Never trust total_tokens to be present. Compute it when absent. This defensive mapping is what keeps your typescript token usage cost types honest when a provider silently changes its payload.

Step 3: Add a typed cost calculator

Pricing is model-specific and changes. Define a pricing table and a function that multiplies tokens by rates. Use per-token rates, not per-1k, to avoid division bugs.

interface ModelPricing {
  inputPerToken: number;
  outputPerToken: number;
  cacheReadPerToken: number;
}

const PRICING: Record<string, ModelPricing> = {
  'gpt-4o': {
    inputPerToken: 0.000005,
    outputPerToken: 0.000015,
    cacheReadPerToken: 0.00000125,
  },
};

export function computeCost(
  usage: TokenUsage,
  model: string,
  pricing: Record<string, ModelPricing> = PRICING
): CostBreakdown {
  const rate = pricing[model];
  if (!rate) throw new Error(`No pricing for model ${model}`);
  const inputCost = usage.promptTokens * rate.inputPerToken;
  const outputCost = usage.completionTokens * rate.outputPerToken;
  const cacheReadCost = (usage.cacheReadTokens ?? 0) * rate.cacheReadPerToken;
  return {
    inputCost,
    outputCost,
    cacheReadCost,
    totalCost: inputCost + outputCost + cacheReadCost,
    currency: 'USD',
  };
}

Round only at display time. Floating point drift across millions of calls is real; keep full precision in the typescript token usage cost types until you render.

Step 4: Validate at runtime with a type guard

Compile-time types vanish at runtime. A malformed response from a flaky provider can still slip through any boundaries. Add a guard that runs before you trust the data.

export function isTokenUsage(u: unknown): u is TokenUsage {
  if (typeof u !== 'object' || u === null) return false;
  const t = u as Record<string, unknown>;
  return (
    typeof t.promptTokens === 'number' &&
    typeof t.completionTokens === 'number' &&
    typeof t.totalTokens === 'number' &&
    (t.cacheReadTokens === undefined || typeof t.cacheReadTokens === 'number')
  );
}

Use it in your mapper or at the edge of the network call. If the guard fails, log the raw payload and throw. This is cheaper than debugging a NaN in the monthly invoice.

Step 5: Wrap a fetch client with typed return

Now compose a typed chat function. We’ll target an OpenAI-compatible endpoint. If you route through a gateway like n4n.ai, the per-token usage metering arrives in the same usage object, so the mapper from Step 2 needs no changes.

interface ChatResult {
  content: string;
  usage: TokenUsage;
  cost: CostBreakdown;
}

export async function chat(
  baseUrl: string,
  apiKey: string,
  model: string,
  messages: { role: string; content: string }[]
): Promise<ChatResult> {
  const res = await fetch(`${baseUrl}/v1/chat/completions`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify({ model, messages }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json = await res.json();
  const usage = toTokenUsage(json.usage);
  if (!isTokenUsage(usage)) throw new Error('invalid usage shape');
  const cost = computeCost(usage, model);
  return {
    content: json.choices[0].message.content,
    usage,
    cost,
  };
}

The return type ChatResult is the only thing your business logic sees. It encodes the typescript token usage cost types end to end.

Step 6: Handle fallback and partial usage

Automatic fallback between providers can yield a response where cache_read_tokens is absent even if your primary model supports it. Your optional fields already absorb that. But watch for total_tokens mismatches when a fallback model uses different tokenization.

Write a reconciliation check:

export function reconcileUsage(u: TokenUsage): TokenUsage {
  const computed = u.promptTokens + u.completionTokens;
  if (u.totalTokens !== computed) {
    // Trust the sum; some providers exclude cache tokens from total.
    return { ...u, totalTokens: computed };
  }
  return u;
}

Call reconcileUsage inside toTokenUsage after mapping. This prevents a provider’s accounting quirk from inflating totalCost via a stale total.

Step 7: Write tests to verify success

You verify the pipeline with two test layers: pure function tests and a mocked integration test. Below is a minimal Vitest example.

import { describe, it, expect } from 'vitest';
import { toTokenUsage, computeCost, isTokenUsage } from './client';

describe('token usage types', () => {
  it('maps raw usage with cache details', () => {
    const raw = {
      prompt_tokens: 10,
      completion_tokens: 5,
      total_tokens: 15,
      prompt_tokens_details: { cached_tokens: 4, cache_creation_tokens: 2 },
    };
    const u = toTokenUsage(raw);
    expect(u).toEqual({
      promptTokens: 10,
      completionTokens: 5,
      totalTokens: 15,
      cacheReadTokens: 4,
      cacheWriteTokens: 2,
    });
    expect(isTokenUsage(u)).toBe(true);
  });

  it('computes cost from pricing', () => {
    const u = toTokenUsage({ prompt_tokens: 1000, completion_tokens: 1000 });
    const cost = computeCost(u, 'gpt-4o');
    expect(cost.inputCost).toBeCloseTo(0.005);
    expect(cost.outputCost).toBeCloseTo(0.015);
    expect(cost.totalCost).toBeCloseTo(0.02);
  });
});

Run vitest run. Green tests confirm your typescript token usage cost types map and price correctly. For integration confidence, mock fetch to return a canned usage object and assert chat() resolves to a ChatResult with non-negative totalCost.

Verification checklist

  • tsc --noEmit passes with strict mode on.
  • Unit tests for mapping, cost, and guard are green.
  • A manual call against a staging endpoint logs usage and cost without undefined fields.
  • Billing export matches the sum of totalCost across a day’s requests within floating-point tolerance.

That last point is the real success criterion. If finance can reconcile without a spreadsheet macro, the types did their job.

Step 8: Extend for multi-provider routing

When you send routing directives or cache-control hints, the response usage may include provider-specific surcharges. Add a provider?: string field to CostBreakdown and branch pricing on it. Keep the core typescript token usage cost types unchanged; extend, don’t rewrite.

interface CostBreakdown {
  inputCost: number;
  outputCost: number;
  cacheReadCost: number;
  totalCost: number;
  currency: 'USD';
  provider?: string;
}

This keeps the client forward-compatible as you add models from the 240+ available behind an OpenAI-compatible gateway, without touching the token counting logic.

Build the types once, map ruthlessly, and let the compiler reject the invoices you’d otherwise write by hand.

Tagstypescripttypestoken-usagebilling

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All typescript typed llm api clients posts →