Expose a bounded loopback-only generation endpoint while keeping model acquisition and trust outside requests.
Goal
Expose a bounded loopback-only generation endpoint while keeping model acquisition and trust outside requests.
Applicability
Use this example as a host-architecture reference for the named framework. Resolve and pin dependencies through the validation solution or the consuming repository policy.
Required packages
- UAIX.LmRuntime.LocalEndpoint
Host responsibilities
- Acquire and approve the model outside request or generation paths.
- Prepare the model-specific prompt and enforce limits.
- Own cancellation, UI dispatch, error presentation, persistence, and evidence retention.
Complete code
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using UAIX.LmRuntime.LocalEndpoint;
namespace GgufRuntime.Examples.AspNetCoreLoopback;
/// <summary>Hosts one verified local GGUF session on an explicitly loopback-only endpoint.</summary>
public static class Program
{
/// <summary>Starts the bounded loopback service after controlled model intake.</summary>
/// <param name="arguments">Model path, trusted SHA-256, and trusted byte count.</param>
/// <returns>Zero after a normal shutdown; two when required arguments are missing.</returns>
public static Int32 Main(String[] arguments)
{
if (arguments.Length != 3)
{
Console.Error.WriteLine(
"Usage: AspNetCoreLoopback <model.gguf> <sha256> <byte-count>");
return 2;
}
String modelPath = Path.GetFullPath(arguments[0]);
String expectedSha256 = arguments[1];
Int64 expectedByteCount = Int64.Parse(arguments[2], CultureInfo.InvariantCulture);
using LoopbackRuntimeHost runtimeHost = LoopbackRuntimeHost.Load(
modelPath,
expectedSha256,
expectedByteCount);
WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty<String>());
builder.WebHost.UseUrls("http://127.0.0.1:5078");
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 64 * 1024;
});
builder.Services.AddSingleton(runtimeHost);
WebApplication application = builder.Build();
application.MapPost(
"/generate",
static (GenerationRequest request, LoopbackRuntimeHost host, HttpContext context) =>
{
if (String.IsNullOrWhiteSpace(request.Prompt)
|| request.Prompt.Length > 32_768
|| request.MaximumTokens is < 1 or > 256)
{
return Results.BadRequest(new { error = "Request exceeds host limits." });
}
LocalGgufGenerationResult result = host.Generate(
request.Prompt,
request.MaximumTokens,
context.RequestAborted);
return Results.Ok(new { text = result.GeneratedText });
});
application.Run();
return 0;
}
}
/// <summary>Owns one verified model and isolated session for the loopback service lifetime.</summary>
public sealed class LoopbackRuntimeHost : IDisposable
{
private readonly LocalGgufModel model;
private readonly LocalGgufSession session;
private readonly SemaphoreSlim generationGate = new SemaphoreSlim(1, 1);
private Boolean disposed;
private LoopbackRuntimeHost(LocalGgufModel model, LocalGgufSession session)
{
this.model = model;
this.session = session;
}
/// <summary>Loads one verified model before any request can reach the service.</summary>
/// <param name="modelPath">The host-approved local GGUF path.</param>
/// <param name="expectedSha256">The host-approved SHA-256 identity.</param>
/// <param name="expectedByteCount">The host-approved byte count.</param>
/// <returns>A service-lifetime runtime host.</returns>
public static LoopbackRuntimeHost Load(
String modelPath,
String expectedSha256,
Int64 expectedByteCount)
{
String trustedModelRoot = Path.GetDirectoryName(modelPath)
?? throw new InvalidOperationException("The model path has no parent directory.");
LocalUaixRuntimeContext uaixContext = CreateUaixContext();
_ = LocalGgufRuntime.VerifyUaixRuntimeContext(uaixContext);
LocalGgufRuntime runtime = new LocalGgufRuntime();
LocalGgufModel model = runtime.LoadVerifiedModel(
modelPath,
new LocalGgufFileExpectation
{
ModelSha256 = expectedSha256,
ModelByteCount = expectedByteCount
},
new LocalGgufModelLoadOptions
{
AllowedRootDirectory = trustedModelRoot,
RejectReparsePoints = true,
MaximumModelBytes = expectedByteCount,
ExecutionLimits = new LocalGgufExecutionLimits
{
MaximumPromptCharacters = 32_768,
MaximumGeneratedTokens = 256,
MaximumStopTokenCount = 32
}
});
try
{
LocalGgufSession session = model.CreateSession(
new LocalGgufSessionContext
{
SessionId = "loopback-session1",
UaixRuntimeContext = uaixContext
});
return new LoopbackRuntimeHost(model, session);
}
catch
{
model.Dispose();
throw;
}
}
/// <summary>Runs one serialized and cancellation-aware generation request.</summary>
/// <param name="preparedPrompt">The complete model-specific prompt prepared by the host.</param>
/// <param name="maximumTokens">The bounded output-token request.</param>
/// <param name="cancellationToken">The HTTP request-aborted token.</param>
/// <returns>The complete local generation result.</returns>
public LocalGgufGenerationResult Generate(
String preparedPrompt,
Int32 maximumTokens,
CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(disposed, this);
generationGate.Wait(cancellationToken);
try
{
return session.GenerateGreedy(
new LocalGgufGenerationRequest
{
Prompt = preparedPrompt,
MaximumTokens = maximumTokens,
ResetSession = true
},
cancellationToken);
}
finally
{
generationGate.Release();
}
}
/// <summary>Disposes the session before the model.</summary>
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
session.Dispose();
model.Dispose();
generationGate.Dispose();
}
private static LocalUaixRuntimeContext CreateUaixContext()
{
return new LocalUaixRuntimeContext
{
LoadedUaixProfilePresent = true,
LoadedUaixProfileId = "profile1",
LoadedUaixProfileDisplayName = "Loopback local profile",
LoadedUaixLoadSessionId = "load1",
LoadedUaixUaiRelativePath = "Memories/Profiles/profile1/.uai",
LoadedUaixSessionRelativePath = "Memories/Sessions/load1.json",
LongTermMemoryRootId = "wiki1",
LongTermMemoryRootRelativePath = "Profiles/profile1",
LongTermMemoryMode = LocalUaixLongTermMemoryMode.Isolated,
RuntimeExecutionAllowed = false,
MemoryCanOverridePolicy = false,
CommandExecutionAllowed = false,
NetworkAccessAllowed = false,
ProviderApisAllowed = false,
WebsitePromptIntakeAllowed = false,
TelemetryEnabled = false,
AutoExportAllowed = false
};
}
}
/// <summary>Represents a bounded loopback-only generation request.</summary>
public sealed class GenerationRequest
{
[Display(Name = "Prompt")]
[Required]
[StringLength(32_768, MinimumLength = 1)]
public String Prompt { get; init; } = String.Empty;
[Display(Name = "Maximum Tokens")]
[Range(1, 256)]
public Int32 MaximumTokens { get; init; } = 128;
}Expected behavior
The host either produces the narrow result described by the example or surfaces a specific identity, validation, load, generation, cancellation, or disposal failure. No hidden fallback is introduced.
Failure cases
- Invalid or untrusted input identity.
- GGUF structure, architecture, tokenizer, storage, kernel, or memory incompatibility.
- Cancellation or host shutdown.
- Framework dispatch or lifecycle misuse.
Security and trust notes
Do not accept remote model uploads, arbitrary paths, arbitrary URLs, provider keys, commands, or private-network targets. The loopback example binds to 127.0.0.1 and still requires a new threat model before broader exposure.
Disposal requirements
The host must finish or cancel generation, dispose the session, then dispose the model. Service and UI examples must connect this order to application shutdown.
Evidence produced
- Resolved package identity and version from the build.
- Model hash and byte count from intake.
- Session identifier, bounds, cancellation state, selected path, result, timing method, and limitations when the host records them.
Build-validation status
State: Experimental. Last compilation attempt UTC: 2026-06-25T00:00:00Z. The source was API-reviewed against current canonical public documentation; local compilation is blocked until a .NET SDK and package restore are available. CI is configured to produce the retained build result.
Open canonical package documentation.
Operational boundary
What this page covers: Expose a bounded loopback-only generation endpoint while keeping model acquisition and trust outside requests.
Available now: The complete source and validation-project mapping are included in this release.
Host responsibility: Provide real inputs, run restore/build/tests, test the host lifecycle, and retain exact evidence.
Not claimed: A successful local build in this environment, finished downloadable application, universal compatibility, or production/safety certification.
Canonical sources: Canonical package guide · NuGet
Last reviewed UTC: