Skip to content

feat: add openrouterwebapi sample#6

Open
WilliamAvHolmberg wants to merge 1 commit into
mainfrom
feat/add-sample-web-api
Open

feat: add openrouterwebapi sample#6
WilliamAvHolmberg wants to merge 1 commit into
mainfrom
feat/add-sample-web-api

Conversation

@WilliamAvHolmberg

@WilliamAvHolmberg WilliamAvHolmberg commented Oct 23, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added a comprehensive ASP.NET Core Web API sample demonstrating OpenRouter integration with chat endpoints (basic, streaming, conversation history), model browsing, artifact generation, multimodal chat with images, and function calling capabilities.
  • Documentation

    • Included setup guide, API documentation, example configurations, and HTTP request samples.

@coderabbitai

coderabbitai Bot commented Oct 23, 2025

Copy link
Copy Markdown

Walkthrough

A new comprehensive ASP.NET Core web API sample project demonstrating OpenRouter.NET integration with controllers for chat operations, model management, streaming responses, artifacts, multimodal content, and function calling. Includes configuration, tools, and documentation.

Changes

Cohort / File(s) Summary
Chat & Model Controllers
samples/OpenRouterWebApiSample/Controllers/ChatController.cs, samples/OpenRouterWebApiSample/Controllers/ModelsController.cs
Adds two ASP.NET Core API controllers: ChatController exposes seven endpoints for basic chat, streaming, conversations, artifact generation, multimodal input, and tool-based interactions with full error handling and logging; ModelsController exposes three endpoints for retrieving models, popular models, and account limits.
DTOs and Request/Response Models
samples/OpenRouterWebApiSample/Models/Dtos.cs
Defines ten public record types including ChatRequest, ChatResponse, TokenUsage, StreamChatRequest, ConversationRequest, ArtifactRequest, ArtifactResponse, ToolRequest, and MultimodalRequest to structure API payloads and responses.
Tool Implementations
samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs
Provides a CalculatorTools class with four arithmetic operations (Add, Multiply, Subtract, Divide) annotated for OpenRouter function calling, including validation for division by zero.
Application Setup
samples/OpenRouterWebApiSample/Program.cs, samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj
Configures the ASP.NET Core application, registers OpenRouterClient as a singleton with API key configuration, enables controllers and OpenAPI support; defines project targeting net9.0 with dependencies on Microsoft.AspNetCore.OpenApi and OpenRouter.NET.
Configuration Files
samples/OpenRouterWebApiSample/appsettings.json, samples/OpenRouterWebApiSample/appsettings.Development.json, samples/OpenRouterWebApiSample/appsettings.json.example, samples/OpenRouterWebApiSample/Properties/launchSettings.json
Establishes logging configuration (Information default, Warning for AspNetCore), allowed hosts settings, launch profiles for http and https, and example configuration template with placeholders for OpenRouter settings.
Documentation & Testing
samples/OpenRouterWebApiSample/README.md, samples/OpenRouterWebApiSample/OpenRouterWebApi.http
Provides comprehensive README documenting features, prerequisites, setup, endpoints, and implementation details; includes HTTP request file with sample payloads for all endpoints.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant ChatController
    participant OpenRouterClient
    participant OpenRouter API

    rect rgb(200, 220, 240)
    Note over Client,OpenRouter API: Basic Chat Flow
    Client->>ChatController: POST /api/chat/basic<br/>(ChatRequest)
    ChatController->>OpenRouterClient: CreateChatCompletionAsync(request)
    OpenRouterClient->>OpenRouter API: HTTP POST
    OpenRouter API-->>OpenRouterClient: ChatCompletion response
    OpenRouterClient-->>ChatController: ChatCompletion object
    ChatController->>ChatController: Map to ChatResponse<br/>(Content, Model, FinishReason, Usage)
    ChatController-->>Client: 200 OK + ChatResponse
    end

    rect rgb(240, 220, 200)
    Note over Client,OpenRouter API: Streaming Chat Flow
    Client->>ChatController: POST /api/chat/stream<br/>(StreamChatRequest)
    ChatController->>ChatController: Configure SSE response
    ChatController->>OpenRouterClient: StreamChatCompletionAsync(request)
    loop For each chunk
        OpenRouterClient->>OpenRouter API: Streaming connection
        OpenRouter API-->>OpenRouterClient: Delta chunks
        OpenRouterClient-->>ChatController: yield stream events
        ChatController-->>Client: event: data chunks
    end
    ChatController-->>Client: event: [DONE]
    end

    rect rgb(220, 240, 200)
    Note over Client,OpenRouter API: Chat with Tools Flow
    Client->>ChatController: POST /api/chat/tools<br/>(ToolRequest)
    ChatController->>ChatController: Register 4 calculator tools
    ChatController->>OpenRouterClient: StreamChatCompletionAsync(request)
    loop Handle stream events
        OpenRouter API-->>OpenRouterClient: Text delta / Tool call
        OpenRouterClient-->>ChatController: StreamEvent
        alt Tool call detected
            ChatController->>ChatController: Execute tool (Add/Multiply/etc)
            ChatController->>OpenRouterClient: Next turn with tool result
        else Text delta
            ChatController->>ChatController: Accumulate text
        end
    end
    ChatController-->>Client: 200 OK + Final text
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Rationale: The PR introduces a complete new sample application (12 files) demonstrating multiple OpenRouter.NET integration patterns—streaming, artifacts, multimodal, and function calling. While the scope is broad, the changes follow consistent, straightforward patterns. Review focus should cover correct API usage, error handling, configuration correctness, and documentation accuracy. Limited complexity in logic density; primarily demonstrative code and configuration.

Poem

🐰 Hoppy Code Review!

New sample app hops into view,
With chat, streams, and tools so true,
Calculator tricks and artifacts bright,
OpenRouter integration done right! 🎯
Config and docs make setup light.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: add openrouterwebapi sample" directly and accurately describes the primary change in this pull request, which is the addition of a complete new OpenRouter Web API sample project. The title uses a clear, actionable verb ("add") and specifies exactly what is being added ("openrouterwebapi sample"), making it easy for a reviewer scanning the project history to understand the purpose of the changeset. The title is concise, follows conventional commit formatting, and appropriately represents the scope of the changes across the multiple files in the samples/OpenRouterWebApiSample directory.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-sample-web-api

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (9)
samples/OpenRouterWebApiSample/README.md (2)

141-152: Add language identifier to fenced code block.

The project structure code block is missing a language specifier, which triggers markdown linters.

Apply this diff:

-```
+```text
 OpenRouterWebApiSample/
 ├── Controllers/
 │   ├── ChatController.cs      # Chat completion endpoints
 │   └── ModelsController.cs    # Model information endpoints
 ├── Models/
 │   └── Dtos.cs               # Request/response DTOs
 ├── Tools/
 │   └── CalculatorTools.cs    # Function calling example
 ├── Program.cs                # Application setup
 └── OpenRouterWebApi.http     # HTTP request examples

---

`31-42`: **Consider emphasizing environment variables more strongly for API keys.**

While Option B shows configuration file usage, the actual `appsettings.json` in the repo doesn't include the OpenRouter section (correctly avoiding secrets in source control). Consider adding a note that if using Option B, developers should add these settings to `appsettings.Development.json` or use user secrets for development, rather than `appsettings.json` which is typically committed to source control.

</blockquote></details>
<details>
<summary>samples/OpenRouterWebApiSample/Controllers/ModelsController.cs (2)</summary><blockquote>

`54-76`: **Speed up membership checks and avoid case sensitivity pitfalls.**

Use a HashSet with OrdinalIgnoreCase and query it.


```diff
-            var popularModels = new[]
-            {
-                "openai/gpt-4o",
-                "anthropic/claude-3.5-sonnet",
-                "meta-llama/llama-3.1-70b-instruct",
-                "google/gemini-pro"
-            };
+            var popularSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+            {
+                "openai/gpt-4o",
+                "anthropic/claude-3.5-sonnet",
+                "meta-llama/llama-3.1-70b-instruct",
+                "google/gemini-pro"
+            };
 
-            var filtered = models
-                .Where(m => popularModels.Contains(m.Id))
+            var filtered = models
+                .Where(m => popularSet.Contains(m.Id))
                 .Select(m => new
                 {
                     m.Id,
                     m.Name,
                     m.ContextLength,
                     Pricing = new
                     {
                         m.Pricing?.Prompt,
                         m.Pricing?.Completion
                     }
                 })
                 .ToList();

Also applies to: 62-65


24-25: Honor client disconnects by passing cancellation tokens to async API calls.

Pass HttpContext.RequestAborted to GetModelsAsync() and GetLimitsAsync() calls. Both methods support CancellationToken parameters (default value present).

Lines 24, 52, 91 in samples/OpenRouterWebApiSample/Controllers/ModelsController.cs:

-            var models = await _client.GetModelsAsync();
+            var models = await _client.GetModelsAsync(HttpContext.RequestAborted);
-            var limits = await _client.GetLimitsAsync();
+            var limits = await _client.GetLimitsAsync(HttpContext.RequestAborted);
samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs (1)

33-42: Be tolerant to near-zero divisors for double.

Direct equality can miss subnormal values; guard with an epsilon.

-    public double Divide(
+    public double Divide(
         [ToolParameter("First number")] double a,
         [ToolParameter("Second number")] double b)
     {
-        if (b == 0)
+        if (Math.Abs(b) < double.Epsilon)
             throw new ArgumentException("Cannot divide by zero");
         
         return a / b;
     }
samples/OpenRouterWebApiSample/Controllers/ChatController.cs (4)

106-110: Flush after writing SSE error.

Ensure the client receives the error event promptly.

         catch (Exception ex)
         {
             _logger.LogError(ex, "Streaming error");
-            await Response.WriteAsync($"data: [ERROR] {ex.Message}\n\n");
+            await Response.WriteAsync($"data: [ERROR] {ex.Message}\n\n");
+            await Response.Body.FlushAsync();
         }

246-252: Avoid per-request tool re-registration.

If OpenRouterClient is singleton/scoped, repeated RegisterTool calls can duplicate entries or throw. Prefer registering once at startup (DI configuration) or make registration idempotent.

If you must register per request, check before registering or clear between runs.

Also applies to: 300-306


46-47: Pass HttpContext.RequestAborted cancellation token to CreateChatCompletionAsync calls for proper client abort handling.

The method supports the cancellation token parameter (with default value). This applies to all three call sites in ChatController.cs:

-            var response = await _client.CreateChatCompletionAsync(chatRequest);
+            var response = await _client.CreateChatCompletionAsync(chatRequest, HttpContext.RequestAborted);

Lines: 46, 131, 221


90-105: Propagate cancellation tokens to streaming calls.

Both StreamAsync and ProcessMessageAsync accept cancellation tokens and should be updated to pass HttpContext.RequestAborted for proper resource cleanup when clients disconnect.

-            await foreach (var chunk in _client.StreamAsync(chatRequest))
+            await foreach (var chunk in _client.StreamAsync(chatRequest, HttpContext.RequestAborted))
-            var (response, history) = await _client.ProcessMessageAsync(chatRequest, maxToolCalls: 5);
+            var (response, history) = await _client.ProcessMessageAsync(chatRequest, maxToolCalls: 5, cancellationToken: HttpContext.RequestAborted);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 738f1ca and 00edbd3.

📒 Files selected for processing (12)
  • samples/OpenRouterWebApiSample/Controllers/ChatController.cs (1 hunks)
  • samples/OpenRouterWebApiSample/Controllers/ModelsController.cs (1 hunks)
  • samples/OpenRouterWebApiSample/Models/Dtos.cs (1 hunks)
  • samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj (1 hunks)
  • samples/OpenRouterWebApiSample/OpenRouterWebApi.http (1 hunks)
  • samples/OpenRouterWebApiSample/Program.cs (1 hunks)
  • samples/OpenRouterWebApiSample/Properties/launchSettings.json (1 hunks)
  • samples/OpenRouterWebApiSample/README.md (1 hunks)
  • samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs (1 hunks)
  • samples/OpenRouterWebApiSample/appsettings.Development.json (1 hunks)
  • samples/OpenRouterWebApiSample/appsettings.json (1 hunks)
  • samples/OpenRouterWebApiSample/appsettings.json.example (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
samples/OpenRouterWebApiSample/Controllers/ModelsController.cs (1)
samples/OpenRouterWebApiSample/Controllers/ChatController.cs (1)
  • ApiController (11-391)
samples/OpenRouterWebApiSample/Controllers/ChatController.cs (6)
samples/OpenRouterWebApiSample/Controllers/ModelsController.cs (1)
  • ApiController (6-101)
src/Models/Artifacts.cs (2)
  • Artifact (51-61)
  • ArtifactCompleted (41-48)
src/Models/Message.cs (1)
  • ContentPart (48-52)
samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs (1)
  • CalculatorTools (7-43)
src/Models/Tool.cs (1)
  • ToolCall (40-53)
src/Extensions/ResponseExtensions.cs (2)
  • GetContent (7-11)
  • GetFinishReason (19-22)
samples/OpenRouterWebApiSample/Program.cs (1)
src/OpenRouterClientOptions.cs (1)
  • OpenRouterClientOptions (3-11)
samples/OpenRouterWebApiSample/Models/Dtos.cs (1)
src/Artifacts/ArtifactDefinition.cs (1)
  • Artifacts (84-132)
🪛 markdownlint-cli2 (0.18.1)
samples/OpenRouterWebApiSample/README.md

141-141: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (7)
samples/OpenRouterWebApiSample/appsettings.Development.json (1)

1-8: LGTM!

Standard development logging configuration is appropriate for a sample project.

samples/OpenRouterWebApiSample/appsettings.json.example (1)

1-14: LGTM!

Good practice providing an example configuration file with clear placeholder values to guide developers without exposing actual secrets.

samples/OpenRouterWebApiSample/Properties/launchSettings.json (1)

1-23: LGTM!

Launch settings are properly configured with appropriate ports (matching README documentation) and launchBrowser disabled for an API project.

samples/OpenRouterWebApiSample/appsettings.json (1)

1-9: LGTM!

Correctly excludes sensitive OpenRouter configuration from the committed settings file, relying on environment variables or development-specific configuration instead.

samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj (1)

1-14: LGTM!

Project configuration is appropriate for a modern .NET 9.0 Web API sample with nullable reference types and implicit usings enabled.

samples/OpenRouterWebApiSample/Program.cs (1)

1-38: LGTM!

The startup configuration properly handles API key loading with a clear fallback mechanism (config → environment variable), registers the OpenRouterClient as a singleton, and correctly restricts OpenAPI to development environments.

samples/OpenRouterWebApiSample/OpenRouterWebApi.http (1)

1-99: LGTM!

The HTTP request examples comprehensively cover all documented endpoints with realistic payloads, providing excellent reference material for developers testing the sample API.

Comment on lines +48 to +57
return Ok(new ChatResponse(
Content: response.Choices?[0]?.Message?.Content?.ToString() ?? "No response",
Model: response.Model ?? request.Model,
FinishReason: response.Choices?[0]?.FinishReason,
Usage: response.Usage != null ? new TokenUsage(
response.Usage.TotalTokens,
response.Usage.PromptTokens,
response.Usage.CompletionTokens
) : null
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid IndexOutOfRange on response.Choices.

Use FirstOrDefault() instead of [0] to handle empty choices safely.

-            return Ok(new ChatResponse(
-                Content: response.Choices?[0]?.Message?.Content?.ToString() ?? "No response",
-                Model: response.Model ?? request.Model,
-                FinishReason: response.Choices?[0]?.FinishReason,
+            return Ok(new ChatResponse(
+                Content: response.Choices?.FirstOrDefault()?.Message?.Content?.ToString() ?? "No response",
+                Model: response.Model ?? request.Model,
+                FinishReason: response.Choices?.FirstOrDefault()?.FinishReason,
                 Usage: response.Usage != null ? new TokenUsage(
                     response.Usage.TotalTokens,
                     response.Usage.PromptTokens,
                     response.Usage.CompletionTokens
                 ) : null
             ));

Apply the same change to the Conversation and Multimodal responses (shown in this hunk).

Also applies to: 133-142, 223-232

🤖 Prompt for AI Agents
In samples/OpenRouterWebApiSample/Controllers/ChatController.cs around lines
48-57, the code accesses response.Choices[0] which can throw IndexOutOfRange
when choices is empty; replace uses of [0] with
response.Choices?.FirstOrDefault() (or .FirstOrDefault() stored in a local
variable) and null-conditional checks before accessing
Message/Content/FinishReason so it safely falls back to defaults; apply the same
change pattern to the other occurrences at lines 133-142 and 223-232
(Conversation and Multimodal response blocks) to avoid indexing an empty
collection.

Comment on lines +207 to +210
foreach (var imageUrl in request.ImageUrls)
{
contentParts.Add(new ImageContent(imageUrl));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Null-safe iteration over image URLs.

Guard against null to prevent NREs.

-            foreach (var imageUrl in request.ImageUrls)
+            foreach (var imageUrl in request.ImageUrls ?? Enumerable.Empty<string>())
             {
                 contentParts.Add(new ImageContent(imageUrl));
             }

Alternative: mark ImageUrls as [Required] in DTOs (see Dtos.cs comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (var imageUrl in request.ImageUrls)
{
contentParts.Add(new ImageContent(imageUrl));
}
foreach (var imageUrl in request.ImageUrls ?? Enumerable.Empty<string>())
{
contentParts.Add(new ImageContent(imageUrl));
}
🤖 Prompt for AI Agents
In samples/OpenRouterWebApiSample/Controllers/ChatController.cs around lines 207
to 210, the foreach over request.ImageUrls can throw a null reference if
ImageUrls is null; change the code to guard against null by iterating over
request.ImageUrls ?? Enumerable.Empty<string>() or add an explicit if
(request.ImageUrls != null) check before the loop, or alternatively make
ImageUrls required on the DTOs per the Dtos.cs suggestion.

Comment on lines +265 to +271
_logger.LogInformation(
"Tool {ToolName} - State: {State}, Result: {Result}",
chunk.ServerTool.ToolName,
chunk.ServerTool.State,
chunk.ServerTool.Result
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reduce logging of tool results to avoid sensitive-data leakage.

Tool results may contain user data. Log metadata, not payloads, or mask them.

-                    _logger.LogInformation(
-                        "Tool {ToolName} - State: {State}, Result: {Result}",
-                        chunk.ServerTool.ToolName,
-                        chunk.ServerTool.State,
-                        chunk.ServerTool.Result
-                    );
+                    _logger.LogInformation(
+                        "Tool {ToolName} - State: {State}",
+                        chunk.ServerTool.ToolName,
+                        chunk.ServerTool.State
+                    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_logger.LogInformation(
"Tool {ToolName} - State: {State}, Result: {Result}",
chunk.ServerTool.ToolName,
chunk.ServerTool.State,
chunk.ServerTool.Result
);
}
_logger.LogInformation(
"Tool {ToolName} - State: {State}",
chunk.ServerTool.ToolName,
chunk.ServerTool.State
);
🤖 Prompt for AI Agents
In samples/OpenRouterWebApiSample/Controllers/ChatController.cs around lines 265
to 271, the current logger writes the full tool result which may contain
user-sensitive data; change the logging to avoid payloads by logging only
metadata (e.g., ToolName and State) and either omit the Result field or replace
it with a redacted placeholder or safe summary (e.g., result length, truncated
hash, or "[REDACTED]"). Update the LogInformation call to remove
chunk.ServerTool.Result from structured output or pass a masked value produced
by a small helper (MaskSensitive(string) or Summary(string)) so no raw tool
payload is emitted to logs. Ensure any helper used is deterministic and does not
include user content.

Comment on lines +309 to +352
// Subscribe to streaming events
_client.OnStreamEvent += async (sender, e) =>
{
_logger.LogInformation("Stream Event: {EventType}, State: {State}", e.EventType, e.State);

var eventData = e.EventType switch
{
StreamEventType.StateChange => (object)new
{
type = "stateChange",
state = e.State.ToString()
},
StreamEventType.TextContent => (object)new
{
type = "text",
content = e.TextDelta
},
StreamEventType.ToolCall => (object)new
{
type = "toolCall",
toolName = e.ToolName,
toolCall = e.ToolCall
},
StreamEventType.ToolResult => (object)new
{
type = "toolResult",
toolName = e.ToolName,
result = e.ToolResult
},
StreamEventType.Error => (object)new
{
type = "error",
error = e.ToolResult
},
_ => null
};

if (eventData != null)
{
var eventJson = System.Text.Json.JsonSerializer.Serialize(eventData);
await Response.WriteAsync($"event: {e.EventType}\ndata: {eventJson}\n\n");
await Response.Body.FlushAsync();
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Event handler leak + concurrent Response writes in SSE tool streaming.

Subscribing per-request without unsubscribe risks memory leaks and cross-request data leakage if OpenRouterClient is singleton. Also, multiple event invocations can write concurrently to the Response, which is not thread-safe.

  • Capture a handler, subscribe, and always unsubscribe in finally.
  • Serialize writes with a gate to avoid interleaving.
  • If supported, pass HttpContext.RequestAborted to downstream APIs.

Example patch (adjust delegate type to match OnStreamEvent):

-            // Subscribe to streaming events
-            _client.OnStreamEvent += async (sender, e) =>
-            {
+            // Subscribe to streaming events
+            var writeGate = new System.Threading.SemaphoreSlim(1, 1);
+            EventHandler<StreamEventArgs> handler = async (sender, e) =>
+            {
+                await writeGate.WaitAsync(HttpContext.RequestAborted);
+                try
+                {
                 _logger.LogInformation("Stream Event: {EventType}, State: {State}", e.EventType, e.State);
 
                 var eventData = e.EventType switch
                 {
                     StreamEventType.StateChange => (object)new
                     {
                         type = "stateChange",
                         state = e.State.ToString()
                     },
                     StreamEventType.TextContent => (object)new
                     {
                         type = "text",
                         content = e.TextDelta
                     },
                     StreamEventType.ToolCall => (object)new
                     {
                         type = "toolCall",
                         toolName = e.ToolName,
                         toolCall = e.ToolCall
                     },
                     StreamEventType.ToolResult => (object)new
                     {
                         type = "toolResult",
                         toolName = e.ToolName,
                         result = e.ToolResult
                     },
                     StreamEventType.Error => (object)new
                     {
                         type = "error",
                         error = e.ToolResult
                     },
                     _ => null
                 };
 
                 if (eventData != null)
                 {
                     var eventJson = System.Text.Json.JsonSerializer.Serialize(eventData);
                     await Response.WriteAsync($"event: {e.EventType}\ndata: {eventJson}\n\n", HttpContext.RequestAborted);
                     await Response.Body.FlushAsync(HttpContext.RequestAborted);
                 }
-            };
+                }
+                finally
+                {
+                    writeGate.Release();
+                }
+            };
+            _client.OnStreamEvent += handler;

And ensure unsubscription:

-            await Response.Body.FlushAsync();
+            await Response.Body.FlushAsync();
+        }
+        finally
+        {
+            try { _client.OnStreamEvent -= handler; } catch { /* best-effort cleanup */ }
         }

Also applies to: 374-377


🏁 Script executed:

#!/bin/bash
# Verify the delegate type of OnStreamEvent to adjust the handler signature.
rg -nP -C2 'OnStreamEvent' --type cs

Length of output: 1215


I need to verify the scope of _client in the ChatController and check if there's already proper cleanup logic:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check _client declaration and scope in ChatController
rg -nP -C10 'class ChatController' samples/OpenRouterWebApiSample/Controllers/ChatController.cs | head -30

# Check if there's a try-finally or using pattern around the stream handler
rg -nP -B5 -A50 'OnStreamEvent \+=' samples/OpenRouterWebApiSample/Controllers/ChatController.cs | head -80

Length of output: 2926


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Get full method context to see if there's try-finally wrapping
rg -nP -B15 'OnStreamEvent \+=' samples/OpenRouterWebApiSample/Controllers/ChatController.cs | head -40

# Check for method signature containing line 310
sed -n '280,360p' samples/OpenRouterWebApiSample/Controllers/ChatController.cs

# Search for OpenRouterClient DI registration
fd -e cs -e json | xargs rg -l 'OpenRouterClient' | head -5

Length of output: 4210


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Get rest of the method to see finally block
sed -n '350,410p' samples/OpenRouterWebApiSample/Controllers/ChatController.cs

# Search for method end and finally block
rg -nP -A80 'StreamChatWithTools' samples/OpenRouterWebApiSample/Controllers/ChatController.cs | tail -100

# Check DI registration - look in Program.cs or Startup
fd -e cs | xargs rg -l 'AddScoped|AddSingleton|AddTransient' | head -3

Length of output: 5186


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check DI registration in Program.cs
cat samples/OpenRouterWebApiSample/Program.cs

Length of output: 957


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check the "Also applies to: 374-377" location
sed -n '370,380p' samples/OpenRouterWebApiSample/Controllers/ChatController.cs

Length of output: 481


Add unsubscribe logic and serialize Response writes to prevent handler leak and concurrent write errors.

The review comment is correct. Verification confirms:

  • OpenRouterClient is registered as AddSingleton (Program.cs line 17), so the _client instance persists across all requests.
  • Handler leak confirmed: The event subscription at line 310 lacks any unsubscribe logic; there is no finally block in the method (lines 295-387), so handlers accumulate on each request call.
  • Concurrent writes confirmed: Multiple event handlers can invoke simultaneously and write directly to Response without synchronization, which is not thread-safe.
  • Lines 374-377 show additional unprotected Response writes that must also be gated.

The suggested fix—capturing the handler reference, unsubscribing in a finally block, using a SemaphoreSlim to serialize writes, and passing HttpContext.RequestAborted—directly addresses these real issues.

🤖 Prompt for AI Agents
In samples/OpenRouterWebApiSample/Controllers/ChatController.cs around lines 309
to 352, the streamed event handler is added to the singleton _client without
being removed and writes to Response concurrently; fix by capturing the delegate
when subscribing, use a SemaphoreSlim (shared inside the request scope) to
serialize all Response.WriteAsync/FlushAsync calls (including the unprotected
writes around lines 374-377), pass HttpContext.RequestAborted into the async
write calls, and ensure you unsubscribe the captured handler in a finally block
so the handler does not leak after the request completes or is cancelled.

Comment on lines +40 to +44
catch (OpenRouterException ex)
{
_logger.LogError(ex, "Error fetching models");
return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align error responses with ChatController (include error code).

Return the provider’s error code for consistency and easier client handling.

-            return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message });
+            return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode });

Apply similarly to the other two catch blocks.

Also applies to: 79-83, 95-99

🤖 Prompt for AI Agents
In samples/OpenRouterWebApiSample/Controllers/ModelsController.cs around lines
40-44 (and similarly update lines 79-83 and 95-99), the catch blocks currently
return only the error message; change the returned payload to include the
provider error code as well by adding the exception's error-code property (e.g.,
ex.ErrorCode or ex.Code) to the anonymous response object so the controller
returns { error = ex.Message, code = ex.ErrorCode } (use a safe fallback/null if
the property is missing) and keep the existing StatusCode(ex.StatusCode ?? 500,
...) behavior.

Comment on lines +1 to +9
namespace OpenRouterWebApi.Models;

public record ChatRequest(
string Model,
string Message,
string? SystemPrompt = null,
float? Temperature = null,
int? MaxTokens = null
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add validation attributes to leverage automatic 400s under [ApiController].

Ensure required fields and sensible ranges; prevents downstream NREs.

+using System.ComponentModel.DataAnnotations;
+
 namespace OpenRouterWebApi.Models;
 
 public record ChatRequest(
-    string Model,
-    string Message,
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message,
     string? SystemPrompt = null,
-    float? Temperature = null,
-    int? MaxTokens = null
+    [Range(0, 2)] float? Temperature = null,   // verify range per provider
+    [Range(1, int.MaxValue)] int? MaxTokens = null
 );
 
 public record ChatResponse(
     string Content,
     string Model,
     string? FinishReason,
     TokenUsage? Usage
 );
 
 public record StreamChatRequest(
-    string Model,
-    string Message,
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message,
     string? SystemPrompt = null
 );
 
 public record ConversationRequest(
-    string Model,
-    List<ConversationMessage> Messages,
+    [Required, MinLength(1)] string Model,
+    [Required] List<ConversationMessage> Messages,
     float? Temperature = null
 );
 
 public record ConversationMessage(
-    string Role,
-    string Content
+    [Required, MinLength(1)] string Role,
+    [Required, MinLength(1)] string Content
 );
 
 public record ArtifactRequest(
-    string Model,
-    string Prompt,
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Prompt,
     string ArtifactType = "code"
 );
 
 public record ArtifactResponse(
     string Text,
     List<ArtifactInfo> Artifacts
 );
 
 public record ArtifactInfo(
-    string Title,
-    string Type,
-    string Content,
+    [Required, MinLength(1)] string Title,
+    [Required, MinLength(1)] string Type,
+    [Required] string Content,
     string? Language = null
 );
 
 public record ToolRequest(
-    string Model,
-    string Message
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message
 );
 
 public record MultimodalRequest(
-    string Model,
-    string Message,
-    List<string> ImageUrls
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message,
+    [Required] List<string> ImageUrls
 );

Please confirm the valid Temperature/MaxTokens ranges for OpenRouter.NET models you target and adjust the annotations accordingly.

Also applies to: 11-16, 24-28, 30-34, 36-39, 41-45, 47-57, 59-62, 64-68


I'll verify the review comment by examining the codebase and researching the OpenRouter API specifications.


🏁 Script executed:

#!/bin/bash
# Find and inspect the Dtos.cs file
fd -t f "Dtos.cs" --exec cat {} \;

Length of output: 1198


🏁 Script executed:

#!/bin/bash
# Check for any existing validation or model binding configuration
rg -n "DataAnnotations|ModelState|ApiController|Validation" --type=cs -B2 -A2

Length of output: 1437


🏁 Script executed:

#!/bin/bash
# Search for OpenRouter API references or configuration
rg -n "OpenRouter|Temperature|MaxTokens" --type=cs -B2 -A2

Length of output: 67142


🌐 Web query:

OpenRouter API Temperature parameter valid range

💡 Result:

The OpenRouter API accepts temperature as a float in the range 0.0 to 2.0 (default 1.0). [1][2]

Sources:
[1] OpenRouter Parameters (docs.openrouter.ai).
[2] OpenRouter Chat Completion / API reference (openrouter.ai docs).


🌐 Web query:

OpenRouter API MaxTokens parameter range limits

💡 Result:

max_tokens must be an integer ≥ 1; the upper limit is the model’s context length minus the prompt length (i.e., you cannot request more output tokens than the model’s remaining context). (openrouter.ai)

Citations:


Add validation attributes to leverage automatic 400s under [ApiController].

Ensure required fields and sensible ranges; prevents downstream NREs.

+using System.ComponentModel.DataAnnotations;
+
 namespace OpenRouterWebApi.Models;
 
 public record ChatRequest(
-    string Model,
-    string Message,
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message,
     string? SystemPrompt = null,
-    float? Temperature = null,
-    int? MaxTokens = null
+    [Range(0, 2)] float? Temperature = null,
+    [Range(1, int.MaxValue)] int? MaxTokens = null
 );
 
 public record ChatResponse(
     string Content,
     string Model,
     string? FinishReason,
     TokenUsage? Usage
 );
 
 public record StreamChatRequest(
-    string Model,
-    string Message,
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message,
     string? SystemPrompt = null
 );
 
 public record ConversationRequest(
-    string Model,
-    List<ConversationMessage> Messages,
+    [Required, MinLength(1)] string Model,
+    [Required] List<ConversationMessage> Messages,
     float? Temperature = null
 );
 
 public record ConversationMessage(
-    string Role,
-    string Content
+    [Required, MinLength(1)] string Role,
+    [Required, MinLength(1)] string Content
 );
 
 public record ArtifactRequest(
-    string Model,
-    string Prompt,
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Prompt,
     string ArtifactType = "code"
 );
 
 public record ArtifactResponse(
     string Text,
     List<ArtifactInfo> Artifacts
 );
 
 public record ArtifactInfo(
-    string Title,
-    string Type,
-    string Content,
+    [Required, MinLength(1)] string Title,
+    [Required, MinLength(1)] string Type,
+    [Required] string Content,
     string? Language = null
 );
 
 public record ToolRequest(
-    string Model,
-    string Message
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message
 );
 
 public record MultimodalRequest(
-    string Model,
-    string Message,
-    List<string> ImageUrls
+    [Required, MinLength(1)] string Model,
+    [Required, MinLength(1)] string Message,
+    [Required] List<string> ImageUrls
 );

Confirmed: OpenRouter API accepts Temperature in range 0–2 and MaxTokens ≥ 1.

Also applies to: 11-16, 24-28, 30-34, 36-39, 41-45, 47-57, 59-62, 64-68

🤖 Prompt for AI Agents
In samples/OpenRouterWebApiSample/Models/Dtos.cs around lines 1 to 9, the
ChatRequest record lacks data-annotation validation attributes so
[ApiController] won't return automatic 400s; add
System.ComponentModel.DataAnnotations attributes: mark Model and Message as
required ([Required]), constrain Temperature to [Range(0, 2)] (nullable allowed)
and MaxTokens to [Range(1, int.MaxValue)] (nullable allowed). Apply the same
pattern to the other DTOs mentioned (lines 11-16, 24-28, 30-34, 36-39, 41-45,
47-57, 59-62, 64-68), importing the DataAnnotations namespace and placing
attributes on the record positional parameters so model binding triggers
automatic validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant