If you're relying on strlen() or basic word counts to guess your OpenAI bill, you're setting money on fire. A single user input containing raw JSON, CJK characters, or indented code can turn a predicted 500-token query into a 3,000-token request. When your application runs thousands of requests an hour, that estimation gap turns into hundreds of extra dollars on your monthly invoice.
Controlling your llm token counting cost requires exact counting before dispatching requests, hard context budget guards, and truncation strategies that trim bloated inputs without stripping essential instructions.
The Math Behind Tokenization Inflation
Large Language Models don't see words or characters; they see token identifiers produced by Byte Pair Encoding (BPE). Pricing is calculated per thousand or per million input and output tokens. The issue is that string length and token count do not scale linearly across different content types.
Consider a standard URL string like https://api.example.com/v2/users/search?query=laravel&limit=50. That string is 67 characters long. In standard English text, 67 characters is roughly 12 words, which usually translates to about 15 tokens. Under OpenAI's cl100k_base tokenizer, that URL takes 22 tokens because punctuation symbols, slashes, and query parameters get split into individual fragments. Multi-byte UTF-8 sequences for non-English languages are even worse: a 10-character Japanese sentence can easily consume 25 to 30 tokens.
Model architectures also dictate tokenizer behavior. Models like gpt-4 use cl100k_base, whereas gpt-4o uses o200k_base. The expanded 200,000 token vocabulary in o200k_base reduces token consumption for code and non-English text by roughly 15% to 20%. However, if your counting utility assumes the older vocabulary while sending prompts to newer models, your internal metrics will be flat-out wrong.
Token Counting in PHP 8.3 and Laravel 12
Running token calculations directly inside your Laravel backend allows you to intercept oversized prompts before they reach the API provider. Rather than relying on approximate HTTP response callbacks after paying for the request, you compute the payload cost in your middleware or service classes.
In PHP 8.3, native BPE implementations like ccronk/tiktoken-php provide fast token lookup. Here is a production-grade service implementation in Laravel 12 that enforces strict token budgets on incoming prompt payloads before invoking an OpenAI client.
namespace App\Services;
use App\Exceptions\TokenBudgetExceededException;
use Tiktoken\EncoderProvider;
class PromptBudgetGuard
{
private mixed $encoder;
public function __construct(string $model = 'gpt-4o')
{
$provider = new EncoderProvider();
// gpt-4o uses o200k_base, gpt-4 uses cl100k_base
$this->encoder = $provider->getForModel($model);
}
public function count(string $text): int
{
return count($this->encoder->encode($text));
}
public function validateAndBuild(
string $systemPrompt,
string $userMessage,
int $maxInputBudget = 3000
): array {
$systemTokens = $this->count($systemPrompt);
$userTokens = $this->count($userMessage);
// 4 tokens overhead per message format wrapper
$totalTokens = $systemTokens + $userTokens + 8;
if ($totalTokens > $maxInputBudget) {
throw new TokenBudgetExceededException(
"Input of {$totalTokens} tokens exceeds the budget cap of {$maxInputBudget}."
);
}
return [
'tokens_used' => $totalTokens,
'payload' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userMessage],
],
];
}
}This service counts the token footprint of system instructions alongside user input while accounting for OpenAI's message envelope overhead. If a prompt exceeds 3,000 tokens, it immediately throws a domain exception, preventing an expensive HTTP call.
Edge Guards and Truncation in Next.js 16
In client-heavy applications built with Next.js 16 and React 19, token counting often happens in Server Actions or Route Handlers before hitting LLM APIs. Using js-tiktoken inside Server Actions gives you tight control over context length.
When context grows too large, naive string slicing with substring(0, maxChars) breaks code blocks, cuts mid-sentence, and splits UTF-8 byte sequences. Instead, you need a token-aware sliding window that keeps full system prompts intact while discarding the oldest conversation turns first.
'use server';
import { getEncoding } from 'js-tiktoken';
interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface PruneResult {
messages: Message[];
tokenCount: int;
}
export async function pruneContextWindow(
systemPrompt: string,
history: Message[],
maxBudget: number = 4000
): Promise<PruneResult> {
const enc = getEncoding('o200k_base');
const systemTokens = enc.encode(systemPrompt).length + 4;
let availableBudget = maxBudget - systemTokens;
if (availableBudget <= 0) {
throw new Error('System prompt exceeds total allocated token budget.');
}
const selectedHistory: Message[] = [];
// Process recent messages first to keep latest context
for (let i = history.length - 1; i >= 0; i--) {
const msg = history[i];
const msgTokens = enc.encode(msg.content).length + 4;
if (availableBudget - msgTokens < 0) {
break; // Stop adding older context once budget is filled
}
availableBudget -= msgTokens;
selectedHistory.unshift(msg);
}
return {
messages: [
{ role: 'system', content: systemPrompt },
...selectedHistory,
],
tokenCount: maxBudget - availableBudget,
};
}This Server Action prioritizes fresh conversation state. It reserves space for system rules first, then steps backward through history, packing as many intact messages as fits inside your specified token threshold.
Meaning-Preserving Truncation Strategies
Discarding old messages works well for chat, but summarizing long document contexts requires structural truncation. Slicing raw text randomly often destroys the semantic meaning required for the model to generate accurate answers.
Structural Markdown Boundary Truncation
When trimming single large documents, break text on structural boundaries instead of arbitrary character counts. Split documents by headers, paragraph breaks (\n\n), or code fence boundaries. Encode each structural block individually, evaluate its weight, and append blocks until reaching the token limit.
Protecting Core System Instructions
Never truncate system prompts dynamically. If your system prompt contains key constraints like "Return output strictly as JSON" and it gets trimmed due to space pressures, the model output will break client-side parsers. Treat system prompt length as a static, non-negotiable deduction from your context window calculation.
Soft Caps and Fallback Model Routing
Rather than dropping data, use a tiered routing strategy based on your pre-calculated token count. If a user query in Laravel stays under 1,000 tokens, dispatch it to gpt-4o for maximum reasoning quality. If context grows to 8,000 tokens, automatically route the request to gpt-4o-mini, which drops input cost by over 90% while keeping the expanded context intact.
Production Gotchas
Implementing client-side or server-side token management comes with subtle edge-case traps:
- WASM Memory Overhead in Edge Runtimes: Importing heavy WebAssembly tokenizers into Next.js Edge routes can cause cold start delays or exceed Vercel's 4MB Edge function size limit. Prefer pure JavaScript implementations like
js-tiktokenor run token checks inside standard Node.js Server Actions. - BPE Encoding Mismatches: Tokenizers are model-specific. Counting tokens for
gpt-4ousing acl100k_baseencoder underestimates token counts by up to 15% on code snippets, resulting in unexpected budget overruns. - Chat ML Template Wrapping Overhead: System and message roles add extra wrapper tokens behind the scenes. OpenAI adds
<|im_start|>and<|im_end|>tokens for every message in an array. Always add a 4-token padding per message item to your strict guard calculations.














