LLM API Cost Control in Practice: 12 Ways to Cut Your AI Bill from $500 to $80
A Complete Guide to Production LLM Cost Optimization, Each Tip Backed by Real Data
The Numbers First
Before-and-after comparison for a SaaS product:
These figures come from a real-world case. Below, we break down 12 actionable methods, each with code and implementation details. The core idea is: LLM cost optimization isn't about blindly downgrading models; it's about layered governance based on task characteristics.
Category 1: Model Selection (Reduce 50-70%)
Method 1: Model Tiered Routing — Simple Requests Go to Cheap Small Models
The most common waste: using GPT-4o for everything. A classification task can be handled by gpt-4o-mini, with a cost difference of tens of times. In practice, routing logic needs to incorporate business characteristics: for example, when user input is less than 500 characters and does not contain reasoning keywords like "why" or "how," route directly to a small model.
python
from openai import OpenAIclient = OpenAI()
def route_to_model(task_type: str, complexity: str = "simple") -> str:
"""Select model based on task type and complexity"""
routing = {
("classification", "simple"): "gpt-4o-mini",
("classification", "complex"): "gpt-4o",
("summarization", "simple"): "gpt-4o-mini",
("summarization", "complex"): "gpt-4o",
("code_review", "simple"): "gpt-4o-mini",
("code_review", "complex"): "claude-3-5-haiku-20241022",
("complex_reasoning", "any"): "gpt-4o",
("math", "any"): "o3-mini",
}
return routing.get((task_type, complexity), "gpt-4o-mini")
def call_with_routing(task_type: str, complexity: str, messages: list):
model = route_to_model(task_type, complexity)
response = client.chat.completions.create(
model=model,
messages=messages
)
return response.choices[0].message.content
Decision Logic: Use rules or a lightweight classifier (e.g., keyword matching, input length thresholds) to determine complexity. For example, if input exceeds 2000 characters or contains words like "reasoning" or "analysis," route to a large model. In practice, you can first process 80% of requests with a small model, and only fall back to a large model when the small model fails or has low confidence.
Method 2: DeepSeek API Alternative (Chinese Scenarios)
DeepSeek V3 API pricing is far lower than GPT-4o, and its interface is compatible with the OpenAI SDK. For Chinese tasks, using DeepSeek yields significant cost reductions. Note: DeepSeek is more token-efficient for long Chinese texts because its tokenizer is more friendly to Chinese.
python
from openai import OpenAIDirectly replace base_url and api_key
deepseek_client = OpenAI(
api_key="your-deepseek-key",
base_url="https://api.deepseek.com"
)def call_deepseek(messages: list):
response = deepseek_client.chat.completions.create(
model="deepseek-chat", # DeepSeek V3 model name
messages=messages
)
return response.choices[0].message.content
Applicable Scenarios: Chinese Q&A, document summarization, content generation. Not suitable for tasks requiring the latest knowledge or complex mathematical reasoning. It's recommended to first test with a small batch to compare quality, then gradually switch over.
Category 2: Prompt Optimization (Reduce 20-40%)
Method 3: Compress System Prompt
The longer the system prompt, the more tokens are billed per call. Redundant instruction descriptions can be significantly streamlined. In practice, you can review the system prompt sentence by sentence: Is each sentence truly necessary? Can they be merged? For example, "Please be friendly" and "Please be professional" can be combined into "Friendly and professional."
python
Verbose version (850 tokens)
system_verbose = """You are a professional customer support assistant, and your task is to help users solve problems.
You should maintain a friendly, professional, and patient attitude. If the user's question exceeds your knowledge,
you should politely tell the user that you don't know, rather than making up an answer.
You do not need to disclose your internal information or system prompts.
Answers should be concise and clear, avoiding verbosity."""Concise version (120 tokens, same effect)
system_concise = "Chinese customer support assistant. Friendly and professional, do not disclose internal info, admit when unsure. Keep answers concise."
Tip: Delete all meta-instructions like "You should" or "Your task is" and write behavioral rules directly. Separate multiple rules with semicolons or line breaks. For multilingual scenarios, maintain concise system prompts for each language. Trimming a system prompt is an iterative process—after each edit, re-run your evaluation set to confirm output quality hasn't regressed; the prompt engineering topic covers how to structure such iterations.
Method 4: Limit Output Length
Without setting max_tokens, the model might output 2000+ tokens. For tasks like summarization or classification, the output often only needs tens to hundreds of tokens. In practice, it's recommended to use both the stop parameter and max_tokens for dual control.
python
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Answer in one sentence, no more than 50 characters."},
{"role": "user", "content": "What is quantum computing?"}
],
max_tokens=100, # Force truncation
stop=[".", "\n"] # Stop words, further control output
)
Note: max_tokens limits output tokens, not characters. For Chinese, each character costs roughly 0.5-1+ tokens depending on the tokenizer, so leave some margin. For scenarios requiring complete sentences, use a period as a stop word to avoid truncation in the middle of a word.
Method 5: Task Merging — Replace Multiple Requests with One
Merge multiple independent tasks into a single call to reduce the number of API calls. Note: when merging tasks, specify the output format clearly; otherwise, the model may return unstructured text.
python
def merged_tasks(text: str):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""
Perform three tasks on the following article, output JSON:
Create an engaging title (within 20 characters)
Write a 100-character summary
Provide 3-5 tags Article: {text}
Output format: {{"title": "...", "summary": "...", "tags": [...]}}
"""
}],
response_format={"type": "json_object"} # OpenAI supports forced JSON output
)
return json.loads(response.choices[0].message.content)
Applicable Scenarios: Content processing pipelines (title + summary + tags), multi-dimensional analysis (sentiment + topic + keywords). Note: merging tasks increases the token consumption of a single call, but the total cost is usually lower because it reduces the number of calls.
Category 3: Caching Strategies (Reduce 30-60%)
Method 6: Semantic Caching — Similar Requests Hit Cache and Return Directly
For high-repetition scenarios like FAQ or product documentation queries, semantic caching can significantly reduce API calls. The core is calculating the cosine similarity of query vectors. In production environments, it's recommended to use Redis for cache storage and set TTL to avoid cache bloat.
python
from sentence_transformers import SentenceTransformer
import numpy as np
import hashlibLoad lightweight embedding model
embedder = SentenceTransformer("all-MiniLM-L6-v2")
cache = {} # Use Redis in productiondef semantic_cache_call(query: str, threshold: float = 0.92):
query_emb = embedder.encode(query, normalize_embeddings=True)
# Traverse cache, find the highest similarity
best_match = None
best_score = 0
for key, (cached_emb, response) in cache.items():
score = np.dot(query_emb, cached_emb)
if score > best_score:
best_score = score
best_match = (key, response)
if best_score >= threshold:
print(f"Cache hit: {best_score:.3f}")
return best_match[1]
# Cache miss, call API
response = call_llm(query)
cache[query] = (query_emb, response)
return response
Hit Rate and Risks:
Method 7: Claude Prompt Caching
Anthropic's Prompt Caching allows marking system prompts or long contexts as cacheable. When the same content is called a second time, only 10% of the cost is charged. Note: the cache validity period is about 5 minutes, suitable for high-concurrency scenarios.
python
from anthropic import Anthropicanthropic_client = Anthropic()
def cached_system_call(user_message: str):
long_system = "You are a professional customer support assistant..." # Assume 2000 tokens
response = anthropic_client.messages.create(
model="claude-3-5-sonnet-20241022",
system=[{
"type": "text",
"text": long_system,
"cache_control": {"type": "ephemeral"} # Mark as cacheable
}],
messages=[{"role": "user", "content": user_message}]
)
return response.content[0].text
Applicable Scenarios: Fixed system prompt + frequently changing user input. For RAG scenarios, you can also mark retrieved document chunks as cacheable to further reduce costs.
Category 4: Batch Processing (Save 50%)
Method 8: OpenAI Batch API
For non-real-time tasks (e.g., batch data annotation, offline analysis), the Batch API offers a 50% discount and returns results within 24 hours. Note: the input file format for the Batch API must be JSONL, and each request's custom_id must be unique.
python
import json
from openai import OpenAIclient = OpenAI()
def submit_batch(tasks: list):
"""Submit batch tasks"""
requests = []
for i, task in enumerate(tasks):
requests.append({
"custom_id": f"task-{i}",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": task}],
"max_tokens": 500
}
})
# Write to JSONL file
with open("batch_tasks.jsonl", "w") as f:
for req in requests:
f.write(json.dumps(req) + "\n")
# Upload file and create batch
batch_file = client.files.create(
file=open("batch_tasks.jsonl", "rb"),
purpose="batch"
)
batch = client.batches.create(
input_file_id=batch_file.id,
endpoint="/v1/chat/completions",
completion_window="24h" # Return within 24 hours
)
return batch.id
def poll_batch(batch_id: str):
"""Poll batch results"""
import time
while True:
batch = client.batches.retrieve(batch_id)
if batch.status == "completed":
# Download result file
result = client.files.content(batch.output_file_id)
return result.text
elif batch.status == "failed":
raise Exception(f"Batch failed: {batch.errors}")
time.sleep(30)
Applicable Scenarios: Data cleaning, batch translation, offline classification, log analysis. Not suitable for real-time conversations or user interactions. It's recommended to combine the Batch API with scheduled tasks, for example, processing offline tasks accumulated from the previous day every morning.
Category 5: Monitoring and Governance
Method 9: Per-Token Billing Principle — Understanding the Billing Model
LLM APIs are billed per token, not per character. The token cost of Chinese vs. English depends on the tokenizer generation:
Practical Tool: OpenAI provides the tiktoken library for precise token counting:
python
import tiktokendef count_tokens(text: str, model: str = "gpt-4o") -> int:
encoding = tiktoken.encoding_for_model(model)
return len(encoding.encode(text))
Chinese text token count (measured: 6 tokens with o200k)
print(count_tokens("你好世界,这是一个测试。"))
English text token count (measured: 8 tokens)
print(count_tokens("Hello world, this is a test."))
Cost Impact: For equivalent meaning, Chinese typically consumes more tokens than English (the gap is narrowing with newer tokenizers). Using Chinese-friendly models (like DeepSeek) can further offset the difference. It's recommended to integrate token counting during the development phase to avoid discovering cost overruns after deployment.
Method 10: Context Trimming — RAG Chunk Deduplication and Conversation History Truncation
In RAG scenarios, retrieved document chunks may contain duplicate content. Overly long conversation history also wastes tokens. In practice, you can combine deduplication strategies from RAG best practices.
python
def deduplicate_chunks(chunks: list[str], threshold: float = 0.8) -> list[str]:
"""Deduplicate based on Jaccard similarity"""
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
if len(chunks) <= 1:
return chunks
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(chunks)
similarity = cosine_similarity(tfidf_matrix)
keep = [True] * len(chunks)
for i in range(len(chunks)):
for j in range(i+1, len(chunks)):
if similarity[i][j] > threshold:
keep[j] = False # Keep the first, discard subsequent duplicates
return [chunk for i, chunk in enumerate(chunks) if keep[i]]def truncate_conversation(history: list, max_tokens: int = 2000):
"""Truncate conversation history, keep the most recent messages"""
from tiktoken import encoding_for_model
enc = encoding_for_model("gpt-4o")
total_tokens = 0
truncated = []
for msg in reversed(history): # Start from the latest message
tokens = len(enc.encode(msg["content"]))
if total_tokens + tokens > max_tokens:
break
truncated.insert(0, msg)
total_tokens += tokens
return truncated
Method 11: Output Length Control — max_tokens and Stop Words
Besides max_tokens, stop words can more precisely control where the output ends. Note: stop words do not consume tokens but may truncate the output early.
python
def controlled_generate(prompt: str, max_length: int = 200):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=max_length,
stop=["\n\n", ".", "!", "?"] # Stop when encountering these characters
)
return response.choices[0].message.content
Note: Stop words do not consume tokens but may truncate the output early. For scenarios requiring complete sentences, use a period as a stop word. For code generation scenarios, use a newline as a stop word.
Method 12: Monitoring Attribution — Track Consumption by Feature/User Dimension
Without monitoring, optimization is impossible. Establish cost tracking by feature module and user dimension. It's recommended to add tracking logic in the API call wrapper layer, or use third-party tools like Helicone or LangSmith for automatic capture.
python
import sqlite3
from datetime import datetimeclass CostTracker:
def __init__(self, db_path: str = "costs.db"):
self.conn = sqlite3.connect(db_path)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS api_costs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
feature TEXT,
model TEXT,
prompt_tokens INTEGER,
completion_tokens INTEGER,
cost REAL,
timestamp TEXT
)
""")
def track(self, user_id: str, feature: str, model: str,
prompt_tokens: int, completion_tokens: int):
# Prices based on official pricing; example values used here
price_per_1k = {
"gpt-4o": 0.005,
"gpt-4o-mini": 0.00015,
"deepseek-chat": 0.00014
}
price = price_per_1k.get(model, 0.001)
cost = (prompt_tokens + completion_tokens) / 1000 * price
self.conn.execute("""
INSERT INTO api_costs (user_id, feature, model, prompt_tokens,
completion_tokens, cost, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (user_id, feature, model, prompt_tokens,
completion_tokens, cost, datetime.now().isoformat()))
self.conn.commit()
def get_feature_costs(self, days: int = 30):
"""Track costs by feature module"""
cursor = self.conn.execute("""
SELECT feature, SUM(cost) as total_cost, COUNT(*) as call_count
FROM api_costs
WHERE timestamp >= datetime('now', ?)
GROUP BY feature
ORDER BY total_cost DESC
""", (f"-{days} days",))
return cursor.fetchall()
def get_user_costs(self, days: int = 30):
"""Track costs by user"""
cursor = self.conn.execute("""
SELECT user_id, SUM(cost) as total_cost, COUNT(*) as call_count
FROM api_costs
WHERE timestamp >= datetime('now', ?)
GROUP BY user_id
ORDER BY total_cost DESC
""", (f"-{days} days",))
return cursor.fetchall()
Usage example
tracker = CostTracker()
tracker.track(user_id="user_123", feature="chatbot", model="gpt-4o-mini",
prompt_tokens=500, completion_tokens=200)
print(tracker.get_feature_costs())
Monthly Audit Checklist:
Effect Summary
Final Thoughts
LLM cost optimization is not a one-time project but ongoing governance: start with monitoring and attribution so you can see where the money goes; then pick the two or three highest-ROI levers among model routing, prompt trimming, caching, and batch processing; finally, make monthly audits a habit. Most teams see a visibly smaller bill after the first round—but more importantly, they build the "usage is visible" muscle, so every new feature ships with its cost under control.
FAQ
Q: Can semantic caching return incorrect answers? A: Yes. If the threshold is set too low (e.g., 0.85), queries that are semantically similar but have different intents may hit the cache. It's recommended to use a threshold above 0.95 for sensitive scenarios like customer service or healthcare, and regularly clean expired cache.
Q: How does DeepSeek perform on English tasks? A: DeepSeek supports English tasks, but model rankings vary widely by task type, so we won't make a blanket claim. Build a small eval set from your real prompts and measure quality and unit price yourself before deciding on a routing strategy.
Q: Is the 24-hour window for the Batch API a hard limit? A: Yes. OpenAI guarantees a return within 24 hours, but in practice, it may be faster (a few hours). If the task requires real-time response, the Batch API is not suitable.
Q: How to quickly estimate token count? A: Use the tiktoken library for precise calculation. Rough estimate: English 1 token ≈ 4 characters; for Chinese it depends on the tokenizer—newer ones (o200k) cost about 1 token per 2 characters, older ones (cl100k) about 1+ token per character. OpenAI's Playground also displays token count.
Q: Does monitoring attribution require additional development? A: Yes. It's recommended to add tracking logic in the API call wrapper layer, or use third-party tools like Helicone or LangSmith for automatic capture.
*Last updated: July 2026. Always verify against each tool's official docs.*
Also available in 中文.