Response and Usage
On this page
Understand What the API Returns
Most developers new to the API read a single line of the response—the generated text—and ignore everything else. That’s a mistake. Every call to /v1/chat/completions returns a rich JSON object. This is where you’ll find the data you need to track costs, detect a truncated response before it reaches the user, and pinpoint a specific call when you open a support ticket.
Full Anatomy of the Response
Let’s start with the metadata. These are the fields you rarely check but will wish you had logged the day an incident occurs:
from mistralai.client import Mistral
import os
client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
response = client.chat.complete(
model="mistral-large-latest",
messages=[{"role": "user", "content": "What is machine learning?"}],
max_tokens=500
)
# Access metadata
print(f"Request ID : {response.id}")
print(f"Model used : {response.model}")
print(f"Created at : {response.created}")
The id field is a unique identifier assigned to each request. This is the reference you’ll provide to technical support, and the one that lets you link a user complaint to a specific call in your logs. The model field confirms which model was actually used. This matters more than you might think when you call a -latest alias and behavior changes overnight without any code changes on your end.
The choices Array
The heart of the response lies in choices. By default, this array contains a single element. From it, you extract both the text and the verdict on how the generation ended:
choice = response.choices[0]
# The generated message
print(choice.message.role) # "assistant"
print(choice.message.content) # The response text
# The index of this response in the array
print(choice.index) # 0
# The stop reason
print(choice.finish_reason) # "stop", "length", or "tool_calls"
If you request multiple responses using the n parameter, which we’ll cover in lesson 15, choices will contain multiple elements, and the index will become meaningful again.
finish_reason Values
This field determines the reliability of your application. There are three possible values. stop signals that the model finished its response naturally. This is the normal case, where you can display the content without reservation. length means the generation was cut off by max_tokens, so the response is potentially incomplete—a summary missing its conclusion, or a list that stops at the seventh item. Finally, tool_calls indicates that the model is requesting the execution of an external tool as part of function calling, and the ball is now in your court.
Each of these cases requires distinct handling, which you should write once and for all:
response = client.chat.complete(
model="mistral-large-latest",
messages=[{"role": "user", "content": "Write a detailed report."}],
max_tokens=200
)
choice = response.choices[0]
if choice.finish_reason == "stop":
# Complete response — normal processing
result = choice.message.content
elif choice.finish_reason == "length":
# Truncated response — retry with more tokens or warn the user
print("Warning: The response was truncated.")
# Option: Retry with a higher max_tokens limit
# Option: Ask the model to continue
elif choice.finish_reason == "tool_calls":
# The model requests a tool call
for tool_call in choice.message.tool_calls:
print(f"Requested tool: {tool_call.function.name}")
Notice that the length branch doesn’t just let the text pass through. You either retry with a higher limit, ask the model to continue, or warn the user. What you must never do is display truncated text as if it were complete.
The usage Object — Track Your Costs
The usage field counts the tokens consumed by the request, separating input from output:
usage = response.usage
print(f"Input tokens : {usage.prompt_tokens}")
print(f"Output tokens : {usage.completion_tokens}")
print(f"Total : {usage.total_tokens}")
This distinction isn’t cosmetic. Mistral models have different rates for input and output. A feature that sends a long context to get three words back has a completely different cost profile than a long draft generated from a brief prompt. The calculation takes just a few lines:
# Hypothetical rates per million tokens (check current pricing)
RATE_INPUT = 2.0 # $/M tokens input
RATE_OUTPUT = 6.0 # $/M tokens output
cost_input = (usage.prompt_tokens / 1_000_000) * RATE_INPUT
cost_output = (usage.completion_tokens / 1_000_000) * RATE_OUTPUT
cost_total = cost_input + cost_output
print(f"Cost of this call: ${cost_total:.6f}")
A single call costs a fraction of a cent, which is falsely reassuring. Multiply that by the real traffic of an application, and the question becomes: which feature consumes what? To answer that, aggregate usage metrics from your very first deployment. Log every call along with the name of the feature that triggered it:
import json
from datetime import datetime
def log_usage(response, endpoint_name: str):
"""Logs usage metrics for each API call."""
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"endpoint": endpoint_name,
"model": response.model,
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens,
"finish_reason": response.choices[0].finish_reason,
"request_id": response.id
}
with open("api_usage.jsonl", "a") as f:
f.write(json.dumps(log_entry) + "\n")
# Usage
response = client.chat.complete(
model="mistral-large-latest",
messages=[{"role": "user", "content": "Hello"}]
)
log_usage(response, "main_chatbot")
A JSONL file like this, reviewed after two weeks, will tell you without ambiguity which screen in your product weighs heaviest on the bill—and how many calls end with length.
Response Content
The content field of the assistant message can take two forms, exactly like on the request side:
# Simple form — string
content = response.choices[0].message.content
# "Machine learning is a branch of artificial intelligence..."
# Structured form — list of blocks (rare in standard chat)
# [{"type": "text", "text": "..."}]
In practice, for standard Chat Completions calls, content is always a string. The structured form is reserved for multimodal responses.
Put It Into Practice
Let’s bring all this together in a utility function—the one you’ll copy from project to project. It assembles the messages, makes the call, and returns a dictionary that exposes both metrics and text:
from mistralai.client import Mistral
def chat_complete(
client: Mistral,
prompt: str,
system: str = "",
model: str = "mistral-large-latest",
max_tokens: int = 1000
) -> dict:
"""Chat Completions call with error handling and metrics."""
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
response = client.chat.complete(
model=model,
messages=messages,
max_tokens=max_tokens
)
return {
"content": response.choices[0].message.content,
"finish_reason": response.choices[0].finish_reason,
"tokens_in": response.usage.prompt_tokens,
"tokens_out": response.usage.completion_tokens,
"model": response.model,
"id": response.id
}
The key point is what the function returns: never just the text. By bubbling up finish_reason, token counts, and the request ID to the caller, you make the most common error impossible—treating a cut-off response as a valid one.
Key Takeaways
- The response contains
choices(content),usage(tokens), and metadata (id,model) - Always check
finish_reason—lengthmeans an incomplete response usagecounts input and output tokens separately for precise cost tracking- Set up metrics logging from your first production deployment
- The
idfield is essential for debugging and technical support