Your First LLM Request: The First Answer with Ollama, C# and TypeScript
We get to know Ollama, look at models with ollama list and ollama ps, and send the first request to the /api/chat endpoint with curl. Then we repeat it in C# with Microsoft.Extensions.AI and OllamaSharp and in TypeScript with the ollama package, and use real output to read the token counts, the nanosecond durations and why the first request is slow.
Version note: The examples in this post were tested with Ollama 0.34 on a local model (gemma3:4b), using .NET 10 and Node 22.
Guess First
You ask the model the same question twice in a row. The first answer takes about 3.7 seconds, the second 1.5 seconds. Did the model think less the second time? Did it answer from a cache? Or was something completely different going on during the first request?
The answer hides in a few numbers that Ollama appends to every response. In this post we will send our first request with curl, then with C# and TypeScript, and read those numbers one by one. The answer to the question will come out of them.
What Ollama Does
Ollama is a tool that lets you run open-weight models on your own computer. It has two jobs: downloading and storing models, and serving them through an HTTP API. Once installed, a server starts in the background and by default listens only on 127.0.0.1:11434. So it is not reachable from outside; your code sends requests to that address, Ollama loads the model into memory, generates the answer and returns it. Think of a database server: just as you connect to SQL Server with a connection string, here you connect with a URL and a model name.
ollama list shows which models are on your computer:
$ ollama list
NAME ID SIZE MODIFIED
qwen3:4b 359d7dd4bcda 2.5 GB 7 minutes ago
nomic-embed-text:latest 0a109f422b47 274 MB 8 minutes ago
gemma3:4b a2af6cc3eb7f 3.3 GB 5 weeks ago
gemma3:12b f4031aab637d 8.1 GB 5 weeks ago
A model name has two parts: gemma3 is the model, and 4b after the colon is its tag, which tells you which size is installed. If you do not have a model, ollama pull gemma3:4b downloads it. ollama ps tells you which models are currently loaded in memory:
$ ollama ps
NAME ID SIZE PROCESSOR CONTEXT UNTIL
qwen3:4b 359d7dd4bcda 3.2 GB 100% GPU 4096 4 minutes from now
gemma3:4b a2af6cc3eb7f 3.7 GB 100% GPU 4096 4 minutes from now
The UNTIL column shows how much longer the model stays in memory. By default Ollama keeps a model loaded for five minutes after the last request and then unloads it. We will come back to the 4096 in the CONTEXT column in the fourth post; it is an important number that most people overlook.
The First Request with curl
Ollama's chat endpoint is /api/chat. You send it a model name and a list of messages:
$ curl -s http://localhost:11434/api/chat -d '{
"model": "gemma3:4b",
"messages": [
{ "role": "user", "content": "Recommend one classic science fiction novel in one sentence." }
],
"stream": false,
"options": { "temperature": 0, "seed": 42 }
}' | jq
messages is an array because a conversation consists of several messages; for now it holds a single message with the user role. We will look at roles in detail in the fifth post. "stream": false asks for the answer in one piece. temperature: 0 and seed: 42 inside options make us get the same answer to the same question every time, so we can put the curl, C# and TypeScript outputs side by side. Here is the response:
{
"model": "gemma3:4b",
"created_at": "2026-11-06T20:14:55.44115Z",
"message": {
"role": "assistant",
"content": "H.G. Wells’ *The War of the Worlds* depicts a terrifying Martian invasion of Earth that exposes humanity’s vulnerability and ultimately, its resilience."
},
"done": true,
"done_reason": "stop",
"total_duration": 1477052125,
"load_duration": 792250,
"prompt_eval_count": 20,
"prompt_eval_cached_count": 15,
"prompt_eval_duration": 79631000,
"eval_count": 33,
"eval_duration": 1395020000
}
The Fields in the Response
message is the model's answer; its role is assistant. We will add this object to the chat history in the next request. done_reason: "stop" says the model finished the answer on its own. Had it hit a token limit we would see length, as in the raw example from the first post.
Token counts live in two separate fields. prompt_eval_count is the number of tokens the model read, that is, what you sent: 20. eval_count is the number of tokens the model generated: 33. Cloud providers price these two separately as input and output tokens, so always track them separately. The sentence we sent is 9 words but took 20 tokens; part of the difference comes from the special tokens of the chat template Ollama places the message into. We will measure that in the third post.
Durations are in nanoseconds. total_duration is the total time of the request, load_duration the time spent loading the model into memory, prompt_eval_duration the time to process the prompt and eval_duration the time to generate the answer. You compute the generation speed from these: 33 tokens / 1.395 seconds ≈ 23.7 tokens per second.
The answer to the opening question is here too. The output above is from the second attempt. On the first request, sent while the model was not in memory, load_duration was 2058644041, about 2.06 seconds, and total_duration had climbed to 3.73 seconds. On the second request the model was already loaded, so load_duration dropped to 0.8 milliseconds. prompt_eval_cached_count: 15 tells another story: 15 of the prompt's 20 tokens were ready from the previous request, and Ollama did not process them again. So the model did not think less; the first request had the extra job of moving a 3.3 GB model from disk into memory. Generating the answer took about 1.3–1.4 seconds both times.
C#: The First Answer with IChatClient
On the C# side we will use Microsoft.Extensions.AI. This library defines common interfaces for talking to LLMs, the most important being IChatClient. The Ollama-specific part comes from the OllamaSharp package: its OllamaApiClient class implements IChatClient. The Microsoft.Extensions.AI.Ollama package you will find online is deprecated, and its NuGet page recommends OllamaSharp instead. Let's set up the project:
$ dotnet new console -n Bookstore.FirstRequest
$ cd Bookstore.FirstRequest
$ dotnet add package Microsoft.Extensions.AI --version 10.11.0
$ dotnet add package OllamaSharp --version 5.5.2
The contents of Program.cs:
using Microsoft.Extensions.AI;
using OllamaSharp;
IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");
// Same temperature and seed as the curl example, so the answers are comparable
ChatOptions options = new() { Temperature = 0, Seed = 42 };
ChatResponse response = await client.GetResponseAsync(
"Recommend one classic science fiction novel in one sentence.", options);
Console.WriteLine(response.Text);
Console.WriteLine($"Model: {response.ModelId}, finish: {response.FinishReason}");
Console.WriteLine($"Input tokens: {response.Usage?.InputTokenCount}, output tokens: {response.Usage?.OutputTokenCount}");
// Ollama-specific durations arrive in AdditionalProperties
foreach (KeyValuePair<string, object?> item in response.AdditionalProperties ?? [])
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
The output of dotnet run:
H.G. Wells’ *The War of the Worlds* depicts a terrifying Martian invasion of Earth that exposes humanity’s vulnerability and ultimately, its resilience.
Model: gemma3:4b, finish: stop
Input tokens: 20, output tokens: 33
load_duration: 00:00:00.0009382
total_duration: 00:00:01.5002021
prompt_eval_duration: 00:00:00.1534930
eval_duration: 00:00:01.3439110
The answer matches the curl one letter for letter. Here we passed a single string to GetResponseAsync; behind the scenes the method turns it into a single ChatMessage with the user role. Temperature and Seed in ChatOptions are carried into Ollama's options field by OllamaSharp.
In the response object, Ollama's fields have turned into common names as well. prompt_eval_count is now Usage.InputTokenCount and eval_count is Usage.OutputTokenCount. done_reason became FinishReason. The durations, which have no equivalent in the common model, sit in the AdditionalProperties dictionary as TimeSpan values. We will see the benefit of this abstraction in the seventeenth post: the same code works with another provider by changing only the line that creates the IChatClient.
TypeScript: The First Answer with the ollama Package
On the TypeScript side we will use Ollama's official ollama package. Node 22.2 cannot run TypeScript files directly, so we add tsx, and to use top-level await we make the project an ES module:
$ mkdir bookstore-ts && cd bookstore-ts
$ npm init -y
$ npm pkg set type=module
$ npm install ollama@0.6.4
$ npm install -D tsx typescript @types/node
first-request.ts:
import ollama from "ollama";
const response = await ollama.chat({
model: "gemma3:4b",
messages: [
{ role: "user", content: "Recommend one classic science fiction novel in one sentence." },
],
options: { temperature: 0, seed: 42 },
});
console.log(response.message.content);
console.log(`Prompt tokens: ${response.prompt_eval_count}, output tokens: ${response.eval_count}`);
// Durations arrive in nanoseconds
const seconds = response.eval_duration / 1e9;
console.log(`Generation: ${seconds.toFixed(2)} s, ${(response.eval_count / seconds).toFixed(1)} tokens/s`);
console.log(`Load: ${(response.load_duration / 1e6).toFixed(1)} ms, total: ${(response.total_duration / 1e6).toFixed(0)} ms`);
The output of npx tsx first-request.ts:
H.G. Wells’ *The War of the Worlds* depicts a terrifying Martian invasion of Earth that exposes humanity’s vulnerability and ultimately, its resilience.
Prompt tokens: 20, output tokens: 33
Generation: 0.75 s, 43.8 tokens/s
Load: 0.8 ms, total: 817 ms
This package does not add an abstraction layer; the response object is a typed version of the JSON you saw with curl. The field names are the same too: prompt_eval_count, eval_duration and the rest. The default address is http://127.0.0.1:11434; to connect to another machine you create your own instance with new Ollama({ host: "..." }).
Same Answer, Three Different Speeds
We got the same text all three ways, because all three went to the same endpoint with the same settings. But look at the generation speed: 23.7 tokens per second with curl, about 24.6 with C#, 43.8 with TypeScript. The code does the same work; the difference comes from how busy the machine was at that moment. Other jobs were using the same Ollama server while I ran these examples. That is why timings you measure locally jump around. Do not decide anything based on a single measurement; we will measure tokens/s properly in the sixteenth post.
As for which layer to use when: curl is ideal for quickly trying a model or a setting. In application code, IChatClient in C# and the ollama package in TypeScript do the job. IChatClient makes switching providers easy but cannot carry every Ollama-specific field into a common name. When you need the raw API, response.RawRepresentation takes you down to OllamaSharp's own object.
Common Pitfalls
The first pitfall is forgetting the stream field. Streaming is on by default in Ollama. Send the same curl request without "stream": false and instead of a single JSON you get one JSON object per line:
{"model":"gemma3:4b","created_at":"2026-11-06T20:17:05.643538Z","message":{"role":"assistant","content":"H"},"done":false}
{"model":"gemma3:4b","created_at":"2026-11-06T20:17:05.669653Z","message":{"role":"assistant","content":"."},"done":false}
{"model":"gemma3:4b","created_at":"2026-11-06T20:17:05.69386Z","message":{"role":"assistant","content":"G"},"done":false}
Try to read this NDJSON stream in one go with JSON.parse and you get an error. We will use streaming on purpose in the seventh post.
The second is an incomplete model name. Without a tag, Ollama looks for the latest tag. I do not have gemma3:latest installed, so a request with "model": "gemma3" came back with HTTP 404:
{"error":"model 'gemma3' not found"}
The third is reading durations as milliseconds. In the raw API every duration is in nanoseconds; 1477052125 is about one and a half seconds, not seventeen days. On the C# side OllamaSharp turns them into TimeSpan, so this pitfall does not exist there.
The fourth is taking the time of the first request as the model's speed. The first request includes the model load time. If you benchmark, send a warm-up request first and measure the ones after it.
A Small Experiment
★☆☆ Change the question in the curl request and look at prompt_eval_count and eval_count. When you ask for "three sentences" instead of "one sentence", how do eval_count and eval_duration change?
★★☆ Confirm with ollama ps that the model is loaded, wait more than five minutes and check again. After the model has been unloaded, run the C# program and measure how much load_duration grows.
★★★ Run the C# example with gemma3:12b. Compare the answer, the token counts and the tokens/s value with the 4b model for the same question, and keep notes to compare with the measurement in the sixteenth post.
In the next post we will chase the prompt_eval_count and eval_count numbers we met here: what a token is, how a tokenizer splits text, why the Turkish version of a sentence takes more tokens than the English one and how many tokens code and numbers cost, all with real measurements.