Skip to content
TutujinAPI

LLM evaluation and benchmarking

An LLM evaluation freezes the question set and prompt template, then has every model under test answer each question several times under the same conditions: questions with an answer key are scored by rule, and open-ended answers are scored against a rubric by a judge model from another vendor. Then you compare each model’s score, latency and cost. One API key calls every model under test. For two full runs a month, each with 1,000 questions and two models answering every question 3 times, at live prices in each model’s cheapest group, model costs are about ¥776 a month, or about ¥388 per run.

Prices updated Nov 13, 2026, 6:13 PM

Monthly cost estimate

Assumptions: two full runs a month; each run has 1,000 questions, and two models under test answer every question 3 times at about 800 input and 1,200 output tokens (including reasoning tokens); half the questions are open-ended, and the judge scores each answer at about 2,000 input tokens (rubric, question, reference and answer) and 200 output tokens.

¥775.68per month

  • Under test: flagship model¥624
  • Under test: mainstream and small model¥94.08
  • Judge-model scoring¥57.6
See the usage assumed

The business problem

Choosing a model, writing a paper or picking a vendor all mean comparing several models’ quality, cost and speed on the same questions. Public leaderboards use questions that are not your task, scored by others with their own prompts and parameters, so the numbers rarely transfer.

Running your own evaluation is hard in the details: APIs and parameters differ between vendors, and reasoning models often reject temperature; the version behind a model name changes; thousands of requests sent at once hit rate limits; and open-ended answers need a person or a judge model to score them, whose scores must hold up too.

One OpenAI-compatible API calls every model under test, so the same script changes only the model parameter. Give each evaluation run its own API keys, and the console’s Logs filtered by them show what the run cost and the tokens and latency of every request.

Recommended models

Recommended models are picked from the live catalog; prices are each model's cheapest group, updated Nov 13, 2026, 6:13 PM. Click a model to see every group's price.

Reference architecture

  1. 1

    Freeze the test set

    Keep questions, answer keys or rubrics and the prompt template as numbered JSONL under version control with the script, and do not change them during the evaluation. Public benchmark questions may be in a model’s training data, so add some new questions of your own that have never been published.

  2. 2

    Fix the conditions

    Use one system prompt for every model; set temperature 0 and a fixed seed where supported, and drop and record them where not. Give the run its own API keys: a quota cap equal to the run’s budget, and model limits that allow only the models the run calls. The group decides the line and the price, so keep each model on one group for the whole run and check the group of every record in Logs afterwards.

  3. 3

    Log every call

    Record the requested model ID, the model named in the response, the parameters actually sent, call time, latency, finish_reason, usage and raw output. Before starting, save the list from /v1/models of the models this key can call.

  4. 4

    Score by rule first

    Multiple-choice, numeric and exact-match short answers are scored in code against the key: stable, reproducible and free.

  5. 5

    Judge-model scoring

    Open-ended answers are scored against a written rubric and a reference answer, with model names hidden; for pairwise comparisons, judge each pair twice with the order swapped. Have people score about 100 answers independently and compare them with the judge; use it only once agreement meets the standard of your field.

  6. 6

    Limit concurrency and summarize

    Cap requests in flight with a semaphore, start at 8 and adjust by the share of 429 responses; the SDK retries with backoff, and questions that still fail are rerun, not dropped from the statistics. Summarize each model’s mean score and spread, latency and tokens; the run’s total cost is Spend in period in Logs, filtered by the run’s key names.

Cost estimate

Assumptions: two full runs a month; each run has 1,000 questions, and two models under test answer every question 3 times at about 800 input and 1,200 output tokens (including reasoning tokens); half the questions are open-ended, and the judge scores each answer at about 2,000 input tokens (rubric, question, reference and answer) and 200 output tokens.

ItemModelUsage assumedCost
Under test: flagship modelgpt-5.56,000 × (800 input + 1,200 output tokens)¥624
Under test: mainstream and small modeldeepseek-v4.1-flash6,000 × (800 input + 1,200 output tokens)¥94.08
Judge-model scoringClaude Sonnet 5.56,000 × (2,000 input + 200 output tokens)¥57.6
Total (per month)¥775.68

Calculated from live prices in each model's cheapest group, updated Nov 13, 2026, 6:13 PM.

Model usage only; human scoring is not included. Each additional model under test adds a line at its own price, and twice the questions or repeats means roughly twice the cost. Reasoning tokens vary widely between models, so run 50 questions first and extrapolate from the real usage. The estimate uses each model’s cheapest group, which may include mixed sources; when results go into a paper and you need to state where the model came from, use groups labeled Official, which cost more; see the group prices on each model’s page.

Sample code

A semaphore caps concurrency and every model answers each question 3 times; answers with a key are scored by rule, and open-ended answers go to the judge with the model name hidden. Models that reject temperature get it dropped automatically, and the parameters actually sent and any failed questions are written to the results file.

Sample code (Python, concurrent evaluation with rule and judge scoring)
import asyncio
import json
import os
import re
import time

from openai import AsyncOpenAI, BadRequestError

client = AsyncOpenAI(base_url="https://www.tutujin.com/v1", api_key=os.environ["TUTUJIN_API_KEY"], max_retries=4)
MODELS = ["gpt-5.5", "deepseek-v4.1-flash"]
JUDGE = "claude-sonnet-5-5"  # from a vendor that is not under test
REPEATS = 3
SYSTEM = "Answer the question. End with a last line of the form 'Answer: <answer>'."
limit = asyncio.Semaphore(8)  # requests in flight; raise it slowly while watching for 429


async def ask(model, messages, params):
    async with limit:
        started = time.time()
        try:
            resp = await client.chat.completions.create(model=model, messages=messages, **params)
        except BadRequestError:
            # many reasoning models reject temperature: retry without it and keep what was really sent
            params = {k: v for k, v in params.items() if k != "temperature"}
            resp = await client.chat.completions.create(model=model, messages=messages, **params)
        return resp, params, round(time.time() - started, 2)


async def judge(item, answer):
    """Open-ended answers: 0-5 against a written rubric, without the name of the model that wrote them."""
    prompt = (
        f"Question:\n{item['prompt']}\n\nReference answer:\n{item['reference']}\n\nRubric:\n{item['rubric']}\n\n"
        f"Answer to score:\n{answer}\n\n"
        'Score the answer from 0 to 5 by the rubric only. Reply with JSON only: {"score": 0, "reason": ""}'
    )
    resp, _, _ = await ask(JUDGE, [{"role": "user", "content": prompt}], {"temperature": 0})
    raw = resp.choices[0].message.content
    return json.loads(raw[raw.find("{"): raw.rfind("}") + 1])["score"] / 5


async def run_one(model, item, rep):
    messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": item["prompt"]}]
    resp, params, latency = await ask(model, messages, {"temperature": 0, "seed": 7})
    text = resp.choices[0].message.content or ""
    if "answer" in item:  # questions with a key are scored by rule
        m = re.search(r"Answer:\s*(.+?)\s*$", text)
        score = float(bool(m) and m.group(1).strip(" *.").lower() == item["answer"].lower())
    else:
        score = await judge(item, text)
    return {
        "item": item["id"], "repeat": rep, "model": model, "returned_model": resp.model, "params": params,
        "score": score, "scored_by": "rule" if "answer" in item else JUDGE, "latency_s": latency,
        "finish_reason": resp.choices[0].finish_reason, "usage": resp.usage.model_dump() if resp.usage else None,
        "called_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "output": text,
    }


async def main(path="testset.jsonl"):
    items = [json.loads(line) for line in open(path, encoding="utf-8")]
    jobs = [(m, it, r) for m in MODELS for it in items for r in range(REPEATS)]
    results = await asyncio.gather(*(run_one(*j) for j in jobs), return_exceptions=True)
    with open("results.jsonl", "a", encoding="utf-8") as f:
        for (m, it, r), res in zip(jobs, results):
            row = res if isinstance(res, dict) else {"item": it["id"], "repeat": r, "model": m, "error": repr(res)}
            f.write(json.dumps(row, ensure_ascii=False) + "\n")
    for m in MODELS:
        ok = [x for x in results if isinstance(x, dict) and x["model"] == m]
        failed = sum(1 for (jm, _, _), x in zip(jobs, results) if jm == m and not isinstance(x, dict))
        print(m, "mean score", round(sum(x["score"] for x in ok) / max(len(ok), 1), 3),
              "| mean latency", round(sum(x["latency_s"] for x in ok) / max(len(ok), 1), 2), "s",
              "| failed, rerun these:", failed)


asyncio.run(main())

Try this with one key

Sign up, create an API key in the console, and put your base_url and key into the code above. For company purchasing or invoices, add our WeChat support JsnonoChat.

Tutujin support WeChat QR code
WeChatJsnonoChat

Scan the code or search the WeChat ID to add us

FAQ

What does one full evaluation run cost?

For 1,000 questions, two models answering each question 3 times and the judge scoring half of them, the cheapest groups come to about ¥388 per run. Cost is driven by questions × models × repeats × output length, and the reasoning tokens of flagship models are often the largest part.

Can I trust the judge model’s scores?

Not without checking. Known biases include favoring longer answers, being swayed by the order answers appear in, possibly favoring models from its own vendor, and misjudging specialist content. Write a specific rubric, hide model names, judge pairs twice with the order swapped, and measure agreement on a sample scored by people; if a question can be scored by rule, do not send it to the judge.

With temperature 0 and a seed, are results fully reproducible?

No. A seed only brings outputs for the same input closer together and does not guarantee identical results, and reasoning models usually do not support temperature. Record the parameters actually sent, run each question several times and report the mean and spread rather than a single run.

How much concurrency can I use? Will I be rate-limited?

Start at 8 concurrent requests and raise it while watching the share of 429 responses; back off and retry on 429, and record questions that still fail and rerun them instead of dropping them from the statistics. Different groups use different lines, so rate limits can differ too. For sustained higher concurrency, add our WeChat support JsnonoChat with the models, groups and expected request volume, and we will assess it and reply.

How do I report which model I used in a paper?

Give the full model ID (names with a date suffix usually point to a fixed snapshot), the date range of the calls, the group and parameters, the prompt template, the number of repeats and the scoring method, and keep the model name returned in each response. The version behind a model name can change, so record everything again when you rerun later.

Can the model comparison pages replace an evaluation?

No. The comparison pages list facts such as prices, input and output types, endpoints and listing dates, with no quality scores; quality has to be measured on your own test set.