Most of the work an AI agent does all day is dull. Sort this ticket. Pull the fields out of that invoice. Summarize this thread for the queue. It isn’t hard work, but there’s a lot of it, and if every item goes to your best model, the bill grows with the queue.
I’ve written about the bigger GPT-6 models already: GPT-6 Astra for the hardest reasoning, GPT-6.1 Sol for production agents, and GPT-6.1 Terra for screens and images. This post is about the smallest one, GPT-6 Luna. Luna’s story isn’t that it’s small. It’s how much useful work you get per dollar when the volume is high. So the questions that matter are where the cheap model is good enough, how you prove it, and how you send the rest somewhere else.
Sources for the facts in this post: Microsoft’s Astra, Sol and Luna announcement, OpenAI’s GPT-6 Sol and Luna announcement, and the Microsoft Learn pages on reasoning models and prompt caching. The full sample is on GitHub: GPT-6-Luna-Csharp-Sample.
What Luna is
GPT-6 Luna became generally available in Microsoft Foundry on September 22, on the same day as GPT-6 Sol. Microsoft calls it Sol’s smaller, faster sibling, built for high-volume work, and lists extraction, summarization, request routing and routine customer interactions as its jobs. Its advice is to keep deeper reasoning for the steps that need it instead of sending every task to the same model.
OpenAI says it trained Sol and Luna with similar methods to Astra, so the advances behind Astra carry down to the cheaper models. In the OpenAI API it’s gpt-6-luna. In Foundry, you deploy it like any other Azure OpenAI model, and Microsoft Learn says no access request is needed. Whether you can deploy it is a separate question from whether you have enough quota and capacity, though: check that your region offers it, and that your quota covers the volume you plan to send.
Where it sits in the GPT-6 family
| Model | Microsoft’s role for it | Input | Output |
|---|---|---|---|
| GPT-6 Astra | The hardest reasoning and high-stakes work | $10.00 | $50.00 |
| GPT-6.1 Sol | Production agents and complex recurring workflows | $2.00 | $10.00 |
| GPT-6.1 Terra | High-volume visual and on-screen work | $1.00 | $5.00 |
| GPT-6 Luna | High-volume data and preparatory work | $0.10 | $0.50 |
Prices are Global Standard, short context, in USD per million tokens, from Microsoft’s announcements for each model. GPT-6.1 Sol kept GPT-6 Sol’s input and output prices.
Luna costs a tenth of Terra and a twentieth of Sol. That gap is the whole reason this post exists.
Here’s how I’d split work across the four posts:
| Work | Start with |
|---|---|
| Sorting, routing and tagging a queue | Luna |
| Pulling fields out of text and documents | Luna |
| Summaries at volume | Luna |
| One typed yes/no or label | The Decisions API (on Luna) |
| Screenshots, photos and scanned pages | Terra, or Luna if it measures well enough |
| Computer use | Terra, or Luna at higher effort for simpler screens |
| Multi-step agents and complex recurring workflows | Sol |
| The hardest reasoning and high-stakes analysis | Astra |
“Start with” is the point. Try the cheapest model that might work, measure it on your own data, and move up only where it falls short.
What OpenAI’s numbers say
OpenAI published several comparisons for Luna. These are vendor-run evaluations, and most of them use Luna at high or max reasoning effort, which costs more per task than the low effort I use in the sample.
- Business workflows (AutomationBench). At high effort, Luna scores 5.4 percentage points higher than GPT-5.6 Luna, at 58% lower cost per task.
- Factuality. At higher effort levels, Luna matches GPT-5.6 Sol on OpenAI’s internal factuality evaluation, at about a hundredth of the cost.
- Coding (DeepSWE v1.1). At max effort, Luna scores 66.6%, which OpenAI says is comparable to Claude Opus 5 and Claude Fable 5 at medium effort, at 93% and 96% lower cost per task.
- Computer use (OSWorld 2.0). At max effort, Luna beats GPT-5.6 Sol at medium effort for a tenth of the cost.
OpenAI also cut the price: GPT-6 Luna costs half of what GPT-5.6 Luna did, $0.10 in and $0.50 out against $0.20 and $1.20.
Two things to take from this. First, Luna isn’t only a classifier. At max effort it does real coding and computer-use work, so it’s worth trying for jobs like summarizing a pull request, suggesting unit tests for a small change, or reading an exception log for the likely cause. A benchmark score shows what it can do, not how it will do on your code, so test those jobs before you rely on them. Second, those results come from turning the effort up, so “Luna is cheap” and “Luna scores 66.6%” aren’t the same run. Measure the effort level you’ll actually use.
The spec sheet
Microsoft Learn lists gpt-6-luna (version 2026-09-22) with the same spec sheet as the rest of the GPT-6 family:
| Feature | GPT-6 Luna | Source |
|---|---|---|
| Context window | 1,050,000 tokens | Learn |
| Maximum input / output | 922,000 / 128,000 tokens | Learn |
| Input / output | Text and images / text | Learn |
| APIs | Chat Completions and Responses | Learn |
| Decisions API | Yes, as its only supported model | Decisions API docs |
| Tools, structured outputs, streaming | Yes | Learn |
| Reasoning effort and verbosity | Yes | Learn |
| logprobs, temperature, top_p | Yes | Learn |
| Computer use | Improved over GPT-5.6 Luna | Foundry model catalog |
Because it takes text and images, it also reads PDFs through the Responses API. The sample uses both.
On computer use: the catalog lists it, and OpenAI’s OSWorld result above backs it up. I’m not turning this post into a computer-use tutorial, because the Terra post covers that pattern. Whether Luna is good enough for a given screen depends on how complex the task is, the effort you run it at, and how often a mistake is acceptable.
One gap in the docs: Learn doesn’t say which reasoning effort values the GPT-6 models accept. Its note about none lists only GPT-5.x models. The sample defaults to low, and example 4 tests the others. On my deployment, Luna accepted none; the results section has the comparison.
What it costs
Microsoft’s prices in USD per million tokens:
| Deployment | Context | Input | Cached input | Cached writes | Output |
|---|---|---|---|---|---|
| Global Standard | Short | $0.10 | $0.01 | $0.125 | $0.50 |
| Global Standard | Long | $0.20 | $0.02 | $0.25 | $0.75 |
| Data Zone Standard (US) | Short | $0.11 | $0.011 | $0.1375 | $0.55 |
| Data Zone Standard (US) | Long | $0.22 | $0.022 | $0.275 | $0.825 |
| Data Zone Standard (EU) | Short | $0.12 | $0.012 | $0.15 | $0.60 |
| Data Zone Standard (EU) | Long | $0.24 | $0.024 | $0.30 | $0.90 |
Standard deployments are available in all 28 Global regions and in the US and EU Data Zones. Unlike Terra, there’s no APAC Data Zone listed for Luna, and Microsoft’s announcement doesn’t list Provisioned Throughput for it.
Look at the Cached writes column. It’s new, and it matters for every GPT-6 model, not just Luna.
Prompt caching changed
Prompt caching used to be free money: repeat the same long prefix and pay less for it. On GPT-5.6 and later models, Microsoft’s docs say writing to the cache can be billed too, and usage reports writes separately as cache_write_tokens. For Luna, a cache write costs $0.125 per million tokens, a bit more than plain input, while a cache read costs $0.01, a tenth of plain input. OpenAI describes the read discount the same way: 90% off cached input.
So caching still pays, but only if the prefix you write is read many times. What to do about it:
- Put the stable part first. Instructions, tool definitions and the output schema are all part of the cached prefix. The part that changes, the ticket or document, goes last.
- Clear 1,024 tokens. Caching only starts at 1,024 tokens, and the first 1,024 must be identical. The sample’s triage policy is about 1,480 tokens for that reason.
- Use prompt_cache_key. Requests with the same key and prefix match the cache more reliably. Above about 15 requests per minute for one key and prefix, some requests miss the cache, so spread heavy traffic across a few keys. That’s a cache-matching behavior, not a rate limit: the requests still succeed, they just pay full price.
- Write once, then read. If you start 100 requests at the same moment, they can all miss the cache and all write it. The sample sends the first request alone, then runs the rest in parallel, and prints the cache numbers for each part.
- Use explicit breakpoints when you want to choose where the cached prefix ends. The Responses API accepts prompt_cache_breakpoint on input_text, input_image and input_file content. A separate setting, prompt_cache_options.mode, decides which breakpoints count: in the default implicit mode the service adds one of its own on the latest message, and in explicit mode it uses only the ones you mark. The sample doesn’t need either, because its prefix is simply everything before the ticket.
OpenAI also says that on GPT-6, changing the reasoning effort or turning tools on and off no longer breaks the cache. That’s from OpenAI’s API announcement, and I haven’t seen it in Microsoft’s docs yet.
Luna or the Decisions API?
I’ve already used Luna once on this blog, in the Decisions API walkthrough, where gpt-6-luna is the only supported model. That’s a different tool for a different job:
| Use the Decisions API when | Use Luna through the Responses API when |
|---|---|
| You need one typed answer: yes or no, a label, a number | You need text: a summary, an extraction, an explanation |
| You don’t want any generated prose | You want structured output with several fields |
| The question fits its three question types | You need tools, PDFs or images |
The sample below needs several fields per ticket, a tool and a PDF, so it uses the Responses API.
The sample
The sample is a .NET 10 console app in the same style as the Terra one: one Program.cs, HttpClient against /openai/v1/responses, user secrets, and an API key or Entra ID. It has four examples:
- Triage 100 support tickets with Luna, and measure accuracy, missed escalations, latency and cost.
- Run the Terra invoice agent on Luna and on Terra, from a PDF and from a PNG of the same invoice, side by side.
- Escalate: Luna triages everything, and only the tickets a rule flags go to Terra for a drafted reply.
- Sweep reasoning effort: the same tickets at none, low and medium.
The tickets, the triage policy and the invoice are made up: Contoso Sync Pro, Fabrikam and everyone else in them are fictional. Each ticket records the category a person would pick and whether a person should see it. The app uses those answers only to score the results; they never go into the prompt.
The sample waits for each complete response, because it processes a queue. For an interactive screen, the Responses API can stream the answer as it’s generated instead.
Setup
Clone the repo and set your settings. The escalation deployment is your Terra (or Sol) deployment on the same resource:
|
1 2 3 4 5 |
git clone https://github.com/taswar/GPT-6-Luna-Csharp-Sample.git cd GPT-6-Luna-Csharp-Sample dotnet user-secrets set “AZURE_OPENAI_ENDPOINT” “https://YOUR-RESOURCE-NAME.openai.azure.com” dotnet user-secrets set “AZURE_OPENAI_DEPLOYMENT” “gpt-6-luna” dotnet user-secrets set “AZURE_OPENAI_ESCALATION_DEPLOYMENT” “YOUR-TERRA-DEPLOYMENT” |
To use Entra ID, leave out the API key and run az login. To use a key, set AZURE_OPENAI_API_KEY the same way. If you check your settings with dotnet user-secrets list, it prints the key in plain text, so don’t paste that output anywhere public.
Run one example at a time, and limit the number of tickets while you try it out:
|
1 |
dotnet run — 1 10 |
|
1 |
dotnet run — all |
Example 1: triage at volume
Each ticket gets one request, and the answer is strict structured output that lands in a C# record:
|
1 |
record TicketTriage(string Category, string Priority, bool NeedsHuman, string Confidence, string Summary); |
The schema limits category, priority and confidence to fixed values with JSON Schema enum, so the model can’t invent a category. This is the core of the example: the request, and the write-once-then-read pattern for the cache.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
async Task TriageOneAsync(int index, Usage usage) { var ticket = tickets[index]; var body = new JsonObject { [“model”] = deployment, // The policy is the same for every ticket, so it forms the cached prefix with the schema. [“instructions”] = policy, [“prompt_cache_key”] = “contoso-triage-policy-v1”, [“input”] = new JsonArray { new JsonObject { [“role”] = “user”, [“content”] = new JsonArray { new JsonObject { [“type”] = “input_text”, [“text”] = ticket.ToPrompt() } } } }, // Nothing chains off these responses, so don’t store them. [“store”] = false, [“reasoning”] = new JsonObject { [“effort”] = effort }, [“text”] = new JsonObject { [“format”] = TicketTriageFormat(), [“verbosity”] = “low” } }; var stopwatch = Stopwatch.StartNew(); using var response = await api.PostAsync(body, cancellationToken); stopwatch.Stop(); usage.Add(response.RootElement); var triage = JsonSerializer.Deserialize<TicketTriage<(ResponsesApi.ReadOutputText(response.RootElement), Json.SnakeCase) ?? throw new InvalidOperationException($“No triage for {ticket.Id}”); results[index] = new TriageResult(ticket, triage, stopwatch.Elapsed); } var run = Stopwatch.StartNew(); await TriageOneAsync(0, first); using var gate = new SemaphoreSlim(4); await Task.WhenAll(Enumerable.Range(1, tickets.Count – 1).Select(async index =< { await gate.WaitAsync(cancellationToken); try { await TriageOneAsync(index, rest); } finally { gate.Release(); } })); |
Three details in there:
- instructions holds the policy, about 1,480 tokens of rules and examples. It’s identical for every ticket, so with the schema it forms the cached prefix.
- store is false. Nothing chains off these responses, so there’s no reason to keep them for 30 days.
- Four at a time. A SemaphoreSlim keeps 4 requests in flight. Raise it until you see 429 responses, which the client retries with backoff.
The report goes further than one accuracy number, because one number hides the mistakes that matter:
- Precision, recall and F1 per category. Precision is how many of the tickets Luna put in a category belong there. Recall is how many of the tickets that belong there it found. A category with good precision and poor recall is quietly losing tickets to its neighbors.
- Missed escalations. These are tickets a person should see that Luna handled alone, such as a data-loss report filed as a sync problem. This is the expensive mistake, and a high overall accuracy can hide it.
- Latency. The median (p50) and the slowest 5% (p95) per request.
- Cache numbers for the first request and the rest separately, so you can see the cache write on the first request and the reads after it.
- Cost per 1,000 tickets and per 1,000 correctly classified tickets. A cheap wrong answer isn’t a saving, so the second number is the honest one.
Example 2: is Luna good enough for the invoice agent?
This runs the invoice agent from the Terra post unchanged: the same check_supplier tool and the same strict schema. It runs four times: Luna and Terra, each reading the invoice as a PDF and as a PNG. Then it prints every field side by side and counts how many match Terra’s PDF result.
The model doesn’t run the tool itself. It asks for check_supplier with an argument, the C# code runs the lookup, and the result goes back as a function_call_output:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// The tool loop: the model asks for check_supplier, this code runs it and sends the result back. // previous_response_id chains the turns; the loop stops when the model asks for no more calls. for (var round = 1; round <= 5; round++) { var outputs = RunToolCalls(response.RootElement, deployment); if (outputs.Count == 0) { break; } var next = new JsonObject { [“model”] = deployment, [“previous_response_id”] = response.RootElement.GetProperty(“id”).GetString(), [“input”] = outputs }; AddToolsAndFormat(next, effort); response.Dispose(); response = await api.PostAsync(next, cancellationToken); usage.Add(response.RootElement); } |
The PDF goes in as an input_file, and the PNG as an input_image with detail set to high so the small print is readable. They don’t cost the same: Microsoft’s docs say the model gets both the extracted text and an image of each page of a PDF, so a PDF can use more input tokens than a picture of the same page. The example prints the tokens for each, so you can see the difference instead of guessing.
This is the test I’d run before moving any agent to a cheaper model: same input, same code, compare the output field by field.
Example 3: escalate the hard cases
Luna triages every ticket, and a rule decides what goes to the bigger model:
|
1 2 3 4 5 6 7 8 9 10 |
// The escalation rule lives in code, not only in the model’s own flags: data loss and p1 tickets always go to a person, // whatever Luna says about its confidence or needs_human. static class Routing { public static bool ShouldEscalate(TicketTriage triage) =< triage.NeedsHuman || triage.Confidence == “low” || triage.Category == “data_loss” || triage.Priority == “p1”; } |
The rule doesn’t rely only on what the model says about itself. A model’s confidence isn’t a measured probability of being right, so data loss and p1 tickets always escalate, even when Luna is sure it can handle them. The rule can still miss a ticket that Luna put in the wrong category entirely, which is why example 1 counts missed escalations.
The escalated tickets go to Terra for a drafted reply. The app prints the escalation rate and compares the cost of this run with an estimate of drafting a reply for every ticket on Terra.
Example 4: which reasoning effort?
Microsoft’s docs don’t say which reasoning effort values GPT-6 models accept, so this example tries the same 20 tickets at none, low and medium and prints accuracy, missed escalations, reasoning tokens, cost and latency for each. If the deployment rejects a value, the row says so instead of the run failing. Higher effort isn’t automatically better for a job this simple: it costs more reasoning tokens, and it only earns them if accuracy goes up.
Estimating cost
The prices live in one place, so you can change them for your region:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// Global Standard short-context prices in USD per million tokens, from Microsoft’s announcements. Edit them for your region. record Price(decimal Input, decimal CachedInput, decimal CacheWrite, decimal Output); static class Prices { static readonly Dictionary<string, Price< Known = new() { [“luna”] = new(0.10m, 0.01m, 0.125m, 0.50m), [“terra”] = new(1.00m, 0.05m, 1.25m, 5.00m), // GPT-6.1 Sol. GPT-6 Sol had the same input and output prices, with $0.20 cached input. [“sol”] = new(2.00m, 0.10m, 2.50m, 10.00m), [“astra”] = new(10.00m, 1.00m, 12.50m, 50.00m) }; // Matches the price by the deployment name, so name your deployments after the model. public static Price? For(string deployment) =< Known.FirstOrDefault(p =< deployment.Contains(p.Key, StringComparison.OrdinalIgnoreCase)).Value; // An estimate. Cached and cache-write tokens are billed at their own rates; the rest of the input at the input rate. public static decimal? Cost(string deployment, Usage usage) { if (For(deployment) is not { } price) { return null; } var uncached = Math.Max(0, usage.Input – usage.Cached – usage.CacheWrite); return (uncached * price.Input + usage.Cached * price.CachedInput + usage.CacheWrite * price.CacheWrite + usage.Output * price.Output) / 1_000_000m; } } |
Every cost the sample prints is an estimate. Microsoft’s docs say cache writes “can incur charges”, and the formula assumes cached and cache-write tokens are part of input_tokens, billed at their own rates instead of the input rate. Check it against your Azure bill. The cache-write prices for Terra ($1.25) and Sol ($2.50) come from their Microsoft announcements.
Results
I ran dotnet run — all against a gpt-6-luna deployment and a gpt-6.1-terra deployment on the same resource. These are my numbers on made-up tickets and one made-up invoice, from one run. They show the method, not how Luna will do on your data. OpenAI’s benchmark results earlier in the post are a separate thing: vendor-run, at higher effort, on different tasks.
Example 1: 100 tickets at effort low
| Metric | Luna |
|---|---|
| Category accuracy | 98 / 100 |
| Macro F1 | 0.98 |
| Missed escalations | 2 of 32 |
| Extra escalations | 0 |
| Latency p50 / p95 | 2.2 s / 3.0 s |
| Whole run, 4 requests at a time | 62.7 s |
| Cost per 1,000 tickets | about $0.046 |
| Cost per 1,000 correctly classified tickets | about $0.047 |
Both wrong categories were tickets I wrote to be unclear. “Slow and keeps logging me out” is a performance problem and a sign-in problem at once, and Luna picked sign-in. “Urgent!!!” says almost nothing, and Luna filed it as other. Luna rated both low confidence, so the escalation rule sent both to a person anyway. A wrong category with an honest confidence is a mistake the routing can catch.
The two missed escalations are more interesting. Both are “Need a VAT invoice” tickets, which I had marked for a person because the policy says changes to an invoice need one. Luna read them as a routine billing question: p4, high confidence, no person needed. That isn’t really a model error. It’s a gap in my policy, which never says whether adding a VAT number counts as changing an invoice. This is what the missed-escalation count is for: it points at the rule you need to write down more clearly.
The cache:
| First request | Other 99 requests | |
|---|---|---|
| Input tokens | 1,452 | 143,494 |
| Cached tokens | 0 | 138,798 |
| Cache-write tokens | 1,449 | 4,399 |
The first request wrote the policy to the cache, and 96.7% of the input on the other 99 requests was read from it. A few later requests wrote the cache again, about three more prefixes’ worth. Running 4 at a time, some requests can still land before the cache is ready for them. Across the run, cache writes cost about $0.0007 and cached reads about $0.0014, and output cost more than everything else together: about $0.0025 of the $0.0046 total.
At under five cents per thousand tickets, the question for Luna on this job isn’t cost. It’s whether your policy is clear enough.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
=== Example 1: triage 100 tickets on gpt–6–luna (effort low) === Category Expected Predicted Precision Recall F1 billing 15 15 1.00 1.00 1.00 data_loss 11 11 1.00 1.00 1.00 feature_request 15 15 1.00 1.00 1.00 other 12 13 0.92 1.00 0.96 performance 16 15 1.00 0.94 0.97 sign_in 15 16 0.94 1.00 0.97 sync 16 15 1.00 0.94 0.97 Accuracy: 98/100, macro F1 0.98 T–1038: expected performance, got sign_in (low) “Slow and keeps logging me out” T–1050: expected sync, got other (low) “Urgent!!!” Escalated 30, expected 32: missed 2, extra 0 Missed T–1076: billing, p4, high “Need a VAT invoice” Missed T–1084: billing, p4, high “Need a VAT invoice” Latency per request: p50 2.2s, p95 3.0s, whole run 62.7s First request: calls=1 input=1452 cached=0 cache_write=1449 output=47 reasoning=0 Other requests: calls=99 input=143494 cached=138798 cache_write=4399 output=4938 reasoning=131 [gpt–6–luna] calls=100 input=144946 cached=138798 cache_write=5848 output=4985 reasoning=131 Estimated cost: $0.0046 total, $0.05 per 1,000 tickets, $0.05 per 1,000 correctly classified tickets |
Example 2: the invoice agent on Luna and Terra
| Luna PDF | Luna PNG | Terra PDF | Terra PNG | |
|---|---|---|---|---|
| Fields matching Terra PDF | 7 / 9 | 8 / 9 | 9 / 9 (reference) | 7 / 9 |
| Needs approval | False | True | True | False |
| Issues flagged | 0 | 0 | 1 | 0 |
| Input tokens | 4,193 | 1,991 | 4,201 | 1,999 |
| Cost per 1,000 invoices (estimated) | $0.57 | $0.25 | $5.93 | $2.62 |
| Time | 6.2 s | 5.0 s | 5.0 s | 5.4 s |
Three things stand out.
Reading was perfect; judging wasn’t. All four runs extracted every field the same way: supplier, invoice number, date, PO number, currency and the $6,545.96 total. Both models called check_supplier correctly. The differences are in the two fields that need judgment. The total is over 5,000, so the invoice needs approval, yet Luna reading the PDF and Terra reading the PNG both said it didn’t. A rule this simple belongs in your code, not in the prompt: compute total < 5000 in C# and keep the model’s answer as a cross-check. Only Terra reading the PDF flagged the footer that says the invoice is a sample, as it did in the Terra post.
The PDF cost twice as many tokens as the PNG. About 4,200 input tokens against about 2,000, on both models. That matches Microsoft’s description: for a PDF, the model gets the extracted text and an image of each page. For a one-page invoice, the image alone was enough.
The PDF requests wrote to the cache and nothing read it back. About 4,190 tokens were written as cache on each PDF run, and the second request in the tool loop read none of them. Cache writes cost 1.25 times the input price, so on Terra that added about a fifth to the cost of the PDF run: $5.93 per 1,000 invoices instead of about $4.88. A one-off document is the case where caching costs you.
Luna reading the PNG was the cheapest run by far, a tenth of Terra reading the same image, and it matched the reference on 8 of 9 fields. For extraction, Luna is enough. For the approval decision, neither model is a replacement for one line of C#.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[gpt–6–luna] check_supplier(“Fabrikam Office Supplies”) –< {“found”:true,“supplier_id”:“V-1042”,“status”:“active”} [gpt–6–luna] check_supplier(“Fabrikam Office Supplies”) –< {“found”:true,“supplier_id”:“V-1042”,“status”:“active”} [gpt–6.1–terra] check_supplier(“Fabrikam Office Supplies”) –< {“found”:true,“supplier_id”:“V-1042”,“status”:“active”} [gpt–6.1–terra] check_supplier(“Fabrikam Office Supplies”) –< {“found”:true,“supplier_id”:“V-1042”,“status”:“active”} Field Luna PDF Luna PNG gpt–6.1–terra PDF gpt–6.1–terra PNG Supplier Fabrikam Office Supplies Fabrikam Office Supplies Fabrikam Office Supplies Fabrikam Office Supplies On file True True True True Invoice number INV–20481 INV–20481 INV–20481 INV–20481 Invoice date 2026–10–12 2026–10–12 2026–10–12 2026–10–12 PO number PO–7734 PO–7734 PO–7734 PO–7734 Currency USD USD USD USD Total 6545.96 6545.96 6545.96 6545.96 Needs approval False True True False Issues 0 0 1 0 Matches ref. 7/9 8/9 9/9 7/9 (The reference is gpt–6.1–terra PDF.) [Luna PDF: gpt–6–luna] calls=2 input=4193 cached=0 cache_write=4187 output=93 reasoning=0 elapsed=6.2s cost=$0.0006 ($0.57 per 1,000 invoices) [Luna PNG: gpt–6–luna] calls=2 input=1991 cached=0 cache_write=0 output=93 reasoning=0 elapsed=5.0s cost=$0.0002 ($0.25 per 1,000 invoices) gpt–6.1–terra PDF issue: Document identifies itself as a sample for a code demo involving fictitious companies. [gpt–6.1–terra PDF: gpt–6.1–terra] calls=2 input=4201 cached=0 cache_write=4195 output=136 reasoning=25 elapsed=5.0s cost=$0.0059 ($5.93 per 1,000 invoices) [gpt–6.1–terra PNG: gpt–6.1–terra] calls=2 input=1999 cached=0 cache_write=0 output=125 reasoning=30 elapsed=5.4s cost=$0.0026 ($2.62 per 1,000 invoices) |
Example 3: escalation
Luna triaged 20 tickets, the rule escalated 4 (20%), and none that should have been escalated were missed. Terra drafted the four replies: a refund request, an SSO outage for a whole company, and two data-loss tickets. The drafts are specific to each ticket, say what happens next, and don’t promise refunds or restores, which is what the instructions asked for. The data-loss reply even tells the customer not to touch the file while it’s investigated.
The run cost about $0.0041, against an estimated $0.0165 to draft a reply for every ticket on Terra, so about a quarter. Luna’s share was under a tenth of a cent, and 99.8% of its input came from the cache that example 1 had written a few minutes earlier.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
=== Example 3: triage 20 tickets on gpt–6–luna, escalate the hard ones === T–1001 [billing, p3, high] Cancel and refund Draft: Hi Ben, I’ve noted your request to cancel Wide World Importers’ Sync Pro subscription and for a refund of the unused portion of your annual plan. I’ll send th… T–1008 [sign_in, p1, high] SSO loop Draft: Hi Viktor, I’m sorry that SSO sign–in is looping for everyone at Fourth Coffee. We’re escalating ticket T–1008 as a priority for investigation. Could you conf… T–1010 [data_loss, p2, medium] Restored wrong version Draft: Hi Tariq, I’m sorry the version restore replaced today’s budget spreadsheet work. Please avoid editing or restoring the file again while we investigate. I’ll … T–1011 [data_loss, p2, medium] Files disappeared from folder Draft: Hi Sam, I’m sorry files have disappeared from Client contracts. I’ll escalate ticket T–1011 to our data–loss team to investigate, including why the files aren’… Escalated 4 of 20 tickets (20%); 0 that should have been escalated weren‘t. [gpt–6–luna] calls=20 input=28990 cached=28930 cache_write=0 output=1074 reasoning=102 [gpt–6.1–terra] calls=4 input=553 cached=0 cache_write=0 output=549 reasoning=148 elapsed=28.6s Estimated cost: $0.0041 for this run, against about $0.0165 to draft a reply for every ticket on gpt–6.1–terra. |
Example 4: reasoning effort
| Effort | Accuracy | Missed escalations | Reasoning tokens | Cost per 1,000 | p50 / p95 |
|---|---|---|---|---|---|
| none | 20 / 20 | 0 | 0 | $0.05 | 2.3 s / 3.0 s |
| low | 20 / 20 | 0 | 0 | $0.04 | 2.1 s / 2.5 s |
| medium | 20 / 20 | 0 | 1,850 | $0.10 | 2.6 s / 3.5 s |
Luna accepted none. On these 20 tickets, low behaved the same way: no reasoning tokens and the same answers. medium spent 1,850 reasoning tokens, more than doubled the cost and added latency, and got nothing more right. For triage this simple, none or low is the setting. The small cost difference between none and low comes from caching: none ran first, so it paid for the cache writes.
Twenty tickets is a small sample, so run this on a few hundred of your own before you settle on a value.
|
1 2 3 4 5 |
=== Example 4: 20 tickets at each reasoning effort on gpt–6–luna === Effort Accuracy Missed esc. Reasoning Cost/1k p50 p95 none 20/20 0 0 $0.05 2.3s 3.0s low 20/20 0 0 $0.04 2.1s 2.5s medium 20/20 0 1850 $0.10 2.6s 3.5s |
What the numbers say
- Luna is good enough for routine triage. 98 of 100 right, and both misses were tickets a person would also hesitate over, rated low confidence.
- Your policy matters more than your model. The only missed escalations came from a rule I never wrote down clearly.
- Put hard rules in code. Two of four invoice runs, across both models, got a simple threshold wrong.
- Images can be cheaper than PDFs. The PNG used half the tokens and lost nothing on extraction.
- Caching saves money on repeated prefixes and costs money on one-off documents.
- Higher effort didn’t help this job. medium doubled the cost for the same answers.
What to measure before you ship
- Accuracy on your data, per category. My tickets are made up and fairly clean. Label a few hundred of your own, and look at precision and recall per category, not just overall accuracy.
- Missed escalations first. This is the number to drive toward zero, even if it raises the escalation rate. A ticket a person should have seen and didn’t costs more than a ticket Terra didn’t need to see.
- The escalation rate. If Luna escalates half your queue, the savings shrink. If it escalates almost nothing, check what it’s letting through.
- Cost per correct answer. Cost per request flatters a cheap model that’s often wrong. Divide by the answers you’d actually keep.
- Cache reads against cache writes. Watch cached and cache_write across a real day of traffic. A prefix that keeps changing writes the cache over and over and saves you nothing.
- Reasoning effort. Run example 4 on your own tickets. OpenAI’s best Luna numbers come from high and max effort, which cost more, and a simple job may not need them.
- Latency at your concurrency. Four requests at a time is a starting point. Raise it, watch p95 and the 429 responses, and keep a retry with backoff.
Final thoughts
Luna is the model I’d put in front of a queue. It reads everything, sorts it, pulls out the fields, and sends the few items it isn’t sure about to Terra or Sol. Most of the volume never reaches the expensive model.
The sample gives you the pieces of that pattern: a cheap, cached first pass with structured output, a field-by-field comparison to prove the cheap model is good enough for a job, an escalation rule that lives in your code, and the measurements to know whether it’s working. Don’t try to make Luna do Astra’s job. Let it handle the routine volume and send the rest up. Swap in your own tickets, your own policy and your own prices, and measure.
Resources
- GPT-6-Luna-Csharp-Sample on GitHub
- GPT-6 Astra, Sol and Luna for production agents in Microsoft Foundry (Microsoft)
- Introducing GPT-6 Sol and Luna (OpenAI)
- Azure OpenAI reasoning models (Microsoft Learn)
- Prompt caching (Microsoft Learn)
- GPT-6.1 Terra for C#: Fast Vision Agents at Lower Cost
- Azure OpenAI Decisions API walkthrough
Building AI features in C#? I write about practical, no-hype prompt engineering and Azure AI patterns for .NET developers. Check out Prompt Engineering for .NET Developers — free, no Python required. You can also subscribe for more posts.

Leave a Reply