apiclawnotes

Microsoft Semantic Kernel with a Custom OpenAI-Compatible Endpoint: AddOpenAIChatCompletion, Uri and the /v1 Path Rules

I'm Aman Kumar. Most of my gateway notes are Python. A fair number of people who write me are on .NET with Microsoft Semantic Kernel — ASP.NET workers, internal copilots, plugins wired through Kernel. The question is always the same: how do I point AddOpenAIChatCompletion at an OpenAI-compatible hub that is not api.openai.com?

The short answer is the Uri overload: pass modelId, endpoint, and apiKey. The longer answer is that Semantic Kernel (via the OpenAI .NET SDK underneath) appends /chat/completions itself, stopped auto-correcting your path after the OpenAI connector rewrite, and still marks the custom-endpoint constructor experimental (SKEXP0010). Get the Uri wrong and you spend an hour debugging a 404 that looks like a bad model ID.

The examples use APIClaw, a gateway I build, so weigh that accordingly. Base URL is https://apiclaw.biz/v1. Copy the model string from /v1/models or the hub — I use YOUR_MODEL_ID below; on that hub examples look like deepseek-v4-flash, deepseek-v4-pro or claude-sonnet-5-5. Crypto is on the site; other payment methods go through support. Swap host, key and model for whatever you run.

Packages

dotnet add package Microsoft.SemanticKernel
# pulls Microsoft.SemanticKernel.Connectors.OpenAI among others

For Azure OpenAI specifically you want Microsoft.SemanticKernel.Connectors.AzureOpenAI and AddAzureOpenAIChatCompletion. This post is only about the OpenAI connector with a custom Message API endpoint — not Azure's resource URL, deployment name, or api-version query string.

Minimal Kernel builder

using Microsoft.SemanticKernel;

#pragma warning disable SKEXP0010 // custom endpoint overload is experimental
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "YOUR_MODEL_ID",
    apiKey: Environment.GetEnvironmentVariable("GATEWAY_API_KEY")
        ?? throw new InvalidOperationException("GATEWAY_API_KEY missing"),
    endpoint: new Uri("https://apiclaw.biz/v1"),
    orgId: null,          // optional; omit unless the hub requires it
    serviceId: "gateway"  // optional; useful when you register several models
);
#pragma warning restore SKEXP0010

Kernel kernel = builder.Build();

string reply = await kernel.InvokePromptAsync<string>(
    "Reply with the single word: ok");
Console.WriteLine(reply);

That posts to POST /v1/chat/completions with your model ID as the JSON model field. If curl against the same host and ID already works, this is the shape that should work in Semantic Kernel too.

You can also construct the service directly:

using Microsoft.SemanticKernel.Connectors.OpenAI;

#pragma warning disable SKEXP0010
var chat = new OpenAIChatCompletionService(
    modelId: "YOUR_MODEL_ID",
    endpoint: new Uri("https://apiclaw.biz/v1"),
    apiKey: Environment.GetEnvironmentVariable("GATEWAY_API_KEY"),
    organization: null);
#pragma warning restore SKEXP0010

Same Uri rules. Prefer the builder when the rest of the app already lives on IKernelBuilder / DI.

Why the path must end at /v1

Microsoft's OpenAI connector migration guide (Semantic Kernel 1.18+ / OpenAI SDK v2) is explicit: the custom-endpoint constructor no longer auto-corrects the Uri. Two historical mistakes now break every call:

  1. Including chat/completions in the endpoint. The OpenAI SDK appends that path for you.
- https://apiclaw.biz/v1/chat/completions
+ https://apiclaw.biz/v1
  1. Omitting /v1. SK will not invent it.
- https://apiclaw.biz/
+ https://apiclaw.biz/v1

Paste the OpenAI-compatible root once, including /v1, and stop. Never put the full chat completions URL in endpoint.

The traps

1. Doubled path

new Uri("https://apiclaw.biz/v1/v1") or .../v1/chat/completions both produce a path the gateway does not serve. You get 404. Confirm the request URL in a logging HttpClient or a proxy if the exception message only shows "Not Found".

2. Missing /v1

new Uri("https://apiclaw.biz") becomes POST /chat/completions (no /v1). Many hubs return 404. Always include /v1 for OpenAI-layout gateways.

3. Azure overload vs OpenAI custom-endpoint overload

AddAzureOpenAIChatCompletion wants an Azure resource endpoint, a deployment name, and usually an API version. It does not take a generic OpenAI-compatible /v1 root the same way. If your hub speaks Chat Completions with Bearer keys (APIClaw and most "OpenAI-compatible" providers), use AddOpenAIChatCompletion(..., endpoint: Uri, apiKey). Do not force the Azure extension onto a non-Azure host.

4. HttpClient BaseAddress interaction

The experimental OpenAIChatCompletionService constructor that takes a Uri falls back to httpClient?.BaseAddress when endpoint is null. If you pass both a Uri and an HttpClient whose BaseAddress already ends in /v1, you can still be fine — the explicit endpoint wins when provided. The trap is constructing the service with endpoint: null and assuming BaseAddress alone is enough while also setting a second base elsewhere, or stacking path segments on BaseAddress (.../v1) while the client later appends /v1/chat/completions through another layer. Prefer one source of truth: pass endpoint: new Uri("https://apiclaw.biz/v1") and keep HttpClient for timeouts, handlers and headers only.

#pragma warning disable SKEXP0010
var http = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
builder.AddOpenAIChatCompletion(
    modelId: "YOUR_MODEL_ID",
    endpoint: new Uri("https://apiclaw.biz/v1"),
    apiKey: Environment.GetEnvironmentVariable("GATEWAY_API_KEY"),
    httpClient: http);
#pragma warning restore SKEXP0010

5. orgId is optional

The overload accepts orgId / organization. Most third-party hubs ignore it. Leave it null unless the provider docs say you must send OpenAI-Organization. Sending a real OpenAI org ID to a non-OpenAI host does nothing useful and can confuse logging.

6. modelId is sent verbatim

Semantic Kernel does not rewrite your model string. Whatever you put in modelId becomes the JSON model field. Gateway IDs are hub-specific — copy them. Wrong ID → model-not-found even when Uri and key are correct.

curl -s https://apiclaw.biz/v1/models \
  -H "Authorization: Bearer $GATEWAY_API_KEY"

7. SKEXP0010

Custom endpoints are still marked [Experimental("SKEXP0010")] on the Uri constructors and extension methods. Without #pragma warning disable SKEXP0010 (or an equivalent project setting), you get analyzer warnings. That is expected — not a sign you picked the wrong overload.

Chat service without a prompt template

If you already hold IChatCompletionService:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var chat = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory("You answer in one short sentence.");
history.AddUserMessage("What is a reverse proxy?");

var message = await chat.GetChatMessageContentAsync(history);
Console.WriteLine(message.Content);

Same underlying HTTP call. Plugins, function calling and streaming all ride on this service once the endpoint is right — start with a plain completion before you turn those on, so path mistakes surface clearly.

DI registration

using Microsoft.SemanticKernel;

var services = new ServiceCollection();

#pragma warning disable SKEXP0010
services.AddOpenAIChatCompletion(
    modelId: "YOUR_MODEL_ID",
    endpoint: new Uri("https://apiclaw.biz/v1"),
    apiKey: Environment.GetEnvironmentVariable("GATEWAY_API_KEY"));
#pragma warning restore SKEXP0010

services.AddTransient<Kernel>();

Same Uri rules as the builder. Register once at startup; resolve Kernel or IChatCompletionService in your handlers.

Sanity checklist

  1. NuGet: Microsoft.SemanticKernel (OpenAI connector), not Azure-only packages for a generic hub.
  2. Overload: AddOpenAIChatCompletion with Uri endpoint — not AddAzureOpenAIChatCompletion.
  3. Endpoint: https://your-host/v1 only. No /chat/completions. No doubled /v1.
  4. #pragma warning disable SKEXP0010 around the call.
  5. modelId copied from the hub; apiKey from a dedicated env var.
  6. Prove the path with curl first: POST /v1/chat/completions with the same key and model.

That is the whole custom-endpoint story for Semantic Kernel on an OpenAI-compatible gateway. Get the Uri to /v1 and leave the rest of the path to the SDK.