Using OpenAI’s o3-mini Reasoning Model in Semantic Kernel

OpenAI shipped o3-mini in January 2025 as a smaller reasoning model built for STEM heavy work such as coding, math and science tasks. It costs a fraction of what o1-mini charged and Microsoft made it available through Azure OpenAI Service almost immediately after launch. Roger Barreto from the Semantic Kernel team published a short guide showing how to plug this model into Semantic Kernel for both C# and Python, and this piece walks through that same integration with a few practical notes from working with reasoning models inside production SK agents.

What o3-mini brings to the table

Before touching code it helps to know what actually changed with this model family compared to a regular chat completion model like GPT-4o. Reasoning models such as o3-mini spend hidden tokens thinking through a problem before they answer, and Microsoft exposes a knob to control how much thinking happens on each request.

  • Reasoning effort control: low, medium or high, trading response speed for depth of reasoning
  • Structured outputs: JSON schema constrained output for downstream automation
  • Function and tool calling that works the same way it did with earlier OpenAI chat models
  • A new developer role that replaces the old system role for instructions (Azure OpenAI maps legacy system messages onto it automatically)
  • Pricing around $1.10 per million input tokens and $4.40 per million output tokens on the OpenAI API, notably cheaper than o1-mini

OpenAI’s own testing found o3-mini made about 39 percent fewer major errors than o1-mini on hard questions while responding roughly 24 percent faster. At medium effort it lands close to the full o1 model on tough math and science problems, and at high effort it can beat o1 on certain benchmarks. Numbers like this deserve a healthy dose of skepticism until you run your own evaluation against your own workload, but the direction matches what most teams see when they swap o1-mini for o3-mini in an existing pipeline.

Adding o3-mini to a Semantic Kernel .NET project

Semantic Kernel treats o3-mini as a normal OpenAI chat completion model, so no new connector is required. You register it the same way you would register GPT-4o, just point modelId at o3-mini and set the reasoning effort on the execution settings. Here is the minimal setup for calling it directly through OpenAIChatCompletionService.

using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
 
#pragma warning disable SKEXP0010 // Reasoning effort is still in preview.
 
// Initialize the OpenAI chat completion service with the o3-mini model.
var chatService = new OpenAIChatCompletionService(
    modelId: "o3-mini",
    apiKey: "YOUR_OPENAI_API_KEY"
);
 
// Create a chat history and add a user message.
ChatHistory chatHistory = [];
chatHistory.AddUserMessage("Why is the sky blue in one sentence?");
 
// Configure reasoning effort for the request.
var settings = new OpenAIPromptExecutionSettings { ReasoningEffort = "high" };
 
// Send the request to o3-mini.
var reply = await chatService.GetChatMessageContentAsync(chatHistory, settings);
Console.WriteLine("o3-mini reply: " + reply);

This runs today, but note the SKEXP0010 pragma sitting above ReasoningEffort. Semantic Kernel still marks the reasoning effort setting as experimental, so expect the API surface to shift in a future release and pin your Semantic Kernel package version if this code needs to stay stable in production. If you are targeting Azure OpenAI instead of the OpenAI API directly, swap in AzureOpenAIChatCompletionService with your deployment name, endpoint and key, but make sure the Azure OpenAI SDK you reference targets REST API version 2024-12-01-preview or later. Older API versions silently ignore the reasoning_effort parameter instead of throwing an error, which is an easy way to lose an afternoon wondering why every response looks like it ran at default effort.

Using o3-mini from Python

The Python side mirrors the C# code closely. Semantic Kernel’s OpenAIChatCompletion connector accepts an instruction_role parameter, and you want to set that to developer since that is the role name reasoning models expect for system style instructions.

import asyncio
 
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIChatPromptExecutionSettings
from semantic_kernel.contents import ChatHistory
 
async def main():
    # Initialize the OpenAI chat completion with the o3-mini model.
    chat_service = OpenAIChatCompletion(ai_model_id="o3-mini", instruction_role="developer")
 
    # Start a chat history and add a developer instruction plus a user prompt.
    chat_history = ChatHistory()
    chat_history.add_developer_message("You are a helpful assistant.")
    chat_history.add_user_message("Why is the sky blue in one sentence?")
 
    # Set high reasoning effort for a more thorough response.
    settings = OpenAIChatPromptExecutionSettings(reasoning_effort="high")
 
    response = await chat_service.get_chat_message_content(chat_history, settings)
    print("o3-mini reply:", response)
 
if __name__ == "__main__":
    asyncio.run(main())

One detail worth calling out: chat_history.add_developer_message is not interchangeable with add_system_message here. If you leave instruction_role at its default and call add_system_message against o3-mini through the direct OpenAI API, the request fails, because plain system messages are not accepted for reasoning models on that API. Azure OpenAI still remaps legacy system messages for you behind the scenes, so this mistake tends to surface only when a team moves an existing SK agent from Azure OpenAI to the direct OpenAI API, or the other way round, and forgets that the expected role name changed underneath them.

Reasoning effort in practice

The reasoning_effort setting is the one parameter worth tuning carefully before shipping anything user facing. High effort produces noticeably better answers on genuinely hard problems, agentic planning steps, multi step math, tricky code generation, but it also burns more hidden reasoning tokens and adds latency you cannot see in the visible response, only in the bill and the clock. Low effort behaves closer to a fast chat model and is a reasonable default for casual conversational turns inside an agent loop where speed matters more than exhaustive correctness.

A pattern that works well in planner driven Semantic Kernel agents is to route only the planning and verification steps through o3-mini at medium or high effort, and keep the conversational turns on a cheaper, non reasoning model. Running every single turn of an agent through a reasoning model at high effort is the most common mistake teams make when they first adopt these models, and it usually shows up as a cost and latency surprise a few weeks into production rather than during initial testing.

o3-mini versus GPT-4o for Semantic Kernel agents

A fair question the original post does not fully answer is when o3-mini actually beats GPT-4o inside a real planner driven agent, and when a non-reasoning model is good enough. In practice, o3-mini earns its keep on tasks with a single correct answer that needs several intermediate logical steps: code generation against a spec, debugging a stack trace, math heavy computation. GPT-4o and similar non-reasoning models remain a better fit for open ended conversation, summarization and tasks where speed and cost matter more than squeezing out the last few percent of accuracy.

Since Semantic Kernel exposes both model families behind the same chat completion abstraction, you can register both connectors in one kernel and pick the model per step instead of committing an entire agent to one model family. That flexibility is arguably the more useful takeaway from this integration than the reasoning_effort parameter itself: the switching cost between o3-mini and GPT-4o in an SK-based agent is close to zero once both services are wired up.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading