Make your first request
The goal of this page is to get a real answer from a model as quickly as possible. You’ll install LM15, set an API key, and ask a question. Then we’ll change one thing at a time, and look at what changes. The Overview explains the ideas behind each part; here we’ll skip the theory and focus on the practice.
On this page, you’ll learn how to:
- ask a question and read everything that comes back;
- give the model instructions;
- carry a conversation into a follow-up question;
- watch an answer arrive as it’s written.
The code follows the language, provider, and model you picked at the top left
of the page. The answers shown are real: we ran each step with gpt-5.6-sol
through OpenAI Codex. Yours will be worded differently, and that’s normal.
Install
Section titled “Install”You’ll need Python 3.10 or newer. Run this in your terminal:
python3 -m pip install --pre lm15The Python core has no required third-party dependencies: it uses Python’s standard library. The --pre flag includes prereleases, so you get the API used in this guide rather than the older stable release.
Using Windows, or keeping this project separate?
On Windows, use py instead of python3.
A virtual environment keeps this project’s packages separate from your other Python work. Create one before installing LM15:
python3 -m venv .venvActivate it on macOS or Linux:
source .venv/bin/activateOr in Windows PowerShell:
.venv\\Scripts\\Activate.ps1Then install inside it:
python -m pip install --pre lm15The R package is not yet publicly available. This example previews its current API.
Set an API key
Section titled “Set an API key”You only need a key from one provider. Choose your provider at the top left of the page, then open its key page below. You may need to create an account and add credit.
- Anthropic API keys —
ANTHROPIC_API_KEY - DeepSeek API keys —
DEEPSEEK_API_KEY - Google Gemini API keys —
GEMINI_API_KEY - Groq API keys —
GROQ_API_KEY - Meta API keys —
META_API_KEY - Moonshot / Kimi API keys —
MOONSHOTAI_API_KEY - OpenAI API keys —
OPENAI_API_KEY - OpenRouter API keys —
OPENROUTER_API_KEY - Z.AI API keys —
ZAI_API_KEY
On macOS or Linux, run the command below in the terminal where you’ll run your code. Replace your-api-key with your own key.
Choose a provider above to fill in the variable name below.
export YOUR_PROVIDER_API_KEY="your-api-key"First time setting an environment variable?
An environment variable is a named value your terminal passes to the programs it starts. LM15’s router reads the key from that value, so you don’t need to put it in your code.
The command above works in Bash and Zsh, the usual shells on macOS and Linux. In Windows PowerShell, use:
$env:YOUR_PROVIDER_API_KEY = "your-api-key"In Windows Command Prompt, use:
set YOUR_PROVIDER_API_KEY=your-api-keyThese commands apply to the current terminal. Run your example from that same terminal; set the key again when you open a new one. An already-open notebook or editor won’t automatically pick it up.
Keep the key private. Don’t include it in shared code, screenshots, or version control, and don’t print it to check whether setup worked.
Running the example sends a real request to your chosen provider and may cost money. Choose a model your account can access.
Using local Ollama instead? The Python router acceptsollama:your-model without an API key. Ollama must be running with that model installed; edit the model name in your copied code.
For explicit keys, rotating credentials, or local Claude and Codex logins, see Python authentication.
Forgot to set the key?
Python reports which provider needs a key and which variable to set. For your selected provider, the message begins:
Set the named variable, then run your code again from the same terminal.
Ask a question
Section titled “Ask a question”The examples in these guides follow one small program: an assistant for a wildlife research station. The station has a mystery: acorns under the oak trees vanish overnight. Let’s ask a model.
from lm15 import LMRouter, Message, Request
request = Request(
model="provider:model",
messages=[Message.user(
"What might be eating the acorns under our oak trees at "
"night?"
)],
)
router = LMRouter()
response = router.complete(request)
print(response.text)Run it. Here is what came back when we did:
Common nighttime acorn eaters include: - **Deer** — often swallow acorns whole; look for hoofprints and pellet-like droppings. - **Mice, rats, voles, and woodrats** — leave neatly gnawed shells, small tooth marks, or piles near cover. - **Flying squirrels** — nocturnal and especially likely near mature oaks; may leave opened shells around tree bases. - **Raccoons and opossums** — may eat acorns along with other foods; identifiable by hand-like raccoon tracks or opossum tracks. - **Wild pigs or bears**, where present — usually leave obvious signs such as rooted-up soil, large tracks, or scat. Day-active **tree squirrels, chipmunks, jays, and turkeys** may also remove or cache acorns, so disappearance noticed in the morning does not necessarily mean it happened overnight. Acorns with a small round hole may instead contain—or have contained—an **acorn-weevil larva**. A motion-activated trail camera aimed at an undisturbed patch beneath the trees is the easiest way to identify the visitor.
Three things happened:
- You described what you want: a request, with a model and one message.
- The router read the model name. Its first part names the provider, so it knew whom to call and which API key to look for in your environment.
- The provider’s answer came back as a response. Its text is the text of the message the model wrote.
The answer is long, and it’s written as a list. We’ll come back to that when we give the model instructions.
Read the whole response
Section titled “Read the whole response”The text is only part of what comes back. Replace the lines after the request with these:
router = LMRouter()
response = router.complete(request)
print(response.text)
print(response.finish_reason) # "stop", "length", "tool_call"…
print(response.usage.input_tokens, response.usage.output_tokens)- finish reason
- stop
- input tokens
- 30
- output tokens
- 581
- The finish reason says why the model stopped.
stopmeans it finished on its own. You’ll also meetlength(it reached a limit you set) andtool_call(it is waiting for your program to run a tool). - Token counts are what the provider counted, and what it bills: here 30 tokens in and 581 out. A count the provider didn’t report is empty, not zero.
Give it instructions
Section titled “Give it instructions”581 tokens is a lot for a quick question. Instructions change
that. They go in system, beside the messages rather than inside them, and they
apply to the whole request. Change the request to this:
request = Request(
model="provider:model",
system=(
"You are the field assistant for a wildlife research "
"station. Answer in two sentences."
),
messages=[Message.user(
"What might be eating the acorns under our oak trees at "
"night?"
)],
)Likely nighttime acorn eaters include deer, raccoons, opossums, wild pigs, wood mice, rats, and other small rodents; squirrels and many birds usually feed during the day. Check for tracks, scat, gnaw marks, or disturbed leaf litter, and use a motion-triggered trail camera to identify the visitor.
- finish reason
- stop
- input tokens
- 40
- output tokens
- 94
The question is the same, but the answer is different: it’s two sentences long (94 tokens instead of 581), and it answers as the station’s assistant.
Ask a follow-up
Section titled “Ask a follow-up”Now a second question: Would the same animals eat hazelnuts? Send it on its own first:
followup = Request(
model="provider:model",
system=(
"You are the field assistant for a wildlife research "
"station. Answer in two sentences."
),
messages=[Message.user("Would the same animals eat hazelnuts?")],
)
print(router.complete(followup).text)Which animals are you referring to? Many rodents, birds, deer, and wild boar eat hazelnuts, but suitability depends on the species.
The model doesn’t know which animals you mean. It remembers nothing between requests: each one must carry the conversation. So send the first question, the model’s answer, and then the new question. Add this after the lines that print the first answer:
followup = Request(
model="provider:model",
system=(
"You are the field assistant for a wildlife research "
"station. Answer in two sentences."
),
messages=[
*request.messages,
response.message,
Message.user("Would the same animals eat hazelnuts?"),
],
)
print(router.complete(followup).text)Yes—mice, rats, squirrels, raccoons, deer, and wild pigs may also eat hazelnuts, although opossums are less likely to target hard-shelled nuts. Small, neat tooth marks often indicate rodents, while crushed shells or scattered husks may point to larger mammals.
- finish reason
- stop
- input tokens
- 149
- output tokens
- 76
This time the model knows which animals you mean. The cost is in the input: 149 tokens, against 35 for the question alone. Each request sends the whole conversation again, and you pay for it again.
Send the model’s whole message, not just its text. A message can carry things the provider needs to continue, such as its reasoning, which the text alone would lose.
Watch it arrive
Section titled “Watch it arrive”A long answer takes a while. Streaming prints it as it’s written. The request is the one you sent in Give it instructions; only the way you receive the answer changes:
stream = ResponseStream(router.stream(request), request)
for text in stream:
print(text, end="", flush=True)
print()
print(stream.usage.output_tokens, "tokens")Likely nighttime acorn eaters include deer, raccoons, opossums, mice, rats, and squirrels caching food around dusk; wild boar may also be responsible where present. Check for tracks, droppings, rooting, gnawed shells, or set up a motion-activated trail camera to identify the visitor.
- finish reason
- stop
- input tokens
- 40
- output tokens
- 91
- pieces streamed
- 67
The answer arrived in 67 pieces. At the end you get the same kind of complete response as before, with its finish reason and token counts.
Compare it with the answer under Give it instructions: same request, different words. A model doesn’t repeat itself word for word, so don’t write code that expects it to.
Use another provider
Section titled “Use another provider”To switch, change the model name, and nothing else. Pick another provider at the top of the page and every example above follows. A model name belongs to one provider, so choose a model that provider serves, and set its API key too.
When something goes wrong
Section titled “When something goes wrong”- No API key found. The error names the environment variable to set; see Set an API key. Set it in the terminal where you run your program.
- The model name is wrong or your account can’t use it. The provider refuses, and LM15 passes its message on.
- A setting the provider can’t honour. LM15 refuses before sending, rather than quietly ignore what you asked. When we asked OpenAI Codex, which has no length limit, for a 16-token limit:
UnsupportedFeatureError: openai-codex: config.max_tokens: this backend has no output cap; dropping it risks unbounded paid generation
The request was never sent, so it didn’t cost anything.
Try it yourself
Section titled “Try it yourself”Predict the answer before you run each one.
- Run the program from Ask a question twice. Do you get the same words?
- Remove “Answer in two sentences.” from the instructions. What happens to the output tokens?
- In the follow-up, keep the first question but leave out the model’s answer. What does the model see, and how might it answer?
What to expect
- No. The same request gave two differently worded answers above, under Give it instructions and Watch it arrive.
- They grow back towards the length of the first, list-shaped answer.
- Two questions in a row, with no answer between them. It may answer both, or only the last; either way, it can’t know what it said before.
You’ve used four of the parts every request has: a model, messages, instructions, and the response. The Overview shows the others, tools and settings, and how LM15 keeps them the same across languages and providers.