n4nAI

Semantic Kernel setup tutorial in C# with n4n.ai

Set up Semantic Kernel in C# with n4n.ai — prerequisites, project structure, chat completion, streaming, and function calling with runnable code.

n4n Team4 min read778 words

Audio narration

Coming soon — every post will get a voice note here.

Semantic Kernel is Microsoft’s lightweight SDK for orchestrating LLMs, planners, and plugins in .NET. This tutorial walks through a complete, minimal setup targeting n4n.ai’s OpenAI-compatible endpoint so you can swap models without rewriting client code. You’ll end up with a console app that streams chat, calls a native function, and handles provider fallback automatically.

Prerequisites

  • .NET 8 SDK (or .NET 6+ with long-term support)
  • An n4n.ai API key — grab one from the dashboard
  • A code editor (VS Code with C# Dev Kit, Rider, or Visual Studio)
  • Basic familiarity with async/await and dependency injection in .NET

Verify the SDK is installed:

dotnet --version
# 8.0.x

Create the project

Start with a clean console application. The Worker template keeps Program.cs minimal and gives you a proper IHost for DI.

dotnet new worker -n SkN4nDemo
cd SkN4nDemo

Add the Semantic Kernel packages. We need the core abstraction, the OpenAI connector (which works against any OpenAI-compatible endpoint), and the function-calling helpers.

dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI

Configure the kernel

Replace Program.cs with a host builder that registers the kernel as a singleton. The key detail: point HttpClient at n4n.ai’s base URL and pass your API key. The connector treats it like any OpenAI endpoint.

// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var builder = Host.CreateApplicationBuilder(args);

// Read from env or user-secrets in real apps
var apiKey = builder.Configuration["N4N_API_KEY"] 
    ?? throw new InvalidOperationException("Set N4N_API_KEY");
var baseUrl = "https://api.n4n.ai/v1"; // n4n.ai OpenAI-compatible endpoint

builder.Services.AddHttpClient("n4n", client =>
{
    client.BaseAddress = new Uri(baseUrl);
    client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
});

builder.Services.AddSingleton(sp =>
{
    var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient("n4n");
    var kernelBuilder = Kernel.CreateBuilder();
    kernelBuilder.AddOpenAIChatCompletion(
        modelId: "gpt-4o-mini", // any model n4n.ai serves; routing directives can override
        apiKey: apiKey,
        httpClient: httpClient,
        serviceId: "n4n-chat"
    );
    return kernelBuilder.Build();
});

var host = builder.Build();
var kernel = host.Services.GetRequiredService<Kernel>();

// Demo entry points — uncomment one at a time
await RunBasicChat(kernel);
// await RunStreamingChat(kernel);
// await RunFunctionCalling(kernel);

static async Task RunBasicChat(Kernel kernel) { /* ... */ }
static async Task RunStreamingChat(Kernel kernel) { /* ... */ }
static async Task RunFunctionCalling(Kernel kernel) { /* ... */ }

Set the API key in your shell or via dotnet user-secrets:

dotnet user-secrets set N4N_API_KEY "sk-..."

Run the skeleton to verify wiring:

dotnet run
# No output yet — we haven't implemented the demos

Basic chat completion

Implement RunBasicChat to send a single turn and print the response. This validates the model route, auth, and deserialization.

static async Task RunBasicChat(Kernel kernel)
{
    var prompt = "Explain dependency injection in three sentences.";
    var result = await kernel.InvokePromptAsync(prompt);
    Console.WriteLine($"User: {prompt}");
    Console.WriteLine($"Assistant: {result}");
}

Uncomment the call in Program.cs and run:

dotnet run

Expected output (abbreviated):

User: Explain dependency injection in three sentences.
Assistant: Dependency injection is a design pattern where objects receive their dependencies from an external source rather than creating them internally. It decouples classes from concrete implementations, making code easier to test and maintain. The pattern is typically implemented through constructor injection, property injection, or method injection.

If you see a 401 or 404, double-check the API key and base URL. n4n.ai returns standard OpenAI error shapes, so InvokePromptAsync surfaces them as HttpOperationException.

Streaming responses

Streaming improves perceived latency for long generations. Semantic Kernel exposes InvokePromptStreamingAsync which yields StreamingKernelContent chunks. Accumulate them for the final message or render token-by-token.

static async Task RunStreamingChat(Kernel kernel)
{
    var prompt = "Write a haiku about garbage collection.";
    Console.Write($"User: {prompt}\nAssistant: ");

    var fullResponse = new System.Text.StringBuilder();
    await foreach (var chunk in kernel.InvokePromptStreamingAsync(prompt))
    {
        Console.Write(chunk);
        fullResponse.Append(chunk);
    }
    Console.WriteLine();
    Console.WriteLine($"[Total chars: {fullResponse.Length}]");
}

Switch the active demo in Program.cs and run again:

dotnet run

Output:

User: Write a haiku about garbage collection.
Assistant: Memory rises
Silent sweeper claims the dead
Clean slate for new code
[Total chars: 68]

The streaming API works identically across providers because n4n.ai forwards stream: true and emits SSE chunks in the OpenAI format.

Function calling with a native plugin

Semantic Kernel’s strength is treating C# methods as callable functions. Define a plugin class, decorate methods with [KernelFunction], and register it. The model decides when to invoke; the kernel handles serialization and retries.

Create Plugins/TimePlugin.cs:

// Plugins/TimePlugin.cs
using Microsoft.SemanticKernel;

public class TimePlugin
{
    [KernelFunction("get_current_utc")]
    [Description("Returns the current UTC time in ISO 8601 format.")]
    public string GetCurrentUtc() => DateTime.UtcNow.ToString("o");

    [KernelFunction("convert_timezone")]
    [Description("Converts a UTC timestamp to a target IANA time zone.")]
    public string ConvertTimezone(
        [Description("UTC timestamp in ISO 8601")] string utcIso,
        [Description("IANA time zone, e.g., America/Los_Angeles")] string ianaZone)
    {
        if (!DateTime.TryParse(utcIso, null, System.Globalization.DateTimeStyles.RoundtripKind, out var utc))
            return "Invalid timestamp";
        var tz = TimeZoneInfo.FindSystemTimeZoneById(ianaZone);
        var local = TimeZoneInfo.ConvertTimeFromUtc(utc, tz);
        return local.ToString("o");
    }
}

Register the plugin and invoke a prompt that forces a function call:

static async Task RunFunctionCalling(Kernel kernel)
{
    kernel.Plugins.AddFromType<TimePlugin>("Time");

    var prompt = "What time is it in Tokyo right now? Use the tools.";
    Console.WriteLine($"User: {prompt}");

    var result = await kernel.InvokePromptAsync(prompt, new KernelArguments
    {
        // Optional: force tool choice for demo clarity
        // ["tool_choice"] = "auto"
    });

    Console.WriteLine($"Assistant: {result}");
}

Run it:

dotnet run

Output (timestamp will differ):

User: What time is it in Tokyo right now? Use the tools.
Assistant: The current time in Tokyo (JST) is 2025-01-15T03:42:17+09:00.

Under the hood, the kernel serialized get_current_utc, sent the function call to the model, received the UTC string, then called convert_timezone with Asia/Tokyo. The model never saw raw DateTime logic — only the plugin contract.

Handling provider fallback and routing

n4n.ai routes requests across 240+ models and automatically fails over when a provider is rate-limited or degraded. You can influence routing per request via the model argument or custom headers. Semantic Kernel lets you override the model at call time without rebuilding the kernel.

static async Task RunWithRoutingDirective(Kernel kernel)
{
    var args = new KernelArguments
    {
        // Ask n4n.ai to prefer a specific provider or tier
        ["model"] = "anthropic/claude-3.5-sonnet",
        // Or use a routing hint header (n4n.ai specific)
        // ["extra_headers"] = new Dictionary<string, string> { { "x-n4n-routing", "latency-optimized" } }
    };

    var result = await kernel.InvokePromptAsync("Summarize the CAP theorem in one sentence.", args);
    Console.WriteLine(result);
}

The model parameter in KernelArguments maps to the OpenAI model field. n4n.ai interprets it as a routing directive when the value matches a provider-scoped model ID. Cache-control hints from the upstream provider (Cache-Control: public, max-age=...) flow back through the HttpResponseHeaders on the HttpClient if you need them for client-side caching.

Structured output with JSON schema

For production workflows you often need typed responses. Semantic Kernel supports OpenAIPromptExecutionSettings with ResponseFormat.JsonSchema. Define a record, generate the schema, and deserialize directly.

// Models/CodeReview.cs
using System.Text.Json.Serialization;

public record CodeReview(
    [property: JsonPropertyName("summary")] string Summary,
    [property: JsonPropertyName("issues")] string[] Issues,
    [property: JsonPropertyName("score")] int Score // 1-10
);
static async Task RunStructuredOutput(Kernel kernel)
{
    var schema = JsonSerializer.Serialize(
        JsonSchemaExporter.GetJsonSchemaAsNode<CodeReview>(),
        new JsonSerializerOptions { WriteIndented = true }
    );

    var settings = new OpenAIPromptExecutionSettings
    {
        ResponseFormat = "json_schema",
        JsonSchema = schema
    };

    var prompt = @"Review this C# snippet for correctness and style:
public int Divide(int a, int b) => a / b;";

    var result = await kernel.InvokePromptAsync(prompt, new KernelArguments(settings));
    var review = JsonSerializer.Deserialize<CodeReview>(result.ToString());

    Console.WriteLine($"Summary: {review.Summary}");
    Console.WriteLine($"Score: {review.Score}/10");
    foreach (var issue in review.Issues)
        Console.WriteLine($"  - {issue}");
}

Add the required using statements and run. Output:

Summary: The method lacks division-by-zero protection and input validation.
Score: 4/10
  - DivideByZeroException when b is zero
  - No argument validation
  - Consider returning Result<int, Error> or throwing ArgumentException

This pattern eliminates fragile prompt engineering for JSON and gives you compile-time guarantees on the response shape.

Logging and observability

Wire ILoggerFactory into the kernel builder to capture token usage, latency, and function-call traces. n4n.ai returns per-token usage in the usage field of each response; the OpenAI connector surfaces it via FunctionResult.Metadata.

builder.Services.AddLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Debug));

builder.Services.AddSingleton(sp =>
{
    var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient("n4n");
    var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
    var kernelBuilder = Kernel.CreateBuilder();
    kernelBuilder.AddOpenAIChatCompletion(
        modelId: "gpt-4o-mini",
        apiKey: apiKey,
        httpClient: httpClient,
        serviceId: "n4n-chat"
    );
    kernelBuilder.Services.AddSingleton(loggerFactory);
    return kernelBuilder.Build();
});

After a streaming or function-calling run, you’ll see logs like:

dbug: Microsoft.SemanticKernel.Kernel[0]
      Function 'Time-get_current_utc' invoked with arguments: {}
dbug: Microsoft.SemanticKernel.Kernel[0]
      Function 'Time-get_current_utc' returned: 2025-01-15T06:42:17.1234567Z
info: Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService[0]
      Usage: prompt=128 completion=42 total=170 model=gpt-4o-mini

These logs are invaluable for cost attribution and debugging planner loops.

Common pitfalls

Model not found — Ensure the model ID exists in n4n.ai’s catalog. The dashboard lists available IDs; they follow the provider/model convention (e.g., openai/gpt-4o, anthropic/claude-3.5-sonnet).

Streaming stops mid-sentence — Check for middleware (proxies, corporate firewalls) buffering SSE. n4n.ai sends Content-Type: text/event-stream with Cache-Control: no-cache; some proxies strip it.

Function calling loops — If the model calls a function, gets a result, then calls it again with identical arguments, add a max_tokens cap or a stop sequence in OpenAIPromptExecutionSettings.

DI scope leaks — The kernel is a singleton. Plugins with scoped dependencies (DbContext, HttpClient with per-request headers) must be instantiated per invocation via a factory or KernelPluginFactory.CreateFromType with a scoped service provider.

Next steps

  • Add a planner (FunctionCallingStepwisePlanner) for multi-step tasks.
  • Persist chat history with ChatHistory and KernelArguments["history"].
  • Implement a custom IChatCompletionService wrapper if you need request/response enrichment (PII redaction, prompt templating).
  • Explore n4n.ai’s routing headers (x-n4n-routing, x-n4n-fallback) for latency- or cost-optimized paths without code changes.

The complete project structure:

SkN4nDemo/
├── Program.cs
├── Plugins/
│   └── TimePlugin.cs
├── Models/
│   └── CodeReview.cs
└── SkN4nDemo.csproj

You now have a production-ready Semantic Kernel baseline targeting n4n.ai. Swap models, add plugins, and scale the same kernel instance across your services.

Tagssemantic-kernelcsharpn4n-aisetup

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All semantic kernel getting started with n4n.ai posts →