From 00edbd30aea4297708440596d361b9b2aedf99b1 Mon Sep 17 00:00:00 2001 From: William Holmberg Date: Thu, 23 Oct 2025 16:28:37 +0200 Subject: [PATCH] feat: add openrouterwebapi sample --- .../Controllers/ChatController.cs | 392 ++++++++++++++++++ .../Controllers/ModelsController.cs | 102 +++++ samples/OpenRouterWebApiSample/Models/Dtos.cs | 69 +++ .../OpenRouterWebApi.csproj | 14 + .../OpenRouterWebApi.http | 99 +++++ samples/OpenRouterWebApiSample/Program.cs | 38 ++ .../Properties/launchSettings.json | 23 + samples/OpenRouterWebApiSample/README.md | 157 +++++++ .../Tools/CalculatorTools.cs | 44 ++ .../appsettings.Development.json | 8 + .../OpenRouterWebApiSample/appsettings.json | 9 + .../appsettings.json.example | 14 + 12 files changed, 969 insertions(+) create mode 100644 samples/OpenRouterWebApiSample/Controllers/ChatController.cs create mode 100644 samples/OpenRouterWebApiSample/Controllers/ModelsController.cs create mode 100644 samples/OpenRouterWebApiSample/Models/Dtos.cs create mode 100644 samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj create mode 100644 samples/OpenRouterWebApiSample/OpenRouterWebApi.http create mode 100644 samples/OpenRouterWebApiSample/Program.cs create mode 100644 samples/OpenRouterWebApiSample/Properties/launchSettings.json create mode 100644 samples/OpenRouterWebApiSample/README.md create mode 100644 samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs create mode 100644 samples/OpenRouterWebApiSample/appsettings.Development.json create mode 100644 samples/OpenRouterWebApiSample/appsettings.json create mode 100644 samples/OpenRouterWebApiSample/appsettings.json.example diff --git a/samples/OpenRouterWebApiSample/Controllers/ChatController.cs b/samples/OpenRouterWebApiSample/Controllers/ChatController.cs new file mode 100644 index 0000000..321e721 --- /dev/null +++ b/samples/OpenRouterWebApiSample/Controllers/ChatController.cs @@ -0,0 +1,392 @@ +using Microsoft.AspNetCore.Mvc; +using OpenRouter.NET; +using OpenRouter.NET.Models; +using OpenRouter.NET.Tools; +using OpenRouter.NET.Events; +using OpenRouterWebApi.Models; +using System.Text; + +namespace OpenRouterWebApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ChatController : ControllerBase +{ + private readonly OpenRouterClient _client; + private readonly ILogger _logger; + + public ChatController(OpenRouterClient client, ILogger logger) + { + _client = client; + _logger = logger; + } + + [HttpPost("basic")] + public async Task> BasicChat([FromBody] ChatRequest request) + { + try + { + var messages = new List(); + + if (!string.IsNullOrEmpty(request.SystemPrompt)) + { + messages.Add(Message.FromSystem(request.SystemPrompt)); + } + + messages.Add(Message.FromUser(request.Message)); + + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = messages, + Temperature = request.Temperature, + MaxTokens = request.MaxTokens + }; + + var response = await _client.CreateChatCompletionAsync(chatRequest); + + 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 + )); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "OpenRouter API error"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode }); + } + } + + [HttpPost("stream")] + public async Task StreamChat([FromBody] StreamChatRequest request) + { + Response.ContentType = "text/event-stream"; + Response.Headers.Append("Cache-Control", "no-cache"); + Response.Headers.Append("Connection", "keep-alive"); + + try + { + var messages = new List(); + + if (!string.IsNullOrEmpty(request.SystemPrompt)) + { + messages.Add(Message.FromSystem(request.SystemPrompt)); + } + + messages.Add(Message.FromUser(request.Message)); + + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = messages + }; + + await foreach (var chunk in _client.StreamAsync(chatRequest)) + { + if (chunk.TextDelta != null) + { + await Response.WriteAsync($"data: {chunk.TextDelta}\n\n"); + await Response.Body.FlushAsync(); + } + + if (chunk.Completion != null) + { + await Response.WriteAsync($"data: [DONE] {chunk.Completion.FinishReason}\n\n"); + await Response.Body.FlushAsync(); + break; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Streaming error"); + await Response.WriteAsync($"data: [ERROR] {ex.Message}\n\n"); + } + } + + [HttpPost("conversation")] + public async Task> Conversation([FromBody] ConversationRequest request) + { + try + { + var messages = request.Messages.Select(m => new Message + { + Role = m.Role, + Content = m.Content + }).ToList(); + + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = messages, + Temperature = request.Temperature + }; + + var response = await _client.CreateChatCompletionAsync(chatRequest); + + 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 + )); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "OpenRouter API error"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode }); + } + } + + [HttpPost("artifacts")] + public async Task> GenerateWithArtifacts([FromBody] ArtifactRequest request) + { + try + { + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = new List { Message.FromUser(request.Prompt) } + }; + + chatRequest.EnableArtifactSupport(); + + var responseText = new StringBuilder(); + var artifacts = new List(); + + await foreach (var chunk in _client.StreamAsync(chatRequest)) + { + if (chunk.TextDelta != null) + { + responseText.Append(chunk.TextDelta); + } + + if (chunk.Artifact is ArtifactCompleted completed) + { + artifacts.Add(new ArtifactInfo( + Title: completed.Title ?? "Untitled", + Type: completed.Type ?? "unknown", + Content: completed.Content ?? "", + Language: completed.Language + )); + } + } + + return Ok(new ArtifactResponse( + Text: responseText.ToString(), + Artifacts: artifacts + )); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "OpenRouter API error"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode }); + } + } + + [HttpPost("multimodal")] + public async Task> MultimodalChat([FromBody] MultimodalRequest request) + { + try + { + var contentParts = new List + { + new TextContent(request.Message) + }; + + foreach (var imageUrl in request.ImageUrls) + { + contentParts.Add(new ImageContent(imageUrl)); + } + + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = new List + { + Message.FromUser(contentParts) + } + }; + + var response = await _client.CreateChatCompletionAsync(chatRequest); + + 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 + )); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "OpenRouter API error"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode }); + } + } + + [HttpPost("tools")] + public async Task> ChatWithTools([FromBody] ToolRequest request) + { + try + { + var calculator = new OpenRouterWebApi.Tools.CalculatorTools(); + + _client.RegisterTool(calculator, nameof(calculator.Add)); + _client.RegisterTool(calculator, nameof(calculator.Multiply)); + _client.RegisterTool(calculator, nameof(calculator.Subtract)); + _client.RegisterTool(calculator, nameof(calculator.Divide)); + + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = new List { Message.FromUser(request.Message) } + }; + + var responseText = new StringBuilder(); + + await foreach (var chunk in _client.StreamAsync(chatRequest)) + { + if (chunk.ServerTool != null) + { + _logger.LogInformation( + "Tool {ToolName} - State: {State}, Result: {Result}", + chunk.ServerTool.ToolName, + chunk.ServerTool.State, + chunk.ServerTool.Result + ); + } + + if (chunk.TextDelta != null) + { + responseText.Append(chunk.TextDelta); + } + } + + return Ok(responseText.ToString()); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "OpenRouter API error"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode }); + } + } + + [HttpPost("tools/stream")] + public async Task StreamChatWithTools([FromBody] ToolRequest request) + { + Response.ContentType = "text/event-stream"; + Response.Headers.Append("Cache-Control", "no-cache"); + Response.Headers.Append("Connection", "keep-alive"); + + try + { + _logger.LogInformation("Starting tool streaming for model: {Model}, message: {Message}", + request.Model, request.Message); + + var calculator = new OpenRouterWebApi.Tools.CalculatorTools(); + + _client.RegisterTool(calculator, nameof(calculator.Add)); + _client.RegisterTool(calculator, nameof(calculator.Multiply)); + _client.RegisterTool(calculator, nameof(calculator.Subtract)); + _client.RegisterTool(calculator, nameof(calculator.Divide)); + + _logger.LogInformation("Registered 4 calculator tools"); + + // 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(); + } + }; + + var chatRequest = new ChatCompletionRequest + { + Model = request.Model, + Messages = new List { Message.FromUser(request.Message) }, + Stream = true + }; + + _logger.LogInformation("Starting ProcessMessageAsync with event handler..."); + + var (response, history) = await _client.ProcessMessageAsync(chatRequest, maxToolCalls: 5); + + _logger.LogInformation("ProcessMessageAsync completed. History count: {Count}", history.Count); + + var completeEvent = new + { + type = "complete", + content = response.GetContent(), + finishReason = response.GetFinishReason() + }; + + var completeJson = System.Text.Json.JsonSerializer.Serialize(completeEvent); + await Response.WriteAsync($"event: complete\ndata: {completeJson}\n\n"); + await Response.Body.FlushAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "ERROR in streaming with tools: {Message}", ex.Message); + var errorEvent = new + { + type = "error", + message = ex.Message, + stackTrace = ex.StackTrace + }; + var eventJson = System.Text.Json.JsonSerializer.Serialize(errorEvent); + await Response.WriteAsync($"event: error\ndata: {eventJson}\n\n"); + } + } +} + diff --git a/samples/OpenRouterWebApiSample/Controllers/ModelsController.cs b/samples/OpenRouterWebApiSample/Controllers/ModelsController.cs new file mode 100644 index 0000000..55ea2e2 --- /dev/null +++ b/samples/OpenRouterWebApiSample/Controllers/ModelsController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Mvc; +using OpenRouter.NET; + +namespace OpenRouterWebApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ModelsController : ControllerBase +{ + private readonly OpenRouterClient _client; + private readonly ILogger _logger; + + public ModelsController(OpenRouterClient client, ILogger logger) + { + _client = client; + _logger = logger; + } + + [HttpGet] + public async Task GetModels() + { + try + { + var models = await _client.GetModelsAsync(); + + var modelInfos = models.Select(m => new + { + m.Id, + m.Name, + m.ContextLength, + Pricing = new + { + m.Pricing?.Prompt, + m.Pricing?.Completion + } + }).ToList(); + + return Ok(modelInfos); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "Error fetching models"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message }); + } + } + + [HttpGet("popular")] + public async Task GetPopularModels() + { + try + { + var models = await _client.GetModelsAsync(); + + var popularModels = new[] + { + "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)) + .Select(m => new + { + m.Id, + m.Name, + m.ContextLength, + Pricing = new + { + m.Pricing?.Prompt, + m.Pricing?.Completion + } + }) + .ToList(); + + return Ok(filtered); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "Error fetching popular models"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message }); + } + } + + [HttpGet("limits")] + public async Task GetAccountLimits() + { + try + { + var limits = await _client.GetLimitsAsync(); + + return Ok(limits); + } + catch (OpenRouterException ex) + { + _logger.LogError(ex, "Error fetching limits"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message }); + } + } +} + diff --git a/samples/OpenRouterWebApiSample/Models/Dtos.cs b/samples/OpenRouterWebApiSample/Models/Dtos.cs new file mode 100644 index 0000000..144af8e --- /dev/null +++ b/samples/OpenRouterWebApiSample/Models/Dtos.cs @@ -0,0 +1,69 @@ +namespace OpenRouterWebApi.Models; + +public record ChatRequest( + string Model, + string Message, + string? SystemPrompt = null, + float? Temperature = null, + int? MaxTokens = null +); + +public record ChatResponse( + string Content, + string Model, + string? FinishReason, + TokenUsage? Usage +); + +public record TokenUsage( + int? TotalTokens, + int? PromptTokens, + int? CompletionTokens +); + +public record StreamChatRequest( + string Model, + string Message, + string? SystemPrompt = null +); + +public record ConversationRequest( + string Model, + List Messages, + float? Temperature = null +); + +public record ConversationMessage( + string Role, + string Content +); + +public record ArtifactRequest( + string Model, + string Prompt, + string ArtifactType = "code" +); + +public record ArtifactResponse( + string Text, + List Artifacts +); + +public record ArtifactInfo( + string Title, + string Type, + string Content, + string? Language = null +); + +public record ToolRequest( + string Model, + string Message +); + +public record MultimodalRequest( + string Model, + string Message, + List ImageUrls +); + diff --git a/samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj b/samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj new file mode 100644 index 0000000..f64e035 --- /dev/null +++ b/samples/OpenRouterWebApiSample/OpenRouterWebApi.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/samples/OpenRouterWebApiSample/OpenRouterWebApi.http b/samples/OpenRouterWebApiSample/OpenRouterWebApi.http new file mode 100644 index 0000000..7245f92 --- /dev/null +++ b/samples/OpenRouterWebApiSample/OpenRouterWebApi.http @@ -0,0 +1,99 @@ +@OpenRouterWebApi_HostAddress = http://localhost:5210 + +### Get all models +GET {{OpenRouterWebApi_HostAddress}}/api/models +Accept: application/json + +### Get popular models +GET {{OpenRouterWebApi_HostAddress}}/api/models/popular +Accept: application/json + +### Get account limits +GET {{OpenRouterWebApi_HostAddress}}/api/models/limits +Accept: application/json + +### Basic chat +POST {{OpenRouterWebApi_HostAddress}}/api/chat/basic +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "message": "What is the capital of France?", + "systemPrompt": "You are a helpful assistant.", + "temperature": 0.7, + "maxTokens": 500 +} + +### Stream chat +POST {{OpenRouterWebApi_HostAddress}}/api/chat/stream +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "message": "Tell me a short story about a robot.", + "systemPrompt": "You are a creative storyteller." +} + +### Conversation with history +POST {{OpenRouterWebApi_HostAddress}}/api/chat/conversation +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "messages": [ + { + "role": "user", + "content": "What is 2+2?" + }, + { + "role": "assistant", + "content": "2+2 equals 4." + }, + { + "role": "user", + "content": "What about if we multiply that by 3?" + } + ], + "temperature": 0.7 +} + +### Generate with artifacts +POST {{OpenRouterWebApi_HostAddress}}/api/chat/artifacts +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "prompt": "Create a simple Python function to calculate fibonacci numbers" +} + +### Multimodal chat with images +POST {{OpenRouterWebApi_HostAddress}}/api/chat/multimodal +Content-Type: application/json + +{ + "model": "openai/gpt-4o", + "message": "What do you see in this image?", + "imageUrls": [ + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + ] +} + +### Chat with tools (non-streaming) +POST {{OpenRouterWebApi_HostAddress}}/api/chat/tools +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "message": "What is 125 multiplied by 8, then add 50 to that result?" +} + +### Chat with tools (streaming) +POST {{OpenRouterWebApi_HostAddress}}/api/chat/tools/stream +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "message": "Calculate (15 + 25) * 3, then divide by 2" +} + +### diff --git a/samples/OpenRouterWebApiSample/Program.cs b/samples/OpenRouterWebApiSample/Program.cs new file mode 100644 index 0000000..6860651 --- /dev/null +++ b/samples/OpenRouterWebApiSample/Program.cs @@ -0,0 +1,38 @@ +using OpenRouter.NET; + +var builder = WebApplication.CreateBuilder(args); + +var apiKey = builder.Configuration["OpenRouter:ApiKey"] + ?? Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"); + +if (string.IsNullOrEmpty(apiKey)) +{ + throw new InvalidOperationException( + "OpenRouter API key not found. Set OPENROUTER_API_KEY environment variable or configure OpenRouter:ApiKey in appsettings.json"); +} + +builder.Services.AddSingleton(sp => new OpenRouterClient(new OpenRouterClientOptions +{ + ApiKey = apiKey, + SiteUrl = builder.Configuration["OpenRouter:SiteUrl"], + SiteName = builder.Configuration["OpenRouter:SiteName"] ?? "OpenRouter Test API" +})); + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/samples/OpenRouterWebApiSample/Properties/launchSettings.json b/samples/OpenRouterWebApiSample/Properties/launchSettings.json new file mode 100644 index 0000000..69f3d58 --- /dev/null +++ b/samples/OpenRouterWebApiSample/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5210", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7035;http://localhost:5210", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/OpenRouterWebApiSample/README.md b/samples/OpenRouterWebApiSample/README.md new file mode 100644 index 0000000..2af1aa6 --- /dev/null +++ b/samples/OpenRouterWebApiSample/README.md @@ -0,0 +1,157 @@ +# OpenRouter.NET Web API Sample + +A comprehensive ASP.NET Core Web API demonstrating various features of the OpenRouter.NET client library. + +## Features Demonstrated + +This sample showcases the following OpenRouter.NET capabilities: + +- **Basic Chat**: Simple chat completions with temperature and token controls +- **Streaming**: Real-time streaming responses using Server-Sent Events (SSE) +- **Conversation History**: Multi-turn conversations with message history +- **Artifacts**: Generate code/content with artifact support +- **Multimodal**: Process images alongside text prompts +- **Function Calling**: Tool/function calling with calculator example +- **Model Management**: List available models and check account limits + +## Prerequisites + +- .NET 9.0 SDK or later +- OpenRouter API key ([Get one here](https://openrouter.ai/keys)) + +## Setup + +1. Set your OpenRouter API key using one of these methods: + + **Option A: Environment Variable (Recommended)** + ```bash + export OPENROUTER_API_KEY="your-api-key-here" + ``` + + **Option B: Configuration File** + + Create or update `appsettings.json`: + ```json + { + "OpenRouter": { + "ApiKey": "your-api-key-here", + "SiteUrl": "https://your-site.com", + "SiteName": "Your Application Name" + } + } + ``` + +2. Run the application: + ```bash + dotnet run + ``` + +3. The API will be available at `https://localhost:5210` (or the port specified in launchSettings.json) + +## API Endpoints + +### Models + +- `GET /api/models` - List all available models +- `GET /api/models/popular` - List popular models +- `GET /api/models/limits` - Get account usage limits + +### Chat + +- `POST /api/chat/basic` - Simple chat completion +- `POST /api/chat/stream` - Streaming chat completion +- `POST /api/chat/conversation` - Multi-turn conversation +- `POST /api/chat/artifacts` - Generate content with artifacts +- `POST /api/chat/multimodal` - Chat with image inputs +- `POST /api/chat/tools` - Chat with function calling (non-streaming) +- `POST /api/chat/tools/stream` - Chat with function calling (streaming) + +## Testing + +Use the included `OpenRouterWebApi.http` file with VS Code's REST Client extension or any HTTP client to test the endpoints. + +Example basic chat request: +```http +POST https://localhost:5210/api/chat/basic +Content-Type: application/json + +{ + "model": "anthropic/claude-3.5-sonnet", + "message": "What is the capital of France?", + "temperature": 0.7 +} +``` + +## Key Implementation Details + +### Dependency Injection + +The OpenRouterClient is registered as a singleton in `Program.cs`: + +```csharp +builder.Services.AddSingleton(sp => new OpenRouterClient(new OpenRouterClientOptions +{ + ApiKey = apiKey, + SiteUrl = builder.Configuration["OpenRouter:SiteUrl"], + SiteName = builder.Configuration["OpenRouter:SiteName"] +})); +``` + +### Error Handling + +All endpoints include proper error handling with `OpenRouterException`: + +```csharp +catch (OpenRouterException ex) +{ + _logger.LogError(ex, "OpenRouter API error"); + return StatusCode(ex.StatusCode ?? 500, new { error = ex.Message, code = ex.ErrorCode }); +} +``` + +### Streaming Responses + +Streaming endpoints use Server-Sent Events (SSE) format: + +```csharp +Response.ContentType = "text/event-stream"; +Response.Headers.Append("Cache-Control", "no-cache"); +await foreach (var chunk in _client.StreamAsync(chatRequest)) +{ + // Process chunks... +} +``` + +### Function Calling + +The calculator tools demonstrate attribute-based function definition: + +```csharp +[ToolMethod("Add two numbers")] +public int Add( + [ToolParameter("First number")] int a, + [ToolParameter("Second number")] int b) +{ + return a + b; +} +``` + +## Project Structure + +``` +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 +``` + +## Learn More + +- [OpenRouter.NET GitHub Repository](https://github.com/WilliamAvHolmberg/OpenRouter.NET) +- [OpenRouter API Documentation](https://openrouter.ai/docs) diff --git a/samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs b/samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs new file mode 100644 index 0000000..f015cb4 --- /dev/null +++ b/samples/OpenRouterWebApiSample/Tools/CalculatorTools.cs @@ -0,0 +1,44 @@ +using OpenRouter.NET; +using OpenRouter.NET.Models; +using OpenRouter.NET.Tools; + +namespace OpenRouterWebApi.Tools; + +public class CalculatorTools +{ + [ToolMethod("Add two numbers")] + public int Add( + [ToolParameter("First number")] int a, + [ToolParameter("Second number")] int b) + { + return a + b; + } + + [ToolMethod("Multiply two numbers")] + public int Multiply( + [ToolParameter("First number")] int a, + [ToolParameter("Second number")] int b) + { + return a * b; + } + + [ToolMethod("Subtract two numbers")] + public int Subtract( + [ToolParameter("First number")] int a, + [ToolParameter("Second number")] int b) + { + return a - b; + } + + [ToolMethod("Divide two numbers")] + public double Divide( + [ToolParameter("First number")] double a, + [ToolParameter("Second number")] double b) + { + if (b == 0) + throw new ArgumentException("Cannot divide by zero"); + + return a / b; + } +} + diff --git a/samples/OpenRouterWebApiSample/appsettings.Development.json b/samples/OpenRouterWebApiSample/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/samples/OpenRouterWebApiSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/OpenRouterWebApiSample/appsettings.json b/samples/OpenRouterWebApiSample/appsettings.json new file mode 100644 index 0000000..4d56694 --- /dev/null +++ b/samples/OpenRouterWebApiSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OpenRouterWebApiSample/appsettings.json.example b/samples/OpenRouterWebApiSample/appsettings.json.example new file mode 100644 index 0000000..8062d0f --- /dev/null +++ b/samples/OpenRouterWebApiSample/appsettings.json.example @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "OpenRouter": { + "ApiKey": "your-openrouter-api-key-here", + "SiteUrl": "https://your-site.com", + "SiteName": "Your Application Name" + } +}