Indiko.Blocks.AI.OpenRouter 5.0.0

There is a newer version of this package available.
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
                    
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="Indiko.Blocks.AI.OpenRouter" Version="5.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Indiko.Blocks.AI.OpenRouter" Version="5.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Indiko.Blocks.AI.OpenRouter" />
                    
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 Indiko.Blocks.AI.OpenRouter --version 5.0.0
                    
#r "nuget: Indiko.Blocks.AI.OpenRouter, 5.0.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 Indiko.Blocks.AI.OpenRouter@5.0.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=Indiko.Blocks.AI.OpenRouter&version=5.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Indiko.Blocks.AI.OpenRouter&version=5.0.0
                    
Install as a Cake Tool

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 → OpenRouterChatService
  • IAiResponseStream → OpenRouterResponseStream
  • IAiVisionService → OpenRouterVisionService
  • IAiFileService → OpenRouterFileService
  • IOcrProcessor → OpenRouterOcrProcessor
  • IImageBatchProcessor → 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: false pins 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: true skips providers that do not support every parameter in the request. Set this whenever you use ResponseFormat — 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.
  • Sort disables 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.
  • Quantizations pins 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, Temperature and TopP from AiChatCompletionRequest — including their defaults. MaxTokens defaults 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 to 0 for 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
  • Indiko.Blocks.AI.Abstractions
  • Indiko.Blocks.AI.OpenRouter — OpenAI and other OpenAI-protocol providers
  • Indiko.Blocks.AI.Mistral
  • Indiko.Blocks.AI.LocalLLM
Product 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. 
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
6.0.2 52 10/23/2026
6.0.1 34 10/23/2026
6.0.0 57 10/22/2026
5.1.0 46 10/22/2026
5.0.0 67 10/21/2026
4.3.1 51 10/21/2026