LLM API Cost Control: Choosing Models by Task Tier
A practical framework for controlling LLM API costs by matching lightweight, standard, and flagship models to task complexity. It covers common billing traps such as caching, output length, tool calls, and retries, and outlines a selection workflow validated with real logs and samples.
1. Most Cost Overruns Start with a Mismatch Between Model Tier and Task
How should teams choose LLM APIs, reduce costs, and identify common billing traps? Once usage grows, most development teams run into these questions. This article presents a process for selecting models by task tier, identifying billing traps, and establishing a monitoring loop. It does not promise a fixed savings percentage. Unit prices vary by provider, model, and time, so actual results should be validated against your own logs and POC tests.
A common cause of cost overruns is default assumptions: routing every request to the most capable model, or assuming that a higher price always means more reliable results. Neither assumption is well founded. Teams should first review the pricing table for their provider, then discuss model selection.
Models can usually be divided into three tiers:
- Lightweight tier: Suitable for low-complexity tasks such as text classification, keyword extraction, format validation, and simple rewriting. It offers fast responses and low unit prices, making it suitable for high-frequency calls.
- Standard tier: Suitable for medium-complexity work such as function writing, code review, unit test generation, local refactoring, and document interpretation. It is the main tier for daily development.
- Flagship tier: Suitable for tasks such as cross-module refactoring, difficult defect localization, system architecture design, and multi-document analysis. The cost per request is high, and using it for simple requests often provides a poor return.
To assess the actual situation, teams need to analyze request logs from the last 30 days and record the number of requests, input tokens, output tokens, and cost by task type. Only with this distribution can teams determine which requests are worth upgrading to a higher tier.
2. Billing Traps: Caching, Output Length, and Tool Calls
Caching. Some providers offer cached billing for repeated prompt prefixes, but the conditions for a cache hit differ by provider, including whether the prefix must match exactly, the minimum length, and the expiration time. Fixed system prompts and reference documents must be placed at the front of the prompt for a cache hit to be possible. Logs must record the number of cached tokens; otherwise, the effect cannot be verified.
Output length. Most providers charge a higher unit price for output tokens than for input tokens. Without an output limit, the model may generate long passages unrelated to the task. Set max_tokens by task type and require the model to return a fixed structure, such as JSON.
Tool calls. Every tool result is sent back as input in subsequent requests and billed again. Looping tool calls or placing an entire file in a tool return value can rapidly increase costs. Set a maximum number of call rounds and return only the fields required by the task.
Retries. Retrying immediately after a timeout can result in duplicate billing. Before retrying, determine whether the request is idempotent, and use exponential backoff. When failures continue, route the request to human handling instead of retrying indefinitely.
3. Model Tier Selection Workflow
Focus on four dimensions during model selection. Each dimension should be validated with real samples:
| Dimension | Question to answer | Preferred tier | Validation method |
|---|---|---|---|
| Task goal | Is the goal to generate or refactor content, or only to extract information? | Generation and refactoring tend toward the standard tier or above | Compare output quality on 50 real samples |
| Context size | Does the input token count exceed the threshold? | For keyword lookup in long text, the lightweight tier is often sufficient | Compare accuracy across tiers on long-text retrieval |
| Error tolerance | Could an error cause a production incident or financial loss? | Use a low-cost model for high tolerance; use the flagship tier or human review for low tolerance | Measure error rate and review cost |
| Output format compliance | Must the output conform to JSON, tables, or another fixed format? | The lightweight tier requires prompt constraints and programmatic validation | Track the format validation failure rate |
The pseudo-code below outlines a selection and invocation skeleton. Replace the model names, thresholds, and validation logic with your team’s own configuration:
import loggingfrom dataclasses import dataclass, fieldfrom typing import Callablelogger = logging.getLogger("model_router")# Replace model names with the provider's actual model IDsLITE = "lite-model"STANDARD = "standard-model"FLAGSHIP = "flagship-model"TIER_UP = {LITE: STANDARD, STANDARD: FLAGSHIP, FLAGSHIP: FLAGSHIP}SIMPLE_KINDS = {"extract", "classify", "format_check"}MID_KINDS = {"rewrite", "generate", "review", "unit_test", "doc_read"}COMPLEX_KINDS = {"refactor", "architecture", "debug_hard", "multi_doc"}VALID_TOLERANCE = {"high", "medium", "low"}# These thresholds are starting values and need calibration with baseline testsLITE_MAX_INPUT_TOKENS = 8000FLAGSHIP_MIN_FILES = 5@dataclassclass Task:kind: str # Task type; see the three sets aboveinput_tokens: int # Input tokens calculated with the provider tokenizererror_tolerance: str # high / medium / lowfiles_touched: int = 1 # Number of files that need coordinated changesdef __post_init__(self):if self.input_tokens < 0 or self.files_touched < 0:raise ValueError("input_tokens and files_touched cannot be negative")if self.error_tolerance not in VALID_TOLERANCE:raise ValueError(f"Unknown error_tolerance: {self.error_tolerance}")def select_model(task: Task) -> str:if task.kind not in SIMPLE_KINDS | MID_KINDS | COMPLEX_KINDS:logger.warning("Unknown task type %s; using the standard tier", task.kind)return STANDARD# Simple task, some errors tolerated, short input: lightweight tierif (task.kind in SIMPLE_KINDSand task.error_tolerance != "low"and task.input_tokens <= LITE_MAX_INPUT_TOKENS):...
