Skip to main content

Getting started

Minos.NET is an unofficial .NET client for TypeSafe AI's Jev, the first System One model. TypeSafe calls Jev a System One model: it answers typed questions directly instead of generating text, and their guide explains the idea. You send Jev the text or JSON to judge, called the state, and a few typed questions about it. Jev answers each question with a typed result and a calibrated confidence, meaning a number meant to track how often such answers are right. You can reach it directly at TypeSafe or through OpenRouter. Servers that speak the same protocol work the same way, such as one you run.

The library turns those questions and answers into ordinary C# types. You declare the questions on a partial record, a source generator writes the request, and the reply comes back as properties you can read. There is no JSON to build or parse by hand.

Not affiliated with TypeSafe AI. Minos.NET is a community project. TypeSafe publishes official SDKs for Python and JavaScript; see docs.typesafe.ai.

The three question types​

Every question is one of three kinds. The next page covers them in detail.

  • Noul asks a yes/no question and answers with the probability of yes. "Noul" is TypeSafe's name for the question type.
  • Choice asks for one option out of a list, and answers with the pick, a confidence and every option's probability.
  • Score asks for a level on an ordered scale, and answers like a Choice, plus the probability-weighted average level.

Requirements​

You need the .NET 10 SDK, version 10.0.100 or later.

The [Questions] source generator compiles against Roslyn 5.0, so your build and your IDE must host Roslyn 5.0 or later. Visual Studio 2026 version 18.0 is the first release that does. An older IDE shows generator errors even when dotnet build succeeds.

Install​

This guide describes Minos.NET 1.1.1.

Install the packages from NuGet:

dotnet add package Minos.NET.SystemOne

The core is provider-neutral: Minos.NET holds the question types, the source generator, the analyzers, the pipeline and IDecisionClient, and knows no provider. Minos.NET.SystemOne is the client for the /v1/systemone protocol, in the Minos.SystemOne namespace. TypeSafe and OpenRouter serve that protocol, and so do the servers you run. It brings Minos.NET, so there is nothing else to install for typed questions. It also brings Minos.NET.DependencyInjection, which is what you use in a .NET host, as dependency injection covers.

For OpenAI's Decisions API, which is in public beta, install Minos.NET.OpenAI instead, or beside it. It holds OpenAIDecisionClient, in the Minos.OpenAI namespace, and brings Minos.NET and Minos.NET.DependencyInjection the same way. Its provider name in logs and telemetry is openai. OpenAI covers it.

dotnet add package Minos.NET.OpenAI

Two optional pipeline stages ship as their own packages: Minos.NET.Caching for UseCaching() and Minos.NET.RateLimiting for UseRateLimiting(). The pipeline page covers both.

What the packages bring with them​

Minos.NET has one runtime dependency from Microsoft, Microsoft.Extensions.Logging.Abstractions 10.0.0 or later, for the logging interfaces that the client's logging uses. Its other runtime dependencies are ZeroAlloc libraries. Minos.NET.SystemOne adds Microsoft.Extensions.Http and Microsoft.Extensions.Options.ConfigurationExtensions, for the IHttpClientFactory and the configuration binding of AddSystemOneDecisionClient, and Minos.NET.OpenAI adds the same two for AddOpenAIDecisionClient. Minos.NET.DependencyInjection adds Microsoft.Extensions.DependencyInjection.Abstractions. Minos.NET.Caching adds Microsoft.Extensions.Caching.Abstractions, for IMemoryCache and IDistributedCache, and references Microsoft.Extensions.Logging.Abstractions for its own warnings. Minos.NET.RateLimiting adds System.Threading.RateLimiting, for RateLimiter.

NuGet also passes the analyzers and source generators of those dependencies on to your build. The ones that arrive are those of ZeroAlloc.Validation, ZeroAlloc.Telemetry, and Microsoft.Extensions.Logging.Abstractions. They do nothing unless you declare [Validate] types, [Instrument] types or your own [LoggerMessage] methods, so your build stays clean and gets no extra source from them.

Building the packages yourself​

To try a change to Minos.NET itself, clone the Minos.NET repository, pack the packages into a folder, and register that folder as a local NuGet source. Use the folder's absolute path. On Windows, give dotnet nuget add source a native path such as C:\src\minos\nupkgs, because a path that mixes forward and back slashes is rejected as invalid.

A local build is versioned as the last release with a -local suffix: the release in .release-please-manifest.json, such as 0.4.0, gives 0.4.0-local. The packed files' names show it. The suffix keeps a local package apart from the published one. NuGet caches packages by id and version, so a local build versioned exactly 0.4.0 would sit in the cache in place of the real 0.4.0, which was built from another commit. A -local version is a prerelease, so it sorts below the release and NuGet never picks it for a stable version range. When the release is itself a prerelease, such as 1.0.0-beta.1, a local build is 1.0.0-0.local.beta.1 instead, which sorts below every prerelease of 1.0.0; use the version from the packed files' names.

dotnet pack src/Minos.NET -c Release -o /absolute/path/nupkgs
dotnet pack src/Minos.NET.SystemOne -c Release -o /absolute/path/nupkgs
dotnet pack src/Minos.NET.OpenAI -c Release -o /absolute/path/nupkgs
dotnet pack src/Minos.NET.DependencyInjection -c Release -o /absolute/path/nupkgs
dotnet nuget add source /absolute/path/nupkgs --name minos-net-local

Then add the packages with that version, in place of the commands above. Replace <release> with the manifest's version:

dotnet add package Minos.NET.SystemOne --version <release>-local
dotnet add package Minos.NET.DependencyInjection --version <release>-local

For OpenAI, add Minos.NET.OpenAI with the same version.

Remove the source when you go back to the published packages: dotnet nuget remove source minos-net-local.

Providers and keys​

SystemOneDecisionClientOptions.Provider says where requests go, and SystemOneProvider has four values.

ProviderRequests go toEnvironment variable
SystemOneProvider.TypeSafehttps://api.typesafe.ai/TYPESAFE_API_KEY
SystemOneProvider.OpenRouterhttps://openrouter.ai/api/OPENROUTER_API_KEY
SystemOneProvider.VllmSemanticRouterthe BaseAddress you setnone
SystemOneProvider.Customthe BaseAddress you setnone

OpenAI's Decisions API is not one of these. It has its own client, OpenAIDecisionClient, which sends to https://api.openai.com/v1/decisions and reads OPENAI_API_KEY; OpenAI shows it, and the provider matrix compares it with the presets.

TypeSafe is the default. Set ApiKey on the options, or leave it unset and the client reads the matching environment variable. VllmSemanticRouter and Custom read none and send no key unless you set ApiKey. The Providers page has the full table of presets, with the paths and default models, and says what to set for a server of your own. SystemOneDecisionClient is disposable and meant to be long-lived: create one, share it, and dispose it at shutdown, as the snippet below does for its short example. The client throws InvalidOperationException when it is created with no key available for a provider that needs one, so a missing key shows up at start-up rather than on the first call.

// TypeSafe is the default provider. With no ApiKey set, the client reads TYPESAFE_API_KEY.
// SystemOneDecisionClient is disposable and meant to be long-lived: create it once, share it, and dispose it at shutdown.
public static async Task<string> ViaTypeSafeAsync(string message, CancellationToken cancellationToken)
{
using var client = new SystemOneDecisionClient(new SystemOneDecisionClientOptions());
return await GettingStartedEvaluation.TriageAsync(client, message, cancellationToken);
}

// OpenRouter: name the provider. With no ApiKey set, the client reads OPENROUTER_API_KEY.
public static async Task<string> ViaOpenRouterAsync(string message, CancellationToken cancellationToken)
{
using var client = new SystemOneDecisionClient(new SystemOneDecisionClientOptions { Provider = SystemOneProvider.OpenRouter });
return await GettingStartedEvaluation.TriageAsync(client, message, cancellationToken);
}

Your first evaluation​

Declare the questions as a partial record marked [Questions]. This one asks two questions about a support message: is it urgent, and which team should handle it. The options of the Choice are the members of an enum, each with a short description that tells Jev what the option means.

using Minos;
using Minos.SystemOne;

public enum SupportTeam
{
[Criterion("Payments, invoices and refunds")]
Billing,

[Criterion("Bugs, outages and integrations")]
Technical,

[Criterion("Pricing, upgrades and new accounts")]
Sales,
}

// Two questions about one message. The generator writes the question definition at compile time, and the
// properties hold the typed answers once Jev has replied.
[Questions]
public partial record TicketCheck
{
[Noul("Does this convey urgency?")]
public partial Noul IsUrgent { get; }

[Choice("Which team should handle this?")]
public partial Choice<SupportTeam> Team { get; }
}

Then call EvaluateAsync<T> with the text to judge. It returns a Result, not the answers directly, because the call can fail in many ways. Check IsFailure first, and read the answers from Value only after that.

public static async Task<string> TriageAsync(IDecisionClient client, string message, CancellationToken cancellationToken)
{
var result = await client.EvaluateAsync<TicketCheck>(message, cancellationToken);

// Every outcome comes back as a value, so check for failure before reading the answers: a network
// error, a rejected key and an unreadable response all arrive here, with a Kind and a Message.
if (result.IsFailure)
{
return $"The call failed, {result.Error.Kind}: {result.Error.Message}";
}

var check = result.Value;

// IsUrgent.Probability is the chance that the answer is yes, and Value is that chance at 0.5 or more.
// Team.Value is the option Jev picked, and Team.Confidence says how far to trust the pick.
return $"{(check.IsUrgent.Value ? "urgent" : "not urgent")}, for {check.Team.Value}";
}

Given a message such as "Help! My payouts have been failing for 3 days.", a typical reply makes this return urgent, for Billing. On failure, result.Error.Kind says what went wrong, for example a rejected key or a network error, and result.Error.Message explains it. The client and its errors lists every kind and what to do about each.

The call needs an IDecisionClient. SystemOneDecisionClient implements it, and a test can hand in a fake, as testing your code shows. See the patterns for complete, runnable uses of these answers.

Where next​

The guide has one page per topic.

Next​

Continue with Question types.