Indiko.Blocks.AI.OpenRouter 6.0.2

dotnet add package Indiko.Blocks.AI.OpenRouter --version 6.0.2
                    
NuGet\Install-Package Indiko.Blocks.AI.OpenRouter -Version 6.0.2
                    
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="6.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Indiko.Blocks.AI.OpenRouter" Version="6.0.2" />
                    
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 6.0.2
                    
#r "nuget: Indiko.Blocks.AI.OpenRouter, 6.0.2"
                    
#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@6.0.2
                    
#: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=6.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Indiko.Blocks.AI.OpenRouter&version=6.0.2
                    
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
Per-image detail (low/high) dropped yes
IAiResponseStream, IOcrProcessor, IImageBatchProcessor registered no yes
Image generation (IAiImageGenerationService) — 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) — reads images.
  • OpenRouterImageGenerationService (IAiImageGenerationService) — draws them.
  • Options: OpenRouterOptions, OpenRouterProviderPreferences, OpenRouterMaxPrice.
  • Models: OpenRouterChatRequest, OpenRouterImageRequest, 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
  • IAiImageGenerationService → OpenRouterImageGenerationService

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
ImageModel model for image generation; no fallback to Model
ImageSize default pixel size for generated images, "{w}x{h}"
ImageOutputFormat default encoding: png, jpeg, webp or svg
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
OmitSamplingParameters leave temperature/top_p out. Set it for a reasoning model
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}");

Reasoning models and require_parameters

Set OmitSamplingParameters whenever Model is a reasoning model — OpenAI's gpt-5/gpt-6 and o-series, the newer Claude models:

{
  "OpenRouterOptions": {
    "Model": "openai/gpt-6-luna",
    "OmitSamplingParameters": true,
    "Provider": { "RequireParameters": true }
  }
}

Those models take no sampling parameters, and OpenRouter's catalogue says so — not one endpoint of openai/gpt-6-luna lists temperature among its supported parameters. Sending them anyway is normally harmless, because a provider ignores what it does not understand.

Together with RequireParameters it is not harmless. That setting asks OpenRouter to route only to providers that support every parameter in the request, so an unsupported temperature empties the candidate list and the call fails with:

404 No endpoints found that can handle the requested parameters.

Which reads like a missing model, and is a parameter problem. Both settings are individually reasonable — that is exactly what makes the combination worth knowing about. To check a model before configuring it:

GET https://openrouter.ai/api/v1/models/{vendor}/{model}/endpoints

and look for temperature in each endpoint's supported_parameters.

Per request, OpenRouterChatRequest.OmitSamplingParameters overrides the option in either direction.

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.

Generating an image

public sealed class CoverImageService(IAiImageGenerationService images)
{
    public async Task<string> DrawAsync(string prompt, Guid postId, CancellationToken ct)
    {
        var result = await images.GenerateImageAsync(
            new AiImageGenerationRequest
            {
                Prompt = prompt,
                AspectRatio = "16:9",
                OutputFormat = "jpeg",
            },
            ct);

        var image = result.FirstImage;
        if (image is null)
        {
            return null;
        }

        var path = $"uploads/{postId}.{image.FileExtension}";
        await File.WriteAllBytesAsync(path, image.ToBytes(), ct);

        return path;
    }
}

Three things differ from the OpenAI image API and catch people out:

  • The bytes come back inline, base64-encoded. There is no hosted URL to download, and no response_format to ask for one — OpenRouter ignores the field rather than rejecting it. That removes a second HTTP call and an expiring link, and it means a large image is a large response body.
  • ImageModel has no fallback to Model. A text model cannot draw, so falling back would turn a missing setting into a provider-side 400 that reads like an outage. Unset, the call throws ArgumentException naming the option.
  • Cost is always reported on result.Usage.CostUsd, without TrackUsageAndCost: the image endpoint returns usage unconditionally.

Count above 1 asks for several images; Count of 1 or 0 leaves n out of the request, because OpenRouter requires n >= 1 and the provider's own default is the better answer than a guess here.

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 46 10/23/2026
6.0.1 30 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