Ozakboy.Ai.Abstractions
0.1.0
dotnet add package Ozakboy.Ai.Abstractions --version 0.1.0
NuGet\Install-Package Ozakboy.Ai.Abstractions -Version 0.1.0
<PackageReference Include="Ozakboy.Ai.Abstractions" Version="0.1.0" />
<PackageVersion Include="Ozakboy.Ai.Abstractions" Version="0.1.0" />
<PackageReference Include="Ozakboy.Ai.Abstractions" />
paket add Ozakboy.Ai.Abstractions --version 0.1.0
#r "nuget: Ozakboy.Ai.Abstractions, 0.1.0"
#:package Ozakboy.Ai.Abstractions@0.1.0
#addin nuget:?package=Ozakboy.Ai.Abstractions&version=0.1.0
#tool nuget:?package=Ozakboy.Ai.Abstractions&version=0.1.0
Ozakboy.Ai.Abstractions
Vendor-neutral abstractions for asking a language model a question and getting a typed object back.
English | 繁體中文
dotnet add package Ozakboy.Ai.Abstractions
Requires .NET 10. Depends on nothing but the .NET BCL and Ozakboy.Core.Abstractions.
Why this exists
An application that consults a language model should not be written against one particular way of reaching it. A local CLI subprocess today, an HTTP API tomorrow, a scripted fake in the tests — the calling code should be the same in all three.
So the contract is deliberately narrow:
Task<Result<AiCompletion<T>>> CompleteAsync<T>(AiRequest request, CancellationToken ct) where T : class;
Plain text in. A typed object out. Every expected failure — timeout, backend unavailable, output that does not
validate — comes back as a Result, never as an exception.
Only the shape of the output is guaranteed, never its correctness. What comes back deserialises into T and
passes the validator you supplied. Whether the values in it are sensible, or safe to act on, is still yours to
decide.
Quick start
using Ozakboy.Ai.Abstractions;
internal sealed class Summary
{
[JsonPropertyName("headline")] public required string Headline { get; init; }
[JsonPropertyName("score")] public required decimal Score { get; init; }
}
var request = new AiRequest
{
SystemPrompt = "You summarise trading journals. Be terse.",
UserPrompt = journalText,
Timeout = TimeSpan.FromMinutes(3),
MaxRepairAttempts = 1,
}.WithValidator<Summary>(summary => summary.Score is >= 0m and <= 1m
? Result.Success()
: Result.Failure("score.range", "score must be between 0 and 1"));
var result = await client.CompleteAsync<Summary>(request, cancellationToken);
if (result.IsFailure)
{
// AiErrorCodes.Timeout / Unavailable / InvalidOutput / Cancelled / RateLimited
logger.LogWarning("AI unavailable: {Code}", result.Error.Code);
return FallbackPlan();
}
var completion = result.GetValueOrThrow();
logger.LogInformation(
"{Model} {In}/{Out} tokens in {Duration}, {Attempts} attempt(s)",
completion.Usage.Model, completion.Usage.InputTokens, completion.Usage.OutputTokens,
completion.Usage.Duration, completion.Usage.Attempts);
How JSON Schema validation actually works here
This is the part worth reading before you trust the output.
There is no JSON Schema validator in this package, and that is deliberate. The .NET BCL does not ship one, and the dependency policy of this package family forbids third-party libraries. So "validation" here is two checks made by the type system rather than one check made by a schema engine:
Strict deserialisation (
AiStructuredOutput.StrictOptions).UnmappedMemberHandling.Disallow— a property the schema did not define fails the parse. Silently dropping an invented"confidence_note"would leave no trace that the model went off contract, and next time the invented name may be one that matters.PropertyNameCaseInsensitive = false—"Symbol"does not quietly matchsymbol. Case-insensitive matching would turn the names in the schema into suggestions.requiredmembers are enforced bySystem.Text.Jsonitself: a missing one throws, which becomes a failedResult.- JSON comments are tolerated (
ReadCommentHandling.Skip). Models like to annotate their JSON, and spending a repair round trip on a//line is waste.
Between them these cover the type, required, and no-additional-properties families of schema constraint — which is most of what a schema actually says about an object.
Your semantic validator (
AiRequest.WithValidator<T>). Numeric ranges, enum allow-lists, cross-field consistency. Nothing is lost by doing these here, because JSON Schema does not enforceminimum/maximumin a structured-output setting anyway. If a value must not exceed 2%, that is a check you write, in either design.
The generated schema (AiStructuredOutput.CreateSchema<T>(), derived by the BCL's JsonSchemaExporter) therefore
has two jobs: it is appended to the prompt so the model knows what shape to answer with, and it can be handed to a
backend that enforces a format natively. It is never walked clause by clause against the returned JSON.
The repair round
When either check fails, the implementation hands the failure message back to the model and asks again, up to
MaxRepairAttempts times (default 1, maximum 3). AiUsage.Attempts reports how many sends it actually took.
When the budget runs out, the result is an ai.invalid_output failure whose Error.Data carries the final
validation message and the first 500 characters of the output (AiErrorDataKeys). That data is for a person —
it is the difference between "the prompt was underspecified" and "the schema asks for too much" — and should not be
branched on.
Why the model is given no tools
The contract is text in, JSON out, and nothing else. Implementations of IAiCompletionClient are expected to run
the model with no tool access at all: no file reads, no shell, no network fetches of its own.
- Everything the model is allowed to see is in the prompt. The caller assembles that context and can therefore redact it. A model that can read files decides for itself what to look at, and the caller loses the ability to say what is off limits.
- The output is a recommendation, not an action. A request produces one JSON object, which the caller is free to reject. Tools would let the model change the world on the way to answering, and the caller's validation would then be arriving after the fact.
- It makes the call reproducible. The same prompt gives a comparable answer regardless of what happens to be in the working directory.
- It is faster and cheaper. No tool round trips, no directory scanning before the first token.
If a caller needs the model to see a file, the caller reads the file and puts it in UserPrompt.
What is in the package
| Type | Purpose |
|---|---|
IAiCompletionClient |
The one-method contract. |
AiRequest |
Prompts, model and timeout overrides, repair budget, optional semantic validator. Non-generic, so one request can be reused for different output types. |
AiCompletion<T> |
The validated value, the raw output text, and the metering. |
AiUsage |
Model actually used, token counts, cache tokens, decimal cost, duration, attempts. |
AiStructuredOutput |
Schema generation, prompt instructions, fence stripping, strict deserialisation. |
AiErrorCodes / AiErrors / AiErrorDataKeys |
The error contract, with one place that maps a code to a category. |
Notes on two design choices
AiRequest is a class, not a record. A record's generated ToString() prints every property, and
UserPrompt in practice holds account and performance data — logging the request object would log all of it.
The validator is stored as a Delegate. The expected output type belongs to the call, not to the request, so
AiRequest carries no generic parameter. WithValidator<T> and GetValidator<T> are the typed way in and out;
using a request built with WithValidator<A> on CompleteAsync<B> throws, because dropping the validation
silently would leave the caller with no way to notice.
Implementations
Ozakboy.Ai.ClaudeCli— runs the locally installed Claude Code CLI as a subprocess, with no tools and no session persistence.
License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Ozakboy.Core.Abstractions (>= 0.3.0)
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.1.0 | 91 | 9/13/2026 |
0.1.0 — 首次發佈。First release.