Jev.Net 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Jev.Net --version 0.2.0
                    
NuGet\Install-Package Jev.Net -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Jev.Net" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jev.Net" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Jev.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Jev.Net --version 0.2.0
                    
#r "nuget: Jev.Net, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Jev.Net@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Jev.Net&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Jev.Net&version=0.2.0
                    
Install as a Cake Tool

Jev.Net

CI NuGet MIT

A lightweight .NET client for the TypeSafe AI API — System One / Jev. Ask small typed questions about text or structured state and get calibrated probabilities back that your code can act on.

A community package. Not affiliated with or endorsed by TypeSafe AI.

dotnet add package Jev.Net

Why this one

One dependency. Microsoft.Extensions.Logging.Abstractions, and nothing else. Retries, backoff, Retry-After and per-attempt timeouts are about a hundred lines of its own, so it drops into a library, a CLI, a desktop app or a worker without changing what your dependency graph looks like. That promise is enforced: CI reads the packed .nuspec and fails on a second dependency. Targets net8.0 and net10.0, and is trim- and Native AOT-compatible — a smoke app is published as a native binary and run on every build.

The Python SDK, in C#. It is a faithful port of the official typesafe-sdk (read at 0.7.0): the same request shape, the same protected headers, the same exception hierarchy and the same message text, the same retry rules, the same dotted field paths when a response is malformed. Its test suite is a mirror of upstream's, file by file — so behaviour you read about in TypeSafe's docs, or debugged once in Python, is the behaviour you get here. Where .NET forces a difference, it is listed at the bottom.

Looking for something else?

TypeSafeAI.Net by Hawxy is an excellent community SDK with a different focus, and it got here first: dependency-injection and IHttpClientFactory integration, enum-typed choices, a QuestionSet builder with typed handles, Native AOT support, and a companion package of Microsoft.Extensions.AI middleware (guardrails, routing, evaluators). If that is the shape of your app, use it — the two projects make different trade-offs on purpose, and both speak the same API.

Quick start

await using var client = new TypeSafeClient();            // key from TYPESAFE_API_KEY

var result = await client.SystemOneAsync(
    "I was charged twice. Please help.",
    new Dictionary<string, Question>
    {
        ["billing"] = new Noul("Is this about billing?"),
        ["tone"]    = new Choice(["calm", "frustrated", "angry"], "What is the customer's tone?"),
        ["urgency"] = new Score(["can wait", "this week", "today"], "How urgent is this ticket?"),
    });

double billing = result.Nouls["billing"].Noul;            // probability of YES — 0.5 is "unsure", not "medium"
string tone    = result.Choices["tone"].Choice;
double urgency = result.Scores["urgency"].Score;          // may fall between rubric levels

Questions are independent and answered in one round trip, so ask everything about a state together.

Typed answers by name

Derive from SystemOneResponse and declare answer-typed properties; each is filled from the answer of the same name ([JsonPropertyName], else the property name — exact, case-insensitive, then snake_case). Every such property is required — a missing answer, or one of the wrong kind, is a TypeSafeApiResponseValidationException naming the field — unless you mark it [OptionalAnswer].

sealed class Ticket : SystemOneResponse
{
    public NoulAnswer Billing { get; set; } = null!;
    public ChoiceAnswer Tone { get; set; } = null!;
    [OptionalAnswer] public ScoreAnswer? Urgency { get; set; }
}

var ticket = await client.SystemOneAsync<Ticket>(state, questions);

To read the body into a type that is entirely yours, pass JSON metadata — source-generated for trimmed and AOT apps, or reflection-based when that doesn't matter:

var mine = await client.SystemOneAsync(state, questions, options: null, MyJsonContext.Default.MyEnvelope);   // AOT-safe
var mine = await client.SystemOneAsync<MyEnvelope>(state, questions, options: null, ResponseJson.SnakeCase); // reflection

State, instructions and criteria: JsonContent

Everywhere the API takes "text, an object, or an array", the SDK takes a JsonContent. It converts implicitly from string and from JsonNode; JsonContent.From(value) serializes anything else (pass a JsonTypeInfo<T> as the second argument in a trimmed or AOT app). Nodes are deep-cloned in and out, so one JsonObject can appear in several questions and a question can be sent any number of times — System.Text.Json nodes have a single parent, and encoding by reference would throw on the second use.

null means omit for an optional field; JsonContent.Null means send an explicit null. A choice label mapped to null is sent as null (undescribed — interpreted by its name).

Raw questions

A JsonObject converts implicitly to a Question and is sent exactly as given — the escape hatch for fields or question types the API adds before this SDK models them. Only its structure is checked (type present; choice and score have criteria; a score rubric is nonempty); its schema is left to the API.

Errors

Exception When
TypeSafeException Base. Also: no API key, invalid timeout, empty questions, empty score rubric.
TypeSafeApiException Any non-2xx. Status, Body, Headers, Endpoint, RequestId.
…BadRequest / Authentication / PermissionDenied / NotFound / UnprocessableEntity 400 / 401 / 403 / 404 / 422
TypeSafeRateLimitException 429. RetryAfter is the server's requested wait.
TypeSafeInternalServerException 5xx
TypeSafeApiConnectionException No HTTP response at all.
TypeSafeApiTimeoutException The per-attempt timeout elapsed (derives from the connection exception).
TypeSafeApiResponseValidationException A 2xx whose body is structurally wrong. FieldPath names the first bad field: answers.tone.confidence, models[1].name.

Message reads POST https://api.typesafe.ai/v1/systemone: 429 <server's explanation> (request_id=…). The endpoint never carries credentials, a query string, or a fragment.

Caller cancellation is not a timeout: cancelling your token surfaces as OperationCanceledException, is never retried, and also interrupts a wait between retries.

Retries

RetryPolicy defaults: 2 retries; 408, 429 and 5xx; connection and timeout failures; exponential backoff from 0.5s to 5s with up to 25% subtracted as jitter; Retry-After / retry-after-ms honored (however long); and a 30s total budget per call that stops before a wait that would exceed it, rethrowing the last error. Set it on the client, or per call through RequestOptions.Retry. RetryPolicy.None disables retrying.

Configuration

Option Environment Default
ApiKey TYPESAFE_API_KEY — (required)
BaseUrl TYPESAFE_BASE_URL https://api.typesafe.ai
Model TYPESAFE_DEFAULT_MODEL jev-latest
Timeout 10s per attempt; Timeout.InfiniteTimeSpan for none
LoggerFactory TYPESAFE_LOG_LEVEL (a floor) no logging

Explicit options win; blank environment values count as unset. Authorization, Accept, User-Agent, X-TypeSafe-SDK and X-TypeSafe-Runtime are protected — neither default nor per-call headers can replace them.

Logging (category Jev.Net): one Information line per attempt; headers and bodies at Debug. Credential-bearing headers — and any header whose name contains token or secret — are redacted. Bodies are not: they are whatever state you sent. Don't enable Debug where that matters.

Pin a model (jev-1.13.0, not jev-latest) wherever you have tuned thresholds against its probabilities.

Python SDK → Jev.Net

If you know typesafe-sdk, you already know this. Everything in the left column behaves the same on the right.

Python Jev.Net
AsyncTypeSafeClient(api_key=…, model=…, base_url=…, timeout=…, headers=…, retry=…) new TypeSafeClient(new TypeSafeClientOptions { ApiKey, Model, BaseUrl, Timeout, Headers, Retry })
transport= / http_client= Handler / HttpClient (mutually exclusive, as upstream)
TYPESAFE_API_KEY, TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL, TYPESAFE_LOG_LEVEL the same four variables, same precedence, blank = unset
await client.system_one(state, questions, model=…, retry=…, timeout=…, extra_headers=…, extra_body=…) await client.SystemOneAsync(state, questions, new SystemOneOptions { Model, Retry, Timeout, ExtraHeaders, ExtraBody })
await client.models.list() await client.Models.ListAsync()
Noul(instructions=…, criteria={"true": …, "false": …}) new Noul(instructions, new NoulCriteria { True = …, False = … })
Choice(instructions=…, criteria={"calm": None, "angry": "…"}) new Choice(["calm", "angry"], instructions) or a Dictionary<string, JsonContent?> with descriptions
Score(instructions=…, criteria=["low", "high"]) new Score(["low", "high"], instructions)
a raw {"type": "noul", …} dict a JsonObject (converts implicitly to Question)
JSONContent (str, mapping, sequence) JsonContent (string, JsonObject, JsonArray, or JsonContent.From(…))
result.nouls["q"].noul, .choices["q"].choice / .confidence / .probabilities, .scores["q"].score / .legend result.Nouls["q"].Noul, .Choices["q"].Choice / .Confidence / .Probabilities, .Scores["q"].Score / .Legend
result.answers, .model, .usage.input_tokens result.Answers, .Model, .Usage.InputTokens
result.request_id, result.raw_http_response result.RequestId, result.RawHttpResponse
response_model=MyResponse (a SystemOneResponse subclass with answer fields) SystemOneAsync<MyResponse>(…)
response_model=AnyPydanticModel SystemOneAsync(state, questions, options, JsonTypeInfo<T>) or SystemOneAsync<T>(state, questions, options, JsonSerializerOptions)
RetryPolicy(max_retries, backoff_initial, backoff_max, backoff_jitter, http_statuses, respect_retry_after, api_connection_error, api_timeout_error, exceptions, predicate, timeout) RetryPolicy { MaxRetries, BackoffInitial, BackoffMax, BackoffJitter, HttpStatuses, RespectRetryAfter, ApiConnectionError, ApiTimeoutError, Exceptions, Predicate, Timeout } — same defaults
TypeSafeError → TypeSafeAPIError → …BadRequestError, …RateLimitError, … TypeSafeException → TypeSafeApiException → …BadRequestException, …RateLimitException, …
error.status, .body, .headers, .endpoint, .request_id, .retry_after_ms, .field_path error.Status, .Body, .Headers, .Endpoint, .RequestId, .RetryAfter, .FieldPath
str(error) error.Message — the same text
unknown answer types skipped with a warning; unknown fields ignored the same
async with client: await using var client = …

Differences from the Python SDK

  • Async only. No synchronous client; that is the .NET convention for HTTP.
  • Durations are TimeSpan, so the NaN/infinite-seconds cases upstream validates cannot be written. There is one per-attempt timeout rather than httpx's connect/read/write/pool split.
  • HttpMessageHandler / HttpClient stand in for httpx's transport / client. A supplied HttpClient is disposed with the SDK client, as upstream — set DisposeHttpClient = false for one from IHttpClientFactory. When you supply one, its own Timeout still caps every attempt.
  • Questions are a closed class hierarchy, so "a question that is not a question" cannot be constructed; typed questions validate at construction with ArgumentExceptions rather than pydantic errors.
  • No covariant question maps — IReadOnlyDictionary is invariant in its value; declare the dictionary as Dictionary<string, Question>.
  • Optional answers are marked, not inferred. Python reads Optional[...]; here a property is required unless it carries [OptionalAnswer]. Nullability is metadata the trimmer removes, so inferring from it would make the same class validate differently in a Native AOT build.
  • TimeProvider is accepted for the retry clock — a .NET addition.
  • It identifies itself as jev-net/<version>, not as the official SDK.

Tests

dotnet test --solution Jev.Net.slnx

Offline and sub-second: a stub HttpMessageHandler, an injected environment reader, an injected delay, and a manual clock. LiveIntegrationTests is the one class that calls the real API, and it is opt-in — it needs TYPESAFE_LIVE_TESTS=1 and TYPESAFE_API_KEY, and reports Inconclusive otherwise.

The suite was mutation-checked: the client was broken on purpose in nine places (protected headers, the retry budget, header redaction, strict number decoding, node cloning, jitter, retry-after-ms precedence, cancellation-vs-timeout, unknown answer types) and every break is caught.

tests/Jev.Net.AotSmoke is not a test project but a console app: CI publishes it with PublishAot and runs the native binary on Linux and Windows, exercising request encoding, retries, answers-by-property-name, source-generated JSON, and error mapping after the trimmer and AOT compiler have been through them.

License

MIT. See NOTICE for the upstream attribution.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.0 176 9/20/2026
0.2.0 155 9/20/2026
0.1.0 86 9/20/2026