Skip to main content

.NET Integration

Use HttpClient against the gateway's documented Chat Completions route. This keeps the integration independent of version-specific AI framework adapters.

Prerequisites

  • Complete the Quickstart and keep the gateway running.
  • Create a client API token for the application.
  • Use .NET 8 or later.

Store the connection values in configuration or a secret manager:

export Keeptrusts__GatewayUrl="http://127.0.0.1:41002/v1/"
export Keeptrusts__ApiToken="kt_..."
export Keeptrusts__Model="gpt-5.4-mini"

The application token is separate from the connected gateway's runtime token and the upstream provider credential.

Register the client

using System.Net.Http.Headers;

builder.Services.AddHttpClient("Keeptrusts", client =>
{
var gatewayUrl = builder.Configuration["Keeptrusts:GatewayUrl"]
?? throw new InvalidOperationException("Keeptrusts:GatewayUrl is required");
var token = builder.Configuration["Keeptrusts:ApiToken"]
?? throw new InvalidOperationException("Keeptrusts:ApiToken is required");

client.BaseAddress = new Uri(gatewayUrl);
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
client.Timeout = TimeSpan.FromSeconds(60);
});

Keep the trailing slash on the configured /v1/ base URL so relative request paths resolve under /v1.

Send a chat request

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;

public sealed class GovernedChatService(IHttpClientFactory clients, IConfiguration config)
{
public async Task<string> CompleteAsync(
string input,
CancellationToken cancellationToken = default)
{
var model = config["Keeptrusts:Model"]
?? throw new InvalidOperationException("Keeptrusts:Model is required");
var payload = new
{
model,
messages = new[] { new { role = "user", content = input } }
};

var response = await clients.CreateClient("Keeptrusts").PostAsJsonAsync(
"chat/completions",
payload,
cancellationToken);

var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.BadRequest)
{
using var errorBody = JsonDocument.Parse(responseBody);
var code = errorBody.RootElement
.GetProperty("error")
.GetProperty("code")
.GetString();
if (code == "content_policy_violation")
{
throw new InvalidOperationException("Request blocked by policy");
}
}

response.EnsureSuccessStatusCode();
using var body = JsonDocument.Parse(responseBody);

return body.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? string.Empty;
}
}

The model must match a configured gateway target. A name in application configuration does not enable that model by itself.

Treat health and authorization separately

GET /healthz checks process health and does not require a model request. An authenticated GET /v1/models verifies the application credential and model catalog.

var client = clients.CreateClient("Keeptrusts");
var models = await client.GetAsync("models", cancellationToken);
models.EnsureSuccessStatusCode();

For an ASP.NET health indicator, decide which contract you need:

  • probe /healthz for liveness without authentication
  • probe /v1/models with the named client for authenticated readiness

Retry carefully

Policy blocks (400 with error.code=content_policy_violation) and authentication failures (401 or 403) are not transient. A 429 or 5xx response may be retryable, but a timeout can happen after the provider accepted the request. Do not add automatic model-call retries without a duplicate-work and duplicate-cost policy.

Verify the governed path

kt policy lint --file policy-config.yaml
kt events tail --since 10m --follow

Send a representative .NET request while the event tail is active. A matching event proves that the application used the Keeptrusts base URL.

Next steps