feat: add openrouterwebapi sample#6
Conversation
WalkthroughA 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
Sequence DiagramssequenceDiagram
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
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
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.RequestAbortedtoGetModelsAsync()andGetLimitsAsync()calls. Both methods supportCancellationTokenparameters (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: PassHttpContext.RequestAbortedcancellation token toCreateChatCompletionAsynccalls 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
StreamAsyncandProcessMessageAsyncaccept cancellation tokens and should be updated to passHttpContext.RequestAbortedfor 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
📒 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
launchBrowserdisabled 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.
| 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 | ||
| )); |
There was a problem hiding this comment.
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.
| foreach (var imageUrl in request.ImageUrls) | ||
| { | ||
| contentParts.Add(new ImageContent(imageUrl)); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| _logger.LogInformation( | ||
| "Tool {ToolName} - State: {State}, Result: {Result}", | ||
| chunk.ServerTool.ToolName, | ||
| chunk.ServerTool.State, | ||
| chunk.ServerTool.Result | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| _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.
| // 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(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 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 csLength 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 -80Length 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 -5Length 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 -3Length of output: 5186
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Check DI registration in Program.cs
cat samples/OpenRouterWebApiSample/Program.csLength 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.csLength 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.
| catch (OpenRouterException ex) | ||
| { | ||
| _logger.LogError(ex, "Error fetching models"); | ||
| return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message }); | ||
| } |
There was a problem hiding this comment.
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.
| namespace OpenRouterWebApi.Models; | ||
|
|
||
| public record ChatRequest( | ||
| string Model, | ||
| string Message, | ||
| string? SystemPrompt = null, | ||
| float? Temperature = null, | ||
| int? MaxTokens = null | ||
| ); |
There was a problem hiding this comment.
🧩 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 -A2Length of output: 1437
🏁 Script executed:
#!/bin/bash
# Search for OpenRouter API references or configuration
rg -n "OpenRouter|Temperature|MaxTokens" --type=cs -B2 -A2Length 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.
Summary by CodeRabbit
New Features
Documentation