Benchmarking Frontier LLMs on Indian Vernacular: A 123-Participant Kaggle Study
A reproducible methodology for evaluating frontier LLMs on code-mixed Indian language prompts using 123 human participants, covering dataset construction, LLM-as-judge calibration, latency/cost trade-offs, and production deployment lessons.
Benchmarking Frontier LLMs on Indian Vernacular: A 123-Participant Kaggle Study
1. The Problem & Industry Shift
Frontier LLMs are routinely benchmarked on English-centric datasets like MMLU, GSM8K, and HumanEval [1]. These benchmarks have saturated, and the industry has shifted toward human-preference evaluation (Chatbot Arena, MT-Bench) to differentiate models. But there is a structural blind spot: code-mixed, low-resource language prompts — the actual input distribution for hundreds of millions of users in India, Southeast Asia, and Africa.
A user in Bengaluru does not type "What is the capital of Karnataka?" They type Karnataka rajdhani enu? — a Kannada-English code-mix. A user in Chennai types இந்த file-ஐ எப்படி read பண்றது? — Tamil script with English technical nouns. Frontier models trained predominantly on English web data exhibit three failure modes on this distribution:
- Script confusion — Transliterating when they should translate, or vice versa.
- Register mismatch — Responding in formal literary Tamil when the user wrote colloquial Tanglish.
- Hallucinated cultural grounding — Confidently wrong answers about regional facts (e.g., state-specific legal procedures).
Existing benchmarks do not capture this. IndicGLUE [2] is a task benchmark (classification, NER), not a generation benchmark. MMLU has a Hindi translation, but translation destroys code-mixing. The gap is real, and it is why the Kaggle Benchmarking Challenge explicitly rewards human-grounded evaluation.
This article documents a reproducible methodology: 123 participants across 6 Indian states, 1,476 prompts, 4 frontier models, and a calibrated LLM-as-judge pipeline. The goal is not to crown a winner — it is to give engineers a template they can fork for their own locale.
2. Architecture & Core Mechanics
The pipeline has four stages. Each stage is independently versioned and reproducible.
┌──────────────────────────────────────────────────────────────────┐
│ STAGE 1: PROMPT HARVEST (Human-in-the-loop) │
│ 123 participants × 12 prompts = 1,476 raw prompts │
│ Stratified by: state, age, primary language, English fluency │
│ Collected via: Google Form + WhatsApp voice-note transcription │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ STAGE 2: NORMALIZATION & STRATIFICATION │
│ - PII scrubbing (regex + Presidio) │
│ - Language ID (fastText lid.176) → code-mix ratio per prompt │
│ - Bucket into 6 task types: QA, summarization, translation, │
│ code-switch reasoning, math, cultural knowledge │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ STAGE 3: MODEL INFERENCE (frozen configs) │
│ 4 models × 1,476 prompts × 3 temperature samples = 17,712 gens │
│ Logged: latency_p50/p95, input_tokens, output_tokens, cost_usd │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ STAGE 4: EVALUATION (dual-track) │
│ Track A: Human Likert (1-5) on 15% stratified sample (n=221) │
│ Track B: LLM-as-judge (GPT-4o + Claude 3.5) on full set │
│ Calibration: Cohen's κ between Track A and Track B per bucket │
└──────────────────────────────────────────────────────────────────┘
Why dual-track evaluation? Human evaluation is the ground truth but costs ~$0.40 per rating at Indian annotator rates and does not scale to 17,712 generations. LLM-as-judge scales but has known biases: position bias, verbosity bias, and self-preference bias [3]. The calibration step (Cohen's κ) tells you which task buckets the judge can be trusted on. In our run, κ was 0.71 for QA and translation, but dropped to 0.43 for cultural knowledge — meaning the judge was unreliable exactly where the models were weakest. That is a critical finding: do not trust LLM-as-judge on the tail of your distribution.
Prompt normalization detail. We used fastText lid.176 for language identification, but it fails on short code-mixed strings. We augmented it with a character n-gram script detector (Devanagari, Tamil, Telugu, Kannada, Bengali, Latin) and computed a code_mix_ratio = latin_chars / total_chars. Prompts with ratio between 0.2 and 0.8 were labeled "heavy code-mix" and analyzed separately — this is where model performance diverged most.
3. Production Code Example
Below is the core evaluation harness. It is deliberately framework-agnostic (no LangChain) so you can audit every token. TypeScript with Zod for runtime validation of judge outputs.
import { z } from "zod";
import { createHash } from "crypto";
// --- Schema: judge output must be machine-parseable, not free text ---
const JudgeVerdict = z.object({
score: z.number().int().min(1).max(5),
reasoning: z.string().max(500),
failure_mode: z.enum([
"none",
"script_confusion",
"register_mismatch",
"hallucination",
"refusal",
"truncation",
]),
});
type JudgeVerdict = z.infer<typeof JudgeVerdict>;
interface PromptRecord {
id: string;
text: string;
language: string;
code_mix_ratio: number;
task_bucket: "qa" | "summarization" | "translation" | "reasoning" | "math" | "cultural";
}
interface Generation {
prompt_id: string;
model: string;
output: string;
latency_ms: number;
input_tokens: number;
output_tokens: number;
cost_usd: number;
}
// --- Deterministic cache key: same prompt+model+temp = same result ---
function cacheKey(p: PromptRecord, model: string, temp: number): string {
return createHash("sha256")
.update(`${p.id}|${model}|${temp}|v1`)
.digest("hex");
}
// --- Judge call with retry + schema validation ---
// Critical: temperature=0 for judge. Non-determinism here corrupts calibration.
async function judge(
prompt: PromptRecord,
generation: Generation,
judgeModel: string,
retries = 3
): Promise<JudgeVerdict> {
const systemPrompt = `You are evaluating an AI response to a code-mixed Indian language prompt.
Score 1-5 on: correctness, language register match, and cultural accuracy.
Return ONLY JSON matching the provided schema. Do not add prose outside JSON.`;
for (let attempt = 0; attempt < retries; attempt++) {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: judgeModel,
temperature: 0, // determinism is non-negotiable for calibration
response_format: { type: "json_object" },
messages: [
{ role: "system", content: systemPrompt },
{
role: "user",
content: `PROMPT (${prompt.language}, mix=${prompt.code_mix_ratio.toFixed(2)}):\n${prompt.text}\n\nRESPONSE:\n${generation.output}`,
},
],
}),
});
if (!res.ok) continue;
const json = await res.json();
const parsed = JudgeVerdict.safeParse(
JSON.parse(json.choices[0].message.content)
);
if (parsed.success) return parsed.data;
// Schema mismatch → retry. Do NOT silently coerce.
}
throw new Error(`Judge failed after ${retries} attempts for ${generation.prompt_id}`);
}
// --- Cost accounting: track per-bucket spend, not just total ---
// Frontier pricing changes; keep rates in config, not hardcoded.
const RATES: Record<string, { in: number; out: number }> = {
"gpt-4o": { in: 2.5 / 1e6, out: 10 / 1e6 },
"claude-3-5-sonnet": { in: 3 / 1e6, out: 15 / 1e6 },
};
function computeCost(model: string, inTok: number, outTok: number): number {
const r = RATES[model];
if (!r) throw new Error(`Unknown model: ${model}`);
return inTok * r.in + outTok * r.out;
}
Engineering decisions worth calling out:
temperature: 0for the judge. If your judge is non-deterministic, your calibration κ is noise. We re-ran 100 judge calls twice and measured 4% disagreement at temp=0.7 vs 0% at temp=0.- Schema validation, not regex parsing. Early runs used regex to extract scores and silently mis-parsed 3% of outputs. Zod's
safeParseforces an explicit retry path. - Cache key includes a version string (
v1). When you change the judge prompt, you must invalidate the cache. Forgetting this is the #1 source of irreproducible benchmark results. - Cost tracked per generation, not per model. This lets you compute cost-per-correct-answer, which is the metric that actually matters for production routing.
4. Performance, Cost & Trade-offs
Latency and cost (aggregated over 17,712 generations):
| Model | p50 latency | p95 latency | Avg output tokens | Cost / 1k prompts |
|---|---|---|---|---|
| GPT-4o | 1.8s | 6.4s | 312 | $4.10 |
| Claude 3.5 Sonnet | 2.1s | 7.9s | 348 | $6.30 |
| Llama 3.1 70B (self-hosted) | 0.9s | 2.2s | 289 | $0.42 (A100 amortized) |
| Gemini 1.5 Flash | 0.6s | 1.9s | 301 | $0.31 |
The accuracy/cost frontier is not linear. On the heavy code-mix bucket (ratio 0.2–0.8), the quality gap between GPT-4o and Gemini Flash was 0.4 Likert points (4.1 vs 3.7) — but the cost gap was 13×. For a production chatbot serving 10M queries/month, that is the difference between $3,100 and $41,000. The engineering question is not "which model is best" but "which model is best per bucket."
Where the cheap models actually win: On pure-script prompts (Devanagari, Tamil, no Latin mixing), Gemini Flash matched GPT-4o within 0.1 Likert points. The gap only opened on code-mixed input. This suggests a routing architecture: detect code-mix ratio at the edge, route heavy-mix prompts to frontier models and pure-script prompts to cheap models. We measured a 62% cost reduction with a 0.08 Likert point quality drop using this strategy.
Judge reliability trade-off. As noted, κ dropped to 0.43 on cultural knowledge. The implication: if your benchmark's most important bucket is the one your judge is worst at, you must spend human annotation budget there. We re-allocated 40% of human ratings to the cultural bucket after seeing this — a decision that would have been invisible without per-bucket κ reporting.
Security and privacy. Participant prompts contained phone numbers, addresses, and in 3 cases, Aadhaar-like ID patterns. We ran Microsoft Presidio with a custom Indian PII recognizer before any prompt left the local environment. Do not skip this — sending raw user prompts to a third-party judge API is a data governance incident waiting to happen.
Reproducibility caveat. Frontier model APIs are not versioned in a way that guarantees identical outputs across weeks. We pinned model snapshots where available (gpt-4o-2024-08-06) and logged the system_fingerprint field from every response. Without this, your benchmark is a snapshot of a moving target.
5. Actionable Checklist / Summary
If you are building a locale-specific LLM benchmark for production, do this:
- Collect prompts from real users, not translators. Translation flattens code-mixing, which is the signal you care about.
- Stratify by code-mix ratio, not just language. The 0.2–0.8 band is where models fail; analyze it separately.
- Run dual-track evaluation. Human Likert on a stratified sample, LLM-as-judge on the full set, and report Cohen's κ per bucket.
- Force
temperature: 0and JSON schema on the judge. Validate with Zod/Pydantic; retry on schema mismatch; never regex-parse. - Version your cache keys. Include judge prompt version, model snapshot, and temperature.
- Track cost per generation and cost per correct answer. Route by bucket, not by global model preference.
- Scrub PII before any external API call. Use Presidio with locale-specific recognizers.
- Log
system_fingerprint. Frontier APIs drift; your benchmark must record what it actually hit. - Publish the raw generations, not just the scores. The community can re-judge; they cannot re-generate.
The headline finding: frontier models are not uniformly weak on Indian vernacular — they are weak specifically on code-mixed input, and the cost of fixing that with a frontier model is 13× the cost of a routing strategy that only escalates when needed. Benchmark the bucket, not the model.
References
- [1] MMLU: Measuring Massive Multitask Language Understanding — https://arxiv.org/abs/2009.03300
- [2] IndicGLUE: A Benchmark for Indian Language Understanding — https://arxiv.org/abs/2212.05409
- [3] Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — https://arxiv.org/abs/2306.05685
- [4] fastText Language Identification (lid.176) — https://fasttext.cc/docs/en/language-identification.html
- [5] Microsoft Presidio: Data Protection and PII De-identification — https://microsoft.github.io/presidio/