From 02fe724d4a8263f537ea3c488a5cdf31be74ac6a Mon Sep 17 00:00:00 2001 From: Kasper Marstal Date: Wed, 3 Dec 2025 21:12:00 +0100 Subject: [PATCH] feat: Add OpenRouter provider --- src/Cellm/AddIn/CellmAddIn.cs | 11 ++++-- .../UserInterface/Resources/OpenRouter.svg | 8 +++++ .../UserInterface/Ribbon/RibbonModelGroup.cs | 4 +++ src/Cellm/Cellm.csproj | 2 ++ .../OpenRouter/OpenRouterConfiguration.cs | 35 +++++++++++++++++++ src/Cellm/Models/Providers/Provider.cs | 3 +- .../Models/ServiceCollectionExtensions.cs | 32 +++++++++++++++++ src/Cellm/Users/Entitlement.cs | 1 + src/Cellm/Users/Models/Entitlements.cs | 3 +- src/Cellm/appsettings.json | 8 +++++ 10 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 src/Cellm/AddIn/UserInterface/Resources/OpenRouter.svg create mode 100644 src/Cellm/Models/Providers/OpenRouter/OpenRouterConfiguration.cs diff --git a/src/Cellm/AddIn/CellmAddIn.cs b/src/Cellm/AddIn/CellmAddIn.cs index bb7c380e..859a3832 100644 --- a/src/Cellm/AddIn/CellmAddIn.cs +++ b/src/Cellm/AddIn/CellmAddIn.cs @@ -15,6 +15,7 @@ using Cellm.Models.Providers.Ollama; using Cellm.Models.Providers.OpenAi; using Cellm.Models.Providers.OpenAiCompatible; +using Cellm.Models.Providers.OpenRouter; using Cellm.Models.Resilience; using Cellm.Tools; using Cellm.Tools.FileReader; @@ -83,6 +84,7 @@ private static ServiceCollection ConfigureServices(ServiceCollection services) .Configure(configuration.GetRequiredSection(nameof(OllamaConfiguration))) .Configure(configuration.GetRequiredSection(nameof(OpenAiConfiguration))) .Configure(configuration.GetRequiredSection(nameof(OpenAiCompatibleConfiguration))) + .Configure(configuration.GetRequiredSection(nameof(OpenRouterConfiguration))) .Configure(configuration.GetRequiredSection(nameof(ResilienceConfiguration))) .Configure(configuration.GetRequiredSection(nameof(SentryConfiguration))); @@ -166,7 +168,8 @@ private static ServiceCollection ConfigureServices(ServiceCollection services) .AddResilientHttpClient(resilienceConfiguration, cellmAddInConfiguration, Provider.DeepSeek) .AddResilientHttpClient(resilienceConfiguration, cellmAddInConfiguration, Provider.Gemini) .AddResilientHttpClient(resilienceConfiguration, cellmAddInConfiguration, Provider.Mistral) - .AddResilientHttpClient(resilienceConfiguration, cellmAddInConfiguration, Provider.OpenAiCompatible); + .AddResilientHttpClient(resilienceConfiguration, cellmAddInConfiguration, Provider.OpenAiCompatible) + .AddResilientHttpClient(resilienceConfiguration, cellmAddInConfiguration, Provider.OpenRouter); #pragma warning disable EXTEXP0018 // Type is for evaluation purposes only and is subject to change or removal in future updates. services @@ -185,7 +188,8 @@ private static ServiceCollection ConfigureServices(ServiceCollection services) .AddMistralChatClient() .AddOllamaChatClient() .AddOpenAiChatClient() - .AddOpenAiCompatibleChatClient(); + .AddOpenAiCompatibleChatClient() + .AddOpenRouterChatClient(); // Add tools services @@ -232,7 +236,8 @@ internal static IEnumerable GetProviderConfigurations() Services.GetRequiredService>().CurrentValue, Services.GetRequiredService>().CurrentValue, Services.GetRequiredService>().CurrentValue, - Services.GetRequiredService>().CurrentValue + Services.GetRequiredService>().CurrentValue, + Services.GetRequiredService>().CurrentValue ]; } diff --git a/src/Cellm/AddIn/UserInterface/Resources/OpenRouter.svg b/src/Cellm/AddIn/UserInterface/Resources/OpenRouter.svg new file mode 100644 index 00000000..78ed1c76 --- /dev/null +++ b/src/Cellm/AddIn/UserInterface/Resources/OpenRouter.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Cellm/AddIn/UserInterface/Ribbon/RibbonModelGroup.cs b/src/Cellm/AddIn/UserInterface/Ribbon/RibbonModelGroup.cs index 11160e2d..d1572248 100644 --- a/src/Cellm/AddIn/UserInterface/Ribbon/RibbonModelGroup.cs +++ b/src/Cellm/AddIn/UserInterface/Ribbon/RibbonModelGroup.cs @@ -12,6 +12,7 @@ using Cellm.Models.Providers.Ollama; using Cellm.Models.Providers.OpenAi; using Cellm.Models.Providers.OpenAiCompatible; +using Cellm.Models.Providers.OpenRouter; using Cellm.Users; using ExcelDna.Integration.CustomUI; using Microsoft.Extensions.AI; @@ -494,6 +495,9 @@ public void ShowProviderSettingsForm(IRibbonControl control) case Provider.OpenAiCompatible: currentBaseAddress = GetProviderConfiguration()?.BaseAddress?.ToString() ?? ""; break; + case Provider.OpenRouter: + currentBaseAddress = GetProviderConfiguration()?.BaseAddress?.ToString() ?? ""; + break; default: break; } diff --git a/src/Cellm/Cellm.csproj b/src/Cellm/Cellm.csproj index 6b88cf94..7aeeb012 100644 --- a/src/Cellm/Cellm.csproj +++ b/src/Cellm/Cellm.csproj @@ -28,6 +28,7 @@ + @@ -90,5 +91,6 @@ + diff --git a/src/Cellm/Models/Providers/OpenRouter/OpenRouterConfiguration.cs b/src/Cellm/Models/Providers/OpenRouter/OpenRouterConfiguration.cs new file mode 100644 index 00000000..357078ce --- /dev/null +++ b/src/Cellm/Models/Providers/OpenRouter/OpenRouterConfiguration.cs @@ -0,0 +1,35 @@ +using Cellm.Users; +using Microsoft.Extensions.AI; + +namespace Cellm.Models.Providers.OpenRouter; + +internal class OpenRouterConfiguration : IProviderConfiguration +{ + public Provider Id { get => Provider.OpenRouter; } + + public string Name { get => "OpenRouter"; } + + public Entitlement Entitlement { get => Entitlement.EnableOpenRouterProvider; } + + public string Icon { get => $"AddIn/UserInterface/Resources/{nameof(Provider.OpenRouter)}.svg"; } + + public Uri BaseAddress => new("https://openrouter.ai/api/v1"); + + public string DefaultModel { get; init; } = string.Empty; + + public string ApiKey { get; init; } = string.Empty; + + public string SmallModel { get; init; } = string.Empty; + + public string MediumModel { get; init; } = string.Empty; + + public string LargeModel { get; init; } = string.Empty; + + public AdditionalPropertiesDictionary? AdditionalProperties { get; init; } = []; + + public bool SupportsJsonSchemaResponses { get; init; } = true; + + public bool SupportsStructuredOutputWithTools { get; init; } = true; + + public bool IsEnabled { get; init; } = false; +} diff --git a/src/Cellm/Models/Providers/Provider.cs b/src/Cellm/Models/Providers/Provider.cs index 33124ed4..f6b5f7b4 100644 --- a/src/Cellm/Models/Providers/Provider.cs +++ b/src/Cellm/Models/Providers/Provider.cs @@ -11,5 +11,6 @@ public enum Provider Mistral, Ollama, OpenAi, - OpenAiCompatible + OpenAiCompatible, + OpenRouter } diff --git a/src/Cellm/Models/ServiceCollectionExtensions.cs b/src/Cellm/Models/ServiceCollectionExtensions.cs index e9f061d7..13addf00 100644 --- a/src/Cellm/Models/ServiceCollectionExtensions.cs +++ b/src/Cellm/Models/ServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ using Cellm.Models.Providers.Ollama; using Cellm.Models.Providers.OpenAi; using Cellm.Models.Providers.OpenAiCompatible; +using Cellm.Models.Providers.OpenRouter; using Cellm.Models.Resilience; using Cellm.Users; using Microsoft.Extensions.AI; @@ -426,6 +427,37 @@ public static IServiceCollection AddOpenAiCompatibleChatClient(this IServiceColl return services; } + public static IServiceCollection AddOpenRouterChatClient(this IServiceCollection services) + { + services + .AddKeyedChatClient(Provider.OpenRouter, serviceProvider => + { + var account = serviceProvider.GetRequiredService(); + account.ThrowIfNotEntitled(Entitlement.EnableOpenRouterProvider); + + var openRouterConfiguration = serviceProvider.GetRequiredService>(); + var resilientHttpClient = serviceProvider.GetResilientHttpClient(Provider.OpenRouter); + + if (string.IsNullOrWhiteSpace(openRouterConfiguration.CurrentValue.ApiKey)) + { + throw new CellmException($"Empty {nameof(OpenRouterConfiguration.ApiKey)} for {Provider.OpenRouter}. Please set your API key."); + } + + var openAiClient = new OpenAIClient( + new ApiKeyCredential(openRouterConfiguration.CurrentValue.ApiKey), + new OpenAIClientOptions + { + Transport = new HttpClientPipelineTransport(resilientHttpClient), + Endpoint = openRouterConfiguration.CurrentValue.BaseAddress + }); + + return openAiClient.GetChatClient(openRouterConfiguration.CurrentValue.DefaultModel).AsIChatClient(); + }, ServiceLifetime.Transient) + .UseFunctionInvocation(); + + return services; + } + public static IServiceCollection AddTools(this IServiceCollection services, params Delegate[] tools) { diff --git a/src/Cellm/Users/Entitlement.cs b/src/Cellm/Users/Entitlement.cs index 71773127..173fa722 100644 --- a/src/Cellm/Users/Entitlement.cs +++ b/src/Cellm/Users/Entitlement.cs @@ -14,6 +14,7 @@ public enum Entitlement EnableOpenAiCompatibleProvider, EnableOpenAiCompatibleProviderLocalModels, EnableOpenAiCompatibleProviderHostedModels, + EnableOpenRouterProvider, EnableModelContextProtocol, DisableTelemetry } diff --git a/src/Cellm/Users/Models/Entitlements.cs b/src/Cellm/Users/Models/Entitlements.cs index 85f8530a..3ff22fed 100644 --- a/src/Cellm/Users/Models/Entitlements.cs +++ b/src/Cellm/Users/Models/Entitlements.cs @@ -22,7 +22,8 @@ internal class Entitlements() Entitlement.EnableOllamaProvider, Entitlement.EnableOpenAiProvider, Entitlement.EnableOpenAiCompatibleProvider, - Entitlement.EnableOpenAiCompatibleProviderLocalModels + Entitlement.EnableOpenAiCompatibleProviderLocalModels, + Entitlement.EnableOpenRouterProvider ]; public IEnumerable AsEnumerable() diff --git a/src/Cellm/appsettings.json b/src/Cellm/appsettings.json index 8e0bb5d7..dfdc5dbe 100644 --- a/src/Cellm/appsettings.json +++ b/src/Cellm/appsettings.json @@ -102,6 +102,14 @@ "ApiKey": "", "IsEnabled": true }, + "OpenRouterConfiguration": { + "DefaultModel": "mistralai/mistral-small-3.2-24b-instruct", + "ApiKey": "", + "SmallModel": "mistralai/mistral-small-3.2-24b-instruct", + "MediumModel": "google/gemini-2.5-flash", + "LargeModel": "anthropic/claude-opus-4-5-20251101", + "IsEnabled": true + }, "ModelContextProtocolConfiguration": { "StdioServers": [ {