Key takeaways
- ✓An OpenAI-compatible server (here `mlx_lm.server`) accepts the same requests as OpenAI's API, so the app keeps its code and changes settings: the address, the model name, the prompt and a couple of call options. Switching between the paid model and your own is a config change, which makes a fair comparison easy.
- ✓On 100 made-up test emails, the 4-bit fine-tune got 99 fully right, gpt-5.4-mini 97 and gpt-5.4-nano 75. Mine and mini disagreed on only 4 emails, 3 to 1 (p = 0.62): a tie. Nano is clearly behind (p < 0.001). The fine-tune trained on emails like these.
- ✓One email at a time, the fine-tune's round trip was 0.11 s typical and 0.13 s at the slow end, against mini's 1.02 s and 2.09 s. Its prompt was 115 tokens against mini's 1,766. No internet trip, a 15× shorter prompt and almost certainly a much smaller model all differ; the lab didn't measure which matters most.
- ✓A self-hosted email costs the machine's hourly price ÷ the emails it handles that hour. The laptop handled 87,263 an hour at 32 simulated users. At a what-if $2/h that's 2.3¢ per 1,000 when always busy and 46¢ at 5%: the same machine, 20× more per email. A $5/h machine at 5% ($1.15) beats mini only at list price ($1.46); with mini's cache discount (estimated 42–77¢, not measured) it loses.
- ✓The table leaves out people, change and volume: someone runs the server, a new product means retraining, and 5,000 emails a day is 3.4 minutes of this server's work. Kept on all day at $2/h, that's $9.60 per 1,000. Self-host when the job is narrow, the machine stays busy, or the data can't leave; otherwise pay per call, and compare right answers first.
The shop's support emails are sorted by an AI model, and today the shop pays OpenAI for every email. In the fine-tuning chapters, a small model learned to do the same job; in the last two chapters it was shrunk to 4 bits and served to many users at once. Now the question that started it: should the shop run that model itself, or keep paying per call? This chapter compares right answers, speed and cost, and then what the comparison leaves out.
The source is Lab 12 (Inference & Serving), script 12_self_host_vs_api.py, recorded in October 2026 on one laptop (Apple M4 Max). It serves Lab 11's fine-tuned Qwen3-0.6B, merged and quantized to 4 bits (0.34 GB, still 99% right; chapter 29), with mlx_lm.server, and runs it against two OpenAI models on the first 100 of Lab 11's test emails. The emails are made up, with known right answers, and the fine-tune trained on emails just like them. The hourly machine prices are what-ifs, not quotes. The lab's API half (streaming, prompt caching, the Batch API, semantic caching, cascades) isn't in the video; one of its results, the cache discount, matters here and is quoted below.
The same code, a different address
An app talks to a model through an API: a standard way for one program to send another a request and get an answer back. OpenAI's API is the one most client code is written for.
mlx_lm.server is an OpenAI-compatible server: it runs the model on the laptop and accepts the same requests as OpenAI's API. So the app keeps its library and its call. The lab's script starts the server like this:
python -m mlx_lm server --model data/models/ticket-0.6b-4bit --port 8090 \
--chat-template-args '{"enable_thinking": false}' --max-tokens 60
and talks to both with the same openai client:
api = OpenAI() # OpenAI's servers
local = OpenAI(base_url="http://localhost:8090/v1", api_key="not-needed") # this laptop
r = client.chat.completions.create(model=model, messages=msgs, **options)
Only settings change: the address, the model name, the prompt, and a couple of call options (the API calls use reasoning_effort="none" and max_completion_tokens, the local ones max_tokens and temperature=0). Switching between the paid model and your own is a config change, which also makes a fair race easy. A model gateway gives you the same thing across many providers.
Right answers: a tie, and a clear loser
The race: the same 100 emails, one at a time. For each, the model fills in four fields: what the customer wants, the order number, the product, and how urgent it is. An answer counts as right only if all four are.
| Setup | Prompt | All four right |
|---|---|---|
| My fine-tune, 4-bit, on the laptop | one line | 99 |
| gpt-5.4-nano, the cheapest of OpenAI's 5.4 models | a page of instructions | 75 |
| gpt-5.4-mini | the instructions plus 10 worked examples | 97 |
Is 99 against 97 a real difference? A paired test (McNemar's) looks only at the emails where the two disagree. Mine and mini disagreed on just 4: three went my way, one went mini's. That's like three heads in four coin flips (p = 0.62), so it's a tie. Nano's 75 is clearly behind (p < 0.001).
Remember what the tie means: on emails like the ones it practised on, the narrow model matches the bigger one. It can't do anything else (the fine-tuning regressions chapter shows what it lost).
Speed: a tenth of a second
Time is the full round trip as the app sees it: localhost for mine, the internet for the API.
| Setup | Typical (median) | Slow end (p95) | Prompt tokens |
|---|---|---|---|
| My fine-tune | 0.11 s | 0.13 s | 115 |
| gpt-5.4-nano | 0.80 s | 1.81 s | 655 |
| gpt-5.4-mini | 1.02 s | 2.09 s | 1,766 |
The slow end (95th percentile) is the time one email in twenty took longer than. Mini's typical email took 9× as long as mine, and mine barely has a slow end: 0.13 s against 0.11.
Why so much faster? Several things differ at once: no trip over the internet, a prompt 15× shorter than mini's (the fine-tune learned the rules, so it doesn't need them spelled out), and almost certainly a much smaller model (mini's size isn't published). The lab didn't measure which of these matters most.
Switch between the measures:
All four fields correct. Mine and mini disagreed on just 4 emails: 3 went my way, 1 went mini's (p = 0.62). That's a tie. Nano is clearly behind (p < 0.001). Mine trained on emails just like these.
Many at once
So far, one email at a time. How many can the laptop handle? The lab sent 64 emails per setting from 1, 4, 16 and 32 simulated users (threads sending back to back):
| Simulated users | Emails a second | Emails an hour | Typical | Slow end |
|---|---|---|---|---|
| 1 | 8.6 | 30,975 | 0.12 s | 0.13 s |
| 4 | 17.0 | 61,286 | 0.23 s | 0.32 s |
| 16 | 22.8 | 82,088 | 0.67 s | 0.85 s |
| 32 | 24.2 | 87,263 | 1.14 s | 1.52 s |
The server works on many emails in one pass, letting new requests join mid-way (continuous batching, chapter 30). Throughput nearly triples, and each email waits longer: at 32 users a typical email takes about as long as mini's. Even so, the slow end, 1.52 s, is still under mini's slow end with a single user (2.09 s). 32 was the most the lab tried, and throughput was still rising.
Simulated users sending at once
Mine, 32 at once
typical 1.14 s · slow end 1.52 s
gpt-5.4-mini, one at a time
typical 1.02 s · slow end 2.09 s
mlx_lm.server works on many emails in one pass, so throughput nearly triples from 1 to 32 users, and each email waits longer. At 32 the typical email takes about as long as mini's, but the slow end (1.52 s) is still under mini's one-user slow end (2.09 s). 32 was the most the lab tried, and throughput was still rising: the 87,263 an hour used below may not be the machine's limit.
What one email really costs
The API charges per email: at list price, $1.46 per 1,000 for mini and $0.18 for nano. Your own machine charges per hour, busy or not. Like a gym membership, the more you use it, the less each visit costs:
cost per 1,000 emails = machine cost per hour ÷ emails handled per hour × 1,000
With the measured 87,263 emails an hour at full use, and what-if hourly prices (a laptop you own costs its power and wear; a rented GPU its rental):
| Machine cost per hour | Used 100% | Used 25% | Used 5% |
|---|---|---|---|
| $0.10 | $0.001 | $0.005 | $0.023 |
| $0.50 | $0.006 | $0.023 | $0.115 |
| $2 | $0.023 | $0.092 | $0.458 |
| $5 | $0.057 | $0.229 | $1.146 |
At $2 an hour, always busy, that's about 2¢ per 1,000. Handling only 5% of what it could, 46¢ per 1,000: the same machine, 20× more per email, because it sits idle most of the hour.
A $5-an-hour machine used 5% of the time comes to $1.15: under mini's $1.46, just. But $1.46 is list price. OpenAI charges a tenth of the input price for the start of a prompt it has seen recently, and mini's ~1,650 tokens of instructions and examples are the same on every call. The lab's own run didn't pass cached tokens to its cost function, and the discount couldn't be measured afterwards. An estimate from OpenAI's prices: if 1,024 to 1,536 of mini's 1,766 prompt tokens were cached on every call, mini would cost about 42–77¢ per 1,000, maybe less if more is cached. Then the mostly idle $5 machine loses, and the $2 machine at 5% (46¢) is inside the range. Nano's 655-token prompt is under the cache's 1,024-token minimum, so its 18¢ stands.
The cache discount is not a guess about whether it exists. In Lab 12's Part 1 (03_prompt_caching.py), from the second call on, 1,792 of about 2,860 input tokens came from the cache and the cost per call halved. It works in steps: the first 1,024 tokens, then 128 at a time. What isn't known is how much of mini's prompt hit the cache in this run.
Pick a machine price and how busy it is:
The machine costs, per hour (what-if)
How busy it is
5% of capacity ≈ 104,716 emails a day, kept on all day
Cheaper than mini at list price; may lose once the discount applies
Hourly prices are what-ifs: a laptop you own costs power and wear, a rented GPU its rental. Capacity is the measured 24.2 emails a second at 32 simulated users. The discount range assumes 1,024 to 1,536 of mini's ~1,766 prompt tokens are cached on every call; the run didn't record it. Not in the number: the people who run the server.
Nano's 18¢ beats the $2 and $5 machines used 5% of the time, and it gets a quarter of the emails wrong. Compare right answers first; a cheap wrong answer isn't cheap.
What the table leaves out
- People. Someone has to run the server, watch it, and fix it when it breaks. That's not in the hourly price.
- Change. When the shop adds products or changes its rules, the paid model reads a new prompt. The fine-tune needs new training data and a new training run (Lab 11).
- Volume. Say the shop gets 5,000 emails a day. That's 3.4 minutes of work for this server. Kept on all day at $2 an hour, it costs $9.60 per 1,000 emails, far more than mini. Unless the machine is free anyway, or privacy or speed forces it, paying per call is simpler at that volume, and nano or a cascade (a cheap model first, a bigger one when it's unsure) may be enough.
When to run it yourself
- Keep the same client code for both, so switching is always a config change, and fallback to the other is easy.
- Self-host when the job is narrow, the volume keeps a machine busy, or the data can't leave the company. The win here came from three things together: a narrow model small enough to be fast (Lab 11), quantized to fit (chapter 29), and kept busy by batching (chapter 30).
- Otherwise, pay per call, and price the API with the cache discount you actually get, not list price.
Caveats
- Made-up emails, and the fine-tune trained on emails like them. The 99% holds for this narrow task; it says nothing about anything else.
- Hourly prices are what-ifs. Put in your own: hardware spread over its life plus power, or a rented GPU.
- Mini's cost is at list price. The cache discount wasn't recorded; the 42–77¢ range is an estimate from OpenAI's prices, not a measurement.
- One laptop, one run, one network. API times include the internet trip from one home connection on one day.
- Capacity is a measured point, not a ceiling. 87,263 an hour is at 32 simulated users, the most tried, with throughput still rising. The simulated users are threads sending back to back, not real traffic.
- The speed gap isn't split by cause. Network, prompt length and model size all differ at once.
The checklist
- Put both behind the same client: an OpenAI-compatible server or a gateway, so switching is a config change.
- Compare right answers first, with a paired test on the same items. A gap of 2 on 100 can be a tie.
- Measure latency as the app sees it: typical and slow end, at the concurrency you expect.
- Measure throughput under load, then price a self-hosted request as hourly cost ÷ requests per hour × 1,000.
- Use how busy the machine will really be, not its peak. At low volume an always-on machine costs more per request than the API.
- Price the API with its discounts: cached prompt tokens, batch pricing.
- Count the people and the retraining before you move a model in-house.
Practice: price the race
Write the lab's nearest-rank percentile and paired McNemar test, then both sides of the bill: an API call's cost per 1,000 (with and without cached tokens) and a self-hosted email's cost from the hourly price, the throughput and how busy the machine is. The tests use the recorded run: 0.11 s and 0.13 s for the fine-tune, p = 0.625 for the tie with mini, $1.46 at list price and 42–77¢ with the estimated discount, 46¢ at 5% busy, and $9.60 for 5,000 emails a day.
Quiz
1.What does an OpenAI-compatible server change for the app that calls a model?
2.The fine-tune got 99 of 100 right and gpt-5.4-mini 97. What can you say?
3.Why was the fine-tune's round trip so much shorter than mini's (0.11 s vs 1.02 s)?
4.With 32 simulated users at once, what happened on the laptop server?
5.At a what-if $2 an hour, why does a self-hosted email cost 20× more when the machine is used 5% of the time than when it's always busy?
6.A $5/h machine used 5% of the time costs $1.15 per 1,000 emails, under mini's $1.46 list price. Why isn't that the end of it?
Check your understanding
Try each question before opening its answer.
Why is switching between a paid API and your own model "a config change"?
An OpenAI-compatible server such as mlx_lm.server accepts the same requests as OpenAI's API. The app keeps its library and its call and changes settings: the address, the model name, the prompt and a couple of call options.
Is 99 right against 97 a win for the fine-tune?
No, a tie. The two disagreed on only 4 of 100 emails, 3 one way and 1 the other, and a paired test gives p = 0.62. Nano's 75 is clearly behind (p < 0.001). And the fine-tune trained on emails like these.
What explains the 0.11 s vs 1.02 s round trip?
Several differences at once: no internet trip, a prompt 15× shorter (115 vs 1,766 tokens), and almost certainly a much smaller model. The lab didn't measure which matters most.
What happens to each email's time when 32 users share the laptop?
Throughput rises from 8.6 to 24.2 emails a second, and the typical email takes 1.14 s, about mini's. The slow end, 1.52 s, is still under mini's 2.09 s with one user.
How do you price a self-hosted email, and what moves it most?
Hourly cost ÷ emails handled per hour × 1,000. How busy the machine is moves it most: at $2/h, 2.3¢ per 1,000 always busy, 46¢ at 5%, and $9.60 if it's kept on all day for 5,000 emails.
Why compare against the API's discounted price, not its list price?
Mini's long, repeated instructions qualify for the prompt-cache discount. Estimated from OpenAI's prices, that brings mini from $1.46 to about 42–77¢ per 1,000, and a mostly idle $5/h machine ($1.15) goes from cheaper to dearer. The run didn't record it, so measure your cached tokens.
Next up: Chapter 32 starts on resilience: what to do when a model call fails, and how retries can make an outage worse unless they wait the right way.