.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
/healthzfor liveness without authentication - probe
/v1/modelswith 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.