Asking a language model to pick a category usually ends with code that parses its prose, hopes the label is spelled the way you expected and breaks the day the model adds a polite sentence before the answer. When I built Linkmortem, I solved that by asking Gemma 4 for a single JSON object with a fixed list of page kinds and validating it in code. Microsoft Foundry now offers a more direct route for this kind of question, the Azure OpenAI Decisions API, which returns typed answers instead of generated text.
This post is for Python developers who classify, filter or route text on Azure and want to know whether the Decisions API fits their case. You'll need basic Python, an Azure subscription and a Foundry resource where you can deploy a model. I cover what the API is, how its three question types work, what it costs, its limits, the mistakes that are easy to make and a minimal example. I don't cover the JavaScript SDK, image inputs in depth or how to tune thresholds with labeled data. Everything here refers to the /openai/v1/decisions endpoint with the gpt-6-luna model and the openai Python package 3.26.0 or later, and I checked it against the official documentation on November 10, 2026.
What the Azure OpenAI Decisions API is
The Decisions API evaluates text, images or both and returns typed answers that your code can read directly. According to the documentation, it's meant for classifying content, routing requests and prioritizing work.
Here's an example of where it helps. Imagine a support inbox that receives "I was charged twice for my order." With a chat model, you'd ask which department should handle it and then parse a sentence. With the Decisions API, you send the message with a list of departments and get back a field called choice with the value billing, plus a probability for every option.
How the three question types work
Every request carries an input, which is the content to evaluate, and a list of questions. Each question has a type, a name and instructions, and the type decides the shape of the answer.
| Type | What you ask | Extra fields in the question | What comes back |
|---|---|---|---|
predicate | Whether a condition is true | None | probability, from 0 to 1 |
choice | Which option fits, with no order between options | choices, each with value and description | choice, a probabilities list and confidence |
score | Where the input sits on ordered levels | levels, each with label and description | score, a probabilities list and confidence |
The type definitions in the openai Python package add a detail the article doesn't spell out, which is that a choice question takes between 2 and 255 options and each one must be unique.
The score field deserves a closer look, because the documentation describes it as "a probability-weighted average of the indices, so it can fall between levels." In the illustrative response in the docs, a bug that fails in Safari but works in Chrome gets a score of 1.1 across three levels numbered from 0 to 2, with a probability of 0.7 for the middle level. If your code expects a whole number, round it on purpose or read the probabilities instead.
Any question can also come back as a refusal, with the question's name and no result, while the other questions in the same request still get answers. The documentation is explicit that "a refusal is different from a predicate answer with a low probability", so your code should check each answer's type before reading it and decide what happens with refusals, such as sending the input to a person.
When one decision depends on another, the docs recommend separate requests. Their example checks first whether a product is damaged and asks for a repair category only when it is.
Status, requirements and regions
The Decisions API is generally available in Microsoft Foundry, as stated in What's new in Microsoft Foundry for October 2026, published on November 5, 2026, and in Microsoft's general availability announcement. The model behind it, gpt-6-luna, joined the generally available lineup on September 22, 2026, according to the GPT-6 announcement on the Azure blog.
To use it, you need these pieces.
- An Azure subscription and a Microsoft Foundry resource.
- A deployment of
gpt-6-luna, which the docs list as the only supported model today. - A region where the API has rolled out. The documentation says it's "rolling out to regions where
gpt-6-lunais available" and that "East US 2 is excluded from the rollout." - The
openaipackage 3.26.0 or later for Python, or 7.30.0 or later for Node.js. - Either an API key or Microsoft Entra ID, since the docs say the API supports both. The Decisions page doesn't list a specific role, so check the permissions of the identity you use before you blame the code. The region point is easy to miss. The region availability page for Foundry Models lists
gpt-6-lunaversion 2026-09-22 in East US 2 for Global Standard and Data Zone Standard deployments, so you can deploy the model there and still find that the Decisions API isn't available on it.
Costs and quotas
The Decisions API runs on gpt-6-luna, so the model's token prices apply. The Azure blog lists these prices in USD per million tokens for a Global Standard deployment with short context.
| Item | Price |
|---|---|
| Input | $0.10 |
| Cached input | $0.01 |
| Cache writes | $0.125 |
| Output | $0.50 |
The same table shows higher prices for long context and for Data Zone deployments in the United States and the European Union. When I checked the Azure OpenAI pricing page on November 10, 2026, it listed GPT-6 Luna without figures, so confirm the price for your region in the pricing calculator before you estimate a budget. The illustrative response in the documentation reports 162 input tokens and 0 output tokens for a single yes or no question, which suggests most of the cost comes from the input, but measure it with your own prompts before you rely on that.
The Decisions page documents request limits, not rate limits. Each request supports up to 400 questions and up to 128 images, with a combined image size of 32 MB in PNG, JPG, WebP or GIF format. The page doesn't mention rate limits, so check the quota assigned to your deployment in Foundry.
Security and privacy
The Decisions API runs on gpt-6-luna, a model sold by Azure, so the rules for those models apply. The page on data, privacy and security states that prompts and completions "are NOT available to OpenAI or other providers of Models sold by Azure" and that they "are not used to train, retrain, or improve the base models." By default, abuse monitoring keeps a data store where prompts and completions can go to human review. If your organization has approval for modified abuse monitoring, that storage and human review aren't performed.
Where the data is processed depends on the deployment type. With a Global deployment, prompts and responses "may be processed in any geography where the relevant model sold by Azure is deployed", while with a Data Zone deployment they "may be processed in any geography within the specified data zone, as defined by Microsoft", such as anywhere within the United States for a resource there. If your input contains personal data, pick the deployment type with that in mind.
Two more habits help. Images can be a base64 data URL or a public HTTPS URL that the service can download "without access to a private network or your browser's cookies", so never point it to a link that exposes private files. And if the input comes from the web, my take is that typed answers narrow what an injected instruction can change, since the answer has to be one of your options, but nothing in the documentation says they stop it from pushing the model toward the wrong option. I wrote about that risk in my post on indirect prompt injection.
Decisions API or structured outputs
The Decisions API isn't a general replacement for other ways of getting structured data. The documentation draws the line this way.
| You need | Use |
|---|---|
| A yes or no probability, one option from a fixed list or a position on ordered levels | Decisions API |
| An object that follows your own JSON Schema, such as extracted fields or a written explanation | Structured outputs with the Responses API |
| The model asking your code to run a tool with arguments | Function calling |
It also matters which model you want. The Decisions API only works with gpt-6-luna today, while Linkmortem runs on Gemma, an open weights model, so the API wouldn't drop into that project without changing the model. Microsoft also lists a separate model called Microsoft-Decision-1 in Foundry Models. I haven't tested it, so I'm not comparing the two here.
Common mistakes and confusions
These come from details in the documentation that are easy to skip.
- Passing the model name instead of the deployment name. The documentation states that
modeltakes your deployment name, not the underlying model name, so use the name you gave the deployment. - Expecting an
api-versionparameter. Every example uses the/openai/v1/base URL, and none of them includes one. - Passing a local file name as an image. The docs say not to, so encode local images in base64 or use a public HTTPS URL.
- Leaving out a fallback option. The documentation recommends "a fallback option, such as
other, when your categories don't cover every possible input." The docs also suggest writing questions around observable criteria, giving each choice a distinct meaning and basing any routing threshold on labeled examples and the cost of false positives and false negatives in your scenario.
A minimal Python example
This example is hypothetical and inspired by Linkmortem's problem. It asks what kind of page a piece of text belongs to and whether the page announces that its content moved. The two questions are independent, so they can share one request. The calls, fields and minimum package version come from the documentation, and the key stays in an environment variable.
I haven't run this code against a live Azure deployment. What I did verify, on November 10, 2026, is that client.decisions.create exists from openai 3.26.0 onward and not in 3.25.0, that the script sends model, input and questions to /openai/v1/decisions, and that it reads the answers correctly with versions 3.26.0 and 3.49.0. For that last part I used a local mock server that returns responses shaped like the illustrative ones in the documentation, including a refusal.
pip install "openai>=3.26.0" export AZURE_OPENAI_ENDPOINT="https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/" export AZURE_OPENAI_API_KEY="<your-key>" export AZURE_OPENAI_DEPLOYMENT="<your-gpt-6-luna-deployment-name>" import os from openai import OpenAI client = OpenAI( base_url=os.environ["AZURE_OPENAI_ENDPOINT"], api_key=os.environ["AZURE_OPENAI_API_KEY"], ) page_text = ( "Oops! The page you're looking for can't be found. " "Our blog moved to a new address last year." ) decision = client.decisions.create( model=os.environ["AZURE_OPENAI_DEPLOYMENT"], input=page_text, questions=[ { "type": "choice", "name": "page_kind", "instructions": "What kind of page is this, based only on its visible text?", "choices": [ {"value": "live", "description": "The page shows the content it promises."}, {"value": "soft_404", "description": "The page says the content doesn't exist or was removed."}, {"value": "parked", "description": "A placeholder or domain for sale with no original content."}, {"value": "other", "description": "Anything that doesn't fit the options above."}, ], }, { "type": "predicate", "name": "announces_move", "instructions": "Does the page say its content moved to another address?", }, ], ) for answer in decision.answers: if answer.type == "refusal": print(f"{answer.name}: refused, send it to human review") elif answer.type == "choice": print(f"{answer.name}: {answer.choice} (confidence {answer.confidence})") elif answer.type == "predicate": print(f"{answer.name}: probability {answer.probability}") Against the mock server, the script printed the lines below. The values come from my mock and not from the service, so don't read them as results.
page_kind: soft_404 (confidence 0.9) announces_move: probability 0.88 When the mock returned a refusal for the second question, the first answer still arrived and the script printed announces_move: refused, send it to human review, which matches how the documentation says refusals behave.
A checklist before you ship it
- Confirm that your question is a yes or no, a choice from a fixed list or a position on ordered levels. Anything else belongs to structured outputs or function calling.
- Deploy
gpt-6-lunain a region where the Decisions API has rolled out, and remember that East US 2 is excluded. - Pass your deployment name in
modeland keep keys in environment variables, or use Microsoft Entra ID. - Check every answer's
type, and give refusals their own path. - Add a fallback option and split questions that depend on each other.
- Choose Global or Data Zone deployment based on where your data may be processed.
- Test thresholds with labeled examples from your own application before you automate a decision. If you want to keep building on Foundry after this, the Develop generative AI apps in Azure learning path covers model selection, deployment, evaluation, tools and responsible AI in six modules.
Have you replaced a fragile label parser with typed answers, or do you still prefer a JSON Schema for this kind of decision?