GPT-6 Luna for C#: The Cheap First Stage for High-Volume Agents

GPT-6-Luna-for-CSharp-The-Cheap-First-Stage-for-High-Volume-Agents

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:

  1. Triage 100 support tickets with Luna, and measure accuracy, missed escalations, latency and cost.
  2. Run the Terra invoice agent on Luna and on Terra, from a PDF and from a PNG of the same invoice, side by side.
  3. Escalate: Luna triages everything, and only the tickets a rule flags go to Terra for a drafted reply.
  4. 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:

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:

Example 1: triage at volume

Each ticket gets one request, and the answer is strict structured output that lands in a C# record:

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.

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:

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:

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:

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.

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#.

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.

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.

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

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.

Taswar Bhatti Avatar

Written by