Indiko.Blocks.AI.OpenRouter
5.0.0
See the version list below for details.
dotnet add package Indiko.Blocks.AI.OpenRouter --version 5.0.0
NuGet\Install-Package Indiko.Blocks.AI.OpenRouter -Version 5.0.0
<PackageReference Include="Indiko.Blocks.AI.OpenRouter" Version="5.0.0" />
<PackageVersion Include="Indiko.Blocks.AI.OpenRouter" Version="5.0.0" />
<PackageReference Include="Indiko.Blocks.AI.OpenRouter" />
paket add Indiko.Blocks.AI.OpenRouter --version 5.0.0
#r "nuget: Indiko.Blocks.AI.OpenRouter, 5.0.0"
#:package Indiko.Blocks.AI.OpenRouter@5.0.0
#addin nuget:?package=Indiko.Blocks.AI.OpenRouter&version=5.0.0
#tool nuget:?package=Indiko.Blocks.AI.OpenRouter&version=5.0.0
Indiko.Blocks.AI.OpenRouter
OpenRouter for the Indiko AI abstraction layer: one API key for hundreds of models, with provider routing, fallback chains and per-call cost reporting.
Overview
This package implements the chat, streaming, vision, file, OCR and batch
interfaces from Indiko.Blocks.AI.Abstractions against the OpenRouter API.
Why this and not Indiko.Blocks.AI.OpenRouter with an Endpoint
The OpenRouter block speaks the OpenAI wire protocol, and OpenRouter accepts it —
so pointing that block at https://openrouter.ai/api/v1 works, and for plain
chat against a single model it is the simpler choice. Use it when that is all
you need.
What it cannot carry is everything OpenRouter adds on top of the OpenAI schema, because the OpenAI SDK has no field for it and drops it:
OpenRouter block with Endpoint |
This block | |
|---|---|---|
| Chat, vision, streaming | yes | yes |
Provider routing (provider) |
dropped | yes |
Model fallback chain (models) |
dropped | yes |
Cost and token accounting (usage.include) |
dropped | yes |
| Reasoning budget, prompt transforms | dropped | yes |
| Model catalogue, credits, key limits | — | yes |
| PDF/file content parts | pre-extract to text yourself | sent as a file |
IAiResponseStream, IOcrProcessor, IImageBatchProcessor registered |
no | yes |
A dropped routing constraint is the worst kind of bug: the call succeeds, from the wrong provider, and nothing says so. That is why this is a block rather than an option.
Key Components
OpenRouterAiBlock(block entry point).OpenRouterApiClient(IOpenRouterApiClient) — the transport, with the provider name, finish reason and cost of each call.OpenRouterCatalogService(IOpenRouterCatalogService) — models, credits, key limits.OpenRouterChatService(IAiChatService).OpenRouterResponseStream(IAiResponseStream).OpenRouterVisionService(IAiVisionService).OpenRouterFileService(IAiFileService).OpenRouterOcrProcessor(IOcrProcessor).OpenRouterImageBatchProcessor(IImageBatchProcessor).- Options:
OpenRouterOptions,OpenRouterProviderPreferences,OpenRouterMaxPrice. - Models:
OpenRouterChatRequest,OpenRouterModelInfo,OpenRouterCreditsInfo,OpenRouterKeyInfo,OpenRouterException.
DI Registration Entry Point
OpenRouterAiBlock.ConfigureServices(...). There is no AddOpenRouter() to
call — BlockManager finds the block by reflection, so a package reference
plus an OpenRouterOptions section is the whole wiring.
It registers, both unkeyed and under the key "OpenRouter":
IAiChatService→OpenRouterChatServiceIAiResponseStream→OpenRouterResponseStreamIAiVisionService→OpenRouterVisionServiceIAiFileService→OpenRouterFileServiceIOcrProcessor→OpenRouterOcrProcessorIImageBatchProcessor→OpenRouterImageBatchProcessor
and unkeyed only: IOpenRouterApiClient, IOpenRouterCatalogService.
Configuration
Configuration section: OpenRouterOptions
Enabled |
false registers nothing. See the caveats |
Platform |
from AiOptions |
ApiKey |
sk-or-v1-…, from the secret store, never from source control |
Model |
OpenRouter notation vendor/model, or openrouter/auto |
FallbackModels |
models to try when Model cannot serve the call |
VisionModel |
model for image work; falls back to Model |
FileModel |
model for document work; falls back to VisionModel, then Model |
Endpoint |
defaults to https://openrouter.ai/api/v1 |
SiteUrl |
sent as HTTP-Referer — app attribution |
AppName |
sent as X-Title — app attribution |
TrackUsageAndCost |
default true; fills AiChatMessage.Usage |
Provider |
provider routing, see below |
Transforms |
prompt transforms, e.g. middle-out |
Timeout |
default 3 minutes |
MaxBatchConcurrency |
default 4; images in flight at once |
Minimal:
{
"OpenRouterOptions": {
"Enabled": true,
"ApiKey": "sk-or-v1-...",
"Model": "anthropic/claude-sonnet-4.5"
}
}
With routing, fallbacks and attribution:
{
"OpenRouterOptions": {
"Enabled": true,
"ApiKey": "sk-or-v1-...",
"Model": "anthropic/claude-sonnet-4.5",
"FallbackModels": [ "openai/gpt-4o-mini", "google/gemini-2.5-flash" ],
"VisionModel": "google/gemini-2.5-flash",
"SiteUrl": "https://example.com",
"AppName": "Example Service",
"Provider": {
"Order": [ "anthropic", "google-vertex" ],
"AllowFallbacks": true,
"RequireParameters": true,
"DataCollection": "deny",
"Sort": "throughput",
"MaxPrice": { "Prompt": 5, "Completion": 20 }
}
}
}
Provider routing
Provider.Order picks who serves a model; FallbackModels picks
another model entirely. They are different mechanisms and both are
useful:
Order+AllowFallbacks: falsepins the call to named providers. Buys predictability at the price of availability — right when the provider is a compliance decision, wrong when it is a price preference.DataCollection: "deny"routes only to providers that do not store prompts.RequireParameters: trueskips providers that do not support every parameter in the request. Set this whenever you useResponseFormat— without it the call can land on a provider that ignores the field and answers in prose, and the parse failure then looks like a model problem rather than a routing one.Sortdisables OpenRouter's own load balancing, which weighs price, latency and throughput together."price"really does mean the cheapest provider every time, including when it is overloaded.Quantizationspins weight precision. A quantised model is cheaper and measurably worse at long reasoning; pinning it is how you stop a provider swap from quietly changing answer quality.
Runtime Behavior and Caveats
A missing or disabled section registers nothing — it does not throw.
A host that references this package and leaves it unconfigured gets a service
without AI, which is what it asked for. Resolve IAiChatService with
GetService rather than GetRequiredService to make the feature optional.
Enabled with an empty ApiKey or Model does throw. That is a typo, not
a decision.
This block honours
MaxTokens,TemperatureandTopPfromAiChatCompletionRequest— including their defaults.MaxTokensdefaults to 256, which is about a paragraph. The OpenRouter, Mistral and LocalLLM blocks ignore the field entirely, so a request moved over from one of them will suddenly stop mid-answer. Set it explicitly, or set it to0for no cap.
Two enabled AI blocks: the unkeyed registration is last-wins. Every AI
block registers IAiChatService unkeyed, and which one survives depends on
assembly scan order — which is to say arbitrary. The block logs a warning
when it finds another one already registered. To be explicit:
public sealed class Summariser(
[FromKeyedServices(OpenRouterServiceKeys.OpenRouter)] IAiChatService chat)
{
// ...
}
Only this block registers keyed services; pulling the others along would change published packages nobody asked to change.
The vision service does not substitute a model of its own choosing. If
Model cannot see and no VisionModel is configured, the call fails with the
provider's own error. The OpenAI block swaps in gpt-4o when the model name
lacks the word "vision" — a name is not a capability, and answering from a
model nobody chose is the worse failure. Ask IOpenRouterCatalogService for
the real modalities instead.
OCR reports no bounding boxes and a confidence of 0.9. A vision model is not an OCR engine: it reads text well and measures nothing. The flat 0.9 is a judgement, set below 1.0 so a caller filtering on confidence has something to filter. For layout-aware extraction, use a dedicated OCR service.
A failed image does not fail a batch. Its error lands in
ImageBatchResult.Errors and the rest continue — which also means a non-empty
Errors list is the only sign the results are incomplete, since the result
lists are not index-aligned with the input.
Errors are typed. OpenRouterException carries the status, the upstream
provider and OpenRouter's error metadata, plus IsTransient (429, timeouts,
upstream failures) and IsOutOfCredit (402). Retrying a 402 will not help;
topping up will.
Minimal Usage
var chat = serviceProvider.GetRequiredService<IAiChatService>();
var answer = await chat.ChatCompletionAsync(
new AiChatCompletionRequest
{
MessageHistory =
[
new AiChatMessage(AiChatMessageRole.System, "Answer in one sentence."),
new AiChatMessage(AiChatMessageRole.User, "Why is the sky blue?"),
],
MaxTokens = 2000,
},
cancellationToken);
Console.WriteLine(answer.Text);
Console.WriteLine($"{answer.Usage?.TotalTokens} tokens, ${answer.Usage?.CostUsd} via {answer.Usage?.ProviderName}");
Per-call routing
var request = new OpenRouterChatRequest
{
MessageHistory = [new AiChatMessage(AiChatMessageRole.User, prompt)],
MaxTokens = 4000,
Model = "anthropic/claude-sonnet-4.5",
FallbackModels = ["openai/gpt-4o-mini"],
Provider = new OpenRouterProviderPreferences
{
DataCollection = "deny",
RequireParameters = true,
},
Reasoning = new OpenRouterReasoningOptions { Effort = "high" },
};
var result = await apiClient.CompleteChatAsync(request, cancellationToken: ct);
Every service takes the plain AiChatCompletionRequest, so portable code
keeps working; pass OpenRouterChatRequest and the extras are picked up.
Streaming
await foreach (var chunk in stream.StreamChatCompletionAsync(request, ct))
{
if (chunk.IsFinal)
{
logger.LogInformation("Cost: ${cost}", chunk.Usage?.CostUsd);
break;
}
Console.Write(chunk.Text);
}
Usage rides on the final chunk because it is not known before the generation ends.
Picking a model at runtime
var models = await catalog.GetModelsAsync(ct);
var cheapestVisionModel = models
.Where(m => m.SupportsImages && m.ContextLength >= 100_000)
.OrderBy(m => m.PromptPricePerThousandTokens)
.First();
var credits = await catalog.GetCreditsAsync(ct);
logger.LogInformation("${remaining} left", credits.Remaining);
Target Framework
- .NET 10
Related Packages
Indiko.Blocks.AI.AbstractionsIndiko.Blocks.AI.OpenRouter— OpenAI and other OpenAI-protocol providersIndiko.Blocks.AI.MistralIndiko.Blocks.AI.LocalLLM
| 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
- CommunityToolkit.Diagnostics (>= 8.4.2)
- Indiko.Blocks.AI.Abstractions (>= 5.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.