From 16f3e790ab9d5e0577b6f6ce9ddab41400d3a6c7 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 9 Apr 2026 14:13:03 +0800 Subject: [PATCH 1/4] Update GatewayLlmExecutionService.cs GatewayLlmExecutionService is now registered directly by implementation type, and the container scans all its public constructors. The source code still retains two constructor paths: one using ConfiguredModelProfileRegistry + IModelSelectionPolicy, and the other using LlmProviderRegistry. Currently, both sets of dependencies are registered in IServiceCollection, and PromptCacheCoordinator and PromptCacheWarmRegistry are also registered. As a result, both public constructors meet the resolution criteria, and the ServiceProvider cannot determine which one to choose, so it throws an AmbiguousConstructorException. The root cause is not in Program.cs, but rather that when the old and new construction methods coexist, the old backward-compatible constructor remains exposed to DI. Feasible fix: Make DI see only one constructor. Preferably change the two backward-compatible constructors based on LlmProviderRegistry to private/internal or remove them, keeping only the public constructor based on ConfiguredModelProfileRegistry + IModelSelectionPolicy. --- .../GatewayLlmExecutionService.cs | 68 ------------------- 1 file changed, 68 deletions(-) diff --git a/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs b/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs index 60768ae..d3bccce 100644 --- a/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs +++ b/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs @@ -89,74 +89,6 @@ public GatewayLlmExecutionService( _logger = logger; } - public GatewayLlmExecutionService( - GatewayConfig config, - LlmProviderRegistry registry, - ProviderPolicyService policyService, - RuntimeEventStore eventStore, - RuntimeMetrics runtimeMetrics, - ProviderUsageTracker providerUsage, - ILogger logger) - : this( - config, - registry, - policyService, - eventStore, - runtimeMetrics, - providerUsage, - new PromptCacheCoordinator(config, new PromptCacheTraceWriter(config)), - new PromptCacheWarmRegistry(), - logger) - { - } - - public GatewayLlmExecutionService( - GatewayConfig config, - LlmProviderRegistry registry, - ProviderPolicyService policyService, - RuntimeEventStore eventStore, - RuntimeMetrics runtimeMetrics, - ProviderUsageTracker providerUsage, - PromptCacheCoordinator promptCacheCoordinator, - PromptCacheWarmRegistry promptCacheWarmRegistry, - ILogger logger) - : this( - config, - CreateCompatibilityServices(config, registry), - policyService, - eventStore, - runtimeMetrics, - providerUsage, - promptCacheCoordinator, - promptCacheWarmRegistry, - logger) - { - } - - private GatewayLlmExecutionService( - GatewayConfig config, - CompatibilityServices compatibility, - ProviderPolicyService policyService, - RuntimeEventStore eventStore, - RuntimeMetrics runtimeMetrics, - ProviderUsageTracker providerUsage, - PromptCacheCoordinator promptCacheCoordinator, - PromptCacheWarmRegistry promptCacheWarmRegistry, - ILogger logger) - : this( - config, - compatibility.Registry, - compatibility.SelectionPolicy, - policyService, - eventStore, - runtimeMetrics, - providerUsage, - promptCacheCoordinator, - promptCacheWarmRegistry, - logger) - { - } - public CircuitState DefaultCircuitState { get From 0f04b709b06d23eb9997cd5ef607da14e2d25fb0 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 9 Apr 2026 14:42:10 +0800 Subject: [PATCH 2/4] Update AdminEndpoints.cs Changes: Added an initial handshake frame : connected\n\n uniformly to both SSE endpoints, ensuring the response starts immediately. Extracted the SSE write logic into a private helper method to reduce duplication. Catch and ignore OperationCanceledException when ctx.RequestAborted is triggered, preventing normal client disconnections from being treated as unhandled exceptions. Affected endpoints: /admin/channels/{channelId}/auth/stream /admin/channels/whatsapp/auth/stream --- .../Endpoints/AdminEndpoints.cs | 95 ++++++++++--------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/src/OpenClaw.Gateway/Endpoints/AdminEndpoints.cs b/src/OpenClaw.Gateway/Endpoints/AdminEndpoints.cs index 15ce76b..a24d910 100644 --- a/src/OpenClaw.Gateway/Endpoints/AdminEndpoints.cs +++ b/src/OpenClaw.Gateway/Endpoints/AdminEndpoints.cs @@ -1380,29 +1380,7 @@ operations.ModelProfiles as ConfiguredModelProfileRegistry using var subscription = authEventStore.Subscribe(); var ct = ctx.RequestAborted; - // Send current state as first event - var currentItems = accountId is not null - ? authEventStore.GetLatest(channelId, accountId) is { } currentEvt ? [currentEvt] : [] - : authEventStore.GetAll(channelId); - foreach (var current in currentItems) - { - var json = JsonSerializer.Serialize(MapChannelAuthStatusItem(current), CoreJsonContext.Default.ChannelAuthStatusItem); - await ctx.Response.WriteAsync($"data: {json}\n\n", ct); - await ctx.Response.Body.FlushAsync(ct); - } - - // Stream subsequent events - await foreach (var evt in subscription.Reader.ReadAllAsync(ct)) - { - if (!string.Equals(evt.ChannelId, channelId, StringComparison.Ordinal)) - continue; - if (accountId is not null && !string.Equals(evt.AccountId, accountId, StringComparison.Ordinal)) - continue; - - var evtJson = JsonSerializer.Serialize(MapChannelAuthStatusItem(evt), CoreJsonContext.Default.ChannelAuthStatusItem); - await ctx.Response.WriteAsync($"data: {evtJson}\n\n", ct); - await ctx.Response.Body.FlushAsync(ct); - } + await StreamChannelAuthEventsAsync(ctx, subscription, authEventStore, channelId, accountId, ct); }); app.MapGet("/admin/channels/whatsapp/auth", (HttpContext ctx, string? accountId) => @@ -1439,27 +1417,7 @@ operations.ModelProfiles as ConfiguredModelProfileRegistry using var subscription = authEventStore.Subscribe(); var ct = ctx.RequestAborted; - var currentItems = accountId is not null - ? authEventStore.GetLatest("whatsapp", accountId) is { } currentEvt ? [currentEvt] : [] - : authEventStore.GetAll("whatsapp"); - foreach (var current in currentItems) - { - var json = JsonSerializer.Serialize(MapChannelAuthStatusItem(current), CoreJsonContext.Default.ChannelAuthStatusItem); - await ctx.Response.WriteAsync($"data: {json}\n\n", ct); - await ctx.Response.Body.FlushAsync(ct); - } - - await foreach (var evt in subscription.Reader.ReadAllAsync(ct)) - { - if (!string.Equals(evt.ChannelId, "whatsapp", StringComparison.Ordinal)) - continue; - if (accountId is not null && !string.Equals(evt.AccountId, accountId, StringComparison.Ordinal)) - continue; - - var evtJson = JsonSerializer.Serialize(MapChannelAuthStatusItem(evt), CoreJsonContext.Default.ChannelAuthStatusItem); - await ctx.Response.WriteAsync($"data: {evtJson}\n\n", ct); - await ctx.Response.Body.FlushAsync(ct); - } + await StreamChannelAuthEventsAsync(ctx, subscription, authEventStore, "whatsapp", accountId, ct); }); app.MapGet("/admin/channels/whatsapp/auth/qr.svg", (HttpContext ctx, string? accountId) => @@ -1739,6 +1697,55 @@ private static ChannelAuthStatusItem MapChannelAuthStatusItem(BridgeChannelAuthE UpdatedAtUtc = evt.UpdatedAtUtc }; + private static async Task StreamChannelAuthEventsAsync( + HttpContext ctx, + ChannelAuthEventStore.AuthEventSubscription subscription, + ChannelAuthEventStore authEventStore, + string channelId, + string? accountId, + CancellationToken ct) + { + try + { + await ctx.Response.WriteAsync(": connected\n\n", ct); + await ctx.Response.Body.FlushAsync(ct); + + var currentItems = accountId is not null + ? authEventStore.GetLatest(channelId, accountId) is { } currentEvt ? [currentEvt] : [] + : authEventStore.GetAll(channelId); + foreach (var current in currentItems) + { + await WriteChannelAuthSseEventAsync(ctx, current, ct); + } + + await foreach (var evt in subscription.Reader.ReadAllAsync(ct)) + { + if (!string.Equals(evt.ChannelId, channelId, StringComparison.Ordinal)) + continue; + if (accountId is not null && !string.Equals(evt.AccountId, accountId, StringComparison.Ordinal)) + continue; + + await WriteChannelAuthSseEventAsync(ctx, evt, ct); + } + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + return; + } + } + + private static Task WriteChannelAuthSseEventAsync(HttpContext ctx, BridgeChannelAuthEvent evt, CancellationToken ct) + { + var json = JsonSerializer.Serialize(MapChannelAuthStatusItem(evt), CoreJsonContext.Default.ChannelAuthStatusItem); + return WriteSsePayloadAsync(ctx, $"data: {json}\n\n", ct); + } + + private static async Task WriteSsePayloadAsync(HttpContext ctx, string payload, CancellationToken ct) + { + await ctx.Response.WriteAsync(payload, ct); + await ctx.Response.Body.FlushAsync(ct); + } + private static WhatsAppSetupResponse BuildWhatsAppSetupResponse( GatewayStartupContext startup, GatewayAppRuntime runtime, From c13e8b383530e321e3d9d917bf403761ae1261bc Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 9 Apr 2026 17:27:19 +0800 Subject: [PATCH 3/4] Add model profile support and bump Avalonia Update tests to instantiate ConfiguredModelProfileRegistry and DefaultModelSelectionPolicy and pass them into GatewayLlmExecutionService (reflecting the new model-profile-based constructor), plus related using and variable adjustments across GatewayAdminEndpointTests, GatewayWorkersTests, and ModelProfileSelectionTests. Remove the custom removal of Avalonia DataAnnotations validation from App.axaml.cs (clean up/usings and keep normal initialization). Bump Avalonia package versions to 11.3.13 in the Companion project and add Tmds.DBus.Protocol (0.21.3). These changes adapt tests and the app to the updated execution API and dependency updates. --- src/OpenClaw.Companion/App.axaml.cs | 82 +++++++------------ .../OpenClaw.Companion.csproj | 11 +-- .../GatewayAdminEndpointTests.cs | 28 ++++--- src/OpenClaw.Tests/GatewayWorkersTests.cs | 74 +++++++++++++---- .../ModelProfileSelectionTests.cs | 8 +- 5 files changed, 114 insertions(+), 89 deletions(-) diff --git a/src/OpenClaw.Companion/App.axaml.cs b/src/OpenClaw.Companion/App.axaml.cs index 19636a6..c80a144 100644 --- a/src/OpenClaw.Companion/App.axaml.cs +++ b/src/OpenClaw.Companion/App.axaml.cs @@ -1,59 +1,39 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core; -using Avalonia.Data.Core.Plugins; -using System.Linq; -using Avalonia.Markup.Xaml; -using OpenClaw.Companion.ViewModels; -using OpenClaw.Companion.Views; -using OpenClaw.Companion.Services; - -namespace OpenClaw.Companion; - -public partial class App : Application -{ - private GatewayWebSocketClient? _client; - - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); - - _client = new GatewayWebSocketClient(); - var settings = new SettingsStore(); - desktop.MainWindow = new MainWindow - { - DataContext = new MainWindowViewModel(settings, _client), - }; - - desktop.Exit += async (_, _) => - { - if (_client is not null) - await _client.DisposeAsync(); - }; - } - - base.OnFrameworkInitializationCompleted(); - } +using Avalonia.Markup.Xaml; +using OpenClaw.Companion.ViewModels; +using OpenClaw.Companion.Views; +using OpenClaw.Companion.Services; - private void DisableAvaloniaDataAnnotationValidation() +namespace OpenClaw.Companion; + +public partial class App : Application +{ + private GatewayWebSocketClient? _client; + + public override void Initialize() { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); + AvaloniaXamlLoader.Load(this); + } - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - BindingPlugins.DataValidators.Remove(plugin); + _client = new GatewayWebSocketClient(); + var settings = new SettingsStore(); + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(settings, _client), + }; + + desktop.Exit += async (_, _) => + { + if (_client is not null) + await _client.DisposeAsync(); + }; } + + base.OnFrameworkInitializationCompleted(); } -} +} \ No newline at end of file diff --git a/src/OpenClaw.Companion/OpenClaw.Companion.csproj b/src/OpenClaw.Companion/OpenClaw.Companion.csproj index bc62c82..d33e59e 100644 --- a/src/OpenClaw.Companion/OpenClaw.Companion.csproj +++ b/src/OpenClaw.Companion/OpenClaw.Companion.csproj @@ -18,12 +18,12 @@ - - - - + + + + - + None All @@ -31,5 +31,6 @@ + diff --git a/src/OpenClaw.Tests/GatewayAdminEndpointTests.cs b/src/OpenClaw.Tests/GatewayAdminEndpointTests.cs index aecf0e2..28ab43e 100644 --- a/src/OpenClaw.Tests/GatewayAdminEndpointTests.cs +++ b/src/OpenClaw.Tests/GatewayAdminEndpointTests.cs @@ -1,21 +1,16 @@ -using System.Collections.Concurrent; -using System.Net; -using System.Net.Http.Headers; -using System.Text.RegularExpressions; -using System.Text; -using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using ModelContextProtocol.AspNetCore; using NSubstitute; -using OpenClaw.Client; -using OpenClaw.Companion.Services; -using OpenClaw.Companion.ViewModels; using OpenClaw.Agent; using OpenClaw.Agent.Plugins; using OpenClaw.Channels; +using OpenClaw.Client; +using OpenClaw.Companion.Services; +using OpenClaw.Companion.ViewModels; using OpenClaw.Core.Abstractions; using OpenClaw.Core.Memory; using OpenClaw.Core.Middleware; @@ -27,11 +22,17 @@ using OpenClaw.Core.Sessions; using OpenClaw.Gateway; using OpenClaw.Gateway.Bootstrap; -using ModelContextProtocol.AspNetCore; using OpenClaw.Gateway.Composition; using OpenClaw.Gateway.Endpoints; using OpenClaw.Gateway.Extensions; using OpenClaw.Gateway.Mcp; +using OpenClaw.Gateway.Models; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; using Xunit; namespace OpenClaw.Tests; @@ -1854,6 +1855,8 @@ private static GatewayAppRuntime CreateRuntime( var approvalAuditStore = new ApprovalAuditStore(storagePath, NullLogger.Instance); var runtimeMetrics = new RuntimeMetrics(); var providerUsage = new ProviderUsageTracker(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -1865,11 +1868,12 @@ private static GatewayAppRuntime CreateRuntime( var pluginHealth = new PluginHealthService(storagePath, NullLogger.Instance); var llmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, - providerUsage, + providerUsage, NullLogger.Instance); var retentionCoordinator = Substitute.For(); retentionCoordinator.GetStatusAsync(Arg.Any()) diff --git a/src/OpenClaw.Tests/GatewayWorkersTests.cs b/src/OpenClaw.Tests/GatewayWorkersTests.cs index d997c31..c37cd42 100644 --- a/src/OpenClaw.Tests/GatewayWorkersTests.cs +++ b/src/OpenClaw.Tests/GatewayWorkersTests.cs @@ -1,8 +1,3 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Net; -using System.Text.Json; -using System.Threading.Channels; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -16,6 +11,12 @@ using OpenClaw.Core.Sessions; using OpenClaw.Gateway; using OpenClaw.Gateway.Extensions; +using OpenClaw.Gateway.Models; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Net; +using System.Text.Json; +using System.Threading.Channels; using Xunit; namespace OpenClaw.Tests; @@ -103,7 +104,9 @@ public async Task Start_LoopbackApprovalStillRequiresRequesterMatch() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); - var providerRegistry = new LlmProviderRegistry(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); + var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); var operations = new RuntimeOperationsState @@ -112,7 +115,8 @@ public async Task Start_LoopbackApprovalStillRequiresRequesterMatch() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -217,6 +221,8 @@ public async Task Start_ReusableApprovalGrant_BypassesPendingApprovalCreation() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -237,7 +243,8 @@ public async Task Start_ReusableApprovalGrant_BypassesPendingApprovalCreation() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -350,6 +357,8 @@ public async Task Start_RouteOverrides_AreAppliedToSessionBeforeRuntimeExecution var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -359,7 +368,8 @@ public async Task Start_RouteOverrides_AreAppliedToSessionBeforeRuntimeExecution ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -456,6 +466,8 @@ public async Task Start_StreamingVerboseFooter_IsSentBeforeAssistantDone() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -465,7 +477,8 @@ public async Task Start_StreamingVerboseFooter_IsSentBeforeAssistantDone() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -579,6 +592,8 @@ public async Task Start_ApprovalTimeout_RecordsTimedOutAuditAndRuntimeEvent() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -588,7 +603,8 @@ public async Task Start_ApprovalTimeout_RecordsTimedOutAuditAndRuntimeEvent() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -687,7 +703,10 @@ public async Task Start_ManagedHeartbeatOk_SuppressesDeliveryAndRecordsStatus() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); + var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); var operations = new RuntimeOperationsState @@ -696,7 +715,8 @@ public async Task Start_ManagedHeartbeatOk_SuppressesDeliveryAndRecordsStatus() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -791,6 +811,8 @@ public async Task Start_ManagedHeartbeatAlert_DeliversMessageAndRecordsStatus() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -800,7 +822,8 @@ public async Task Start_ManagedHeartbeatAlert_DeliversMessageAndRecordsStatus() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -901,6 +924,8 @@ public async Task Start_ManagedHeartbeatAlert_DeliveryFailureDoesNotMarkDelivere var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -910,7 +935,8 @@ public async Task Start_ManagedHeartbeatAlert_DeliveryFailureDoesNotMarkDelivere ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -1006,6 +1032,8 @@ public async Task Start_ManagedHeartbeatError_StaysInternalAndRecordsErrorStatus var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -1015,7 +1043,8 @@ public async Task Start_ManagedHeartbeatError_StaysInternalAndRecordsErrorStatus ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -1115,6 +1144,8 @@ public async Task Start_BridgedGroupMessage_UsesGroupIdForSessionReplyAndTyping( var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -1124,7 +1155,8 @@ public async Task Start_BridgedGroupMessage_UsesGroupIdForSessionReplyAndTyping( ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -1241,6 +1273,8 @@ public async Task Start_BridgedTypingCleanup_OnAgentFailure_SendsTypingStop() var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -1250,7 +1284,8 @@ public async Task Start_BridgedTypingCleanup_OnAgentFailure_SendsTypingStop() ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, @@ -1352,6 +1387,8 @@ public async Task Start_BridgedTypingCleanup_OnAgentCancellation_SendsTypingStop var pairingManager = new OpenClaw.Core.Security.PairingManager(storagePath, NullLogger.Instance); var commandProcessor = new ChatCommandProcessor(sessionManager); var runtimeMetrics = new OpenClaw.Core.Observability.RuntimeMetrics(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); var providerPolicies = new ProviderPolicyService(storagePath, NullLogger.Instance); var runtimeEvents = new RuntimeEventStore(storagePath, NullLogger.Instance); @@ -1361,7 +1398,8 @@ public async Task Start_BridgedTypingCleanup_OnAgentCancellation_SendsTypingStop ProviderRegistry = providerRegistry, LlmExecution = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, providerPolicies, runtimeEvents, runtimeMetrics, diff --git a/src/OpenClaw.Tests/ModelProfileSelectionTests.cs b/src/OpenClaw.Tests/ModelProfileSelectionTests.cs index 9a42c58..63c2cd3 100644 --- a/src/OpenClaw.Tests/ModelProfileSelectionTests.cs +++ b/src/OpenClaw.Tests/ModelProfileSelectionTests.cs @@ -414,12 +414,14 @@ public async Task GatewayExecutionService_CompatibilityConstructor_UsesInjectedP Model = "legacy-model" } }; - - var providerRegistry = new LlmProviderRegistry(); + var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); + var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); + var providerRegistry = new LlmProviderRegistry(); providerRegistry.RegisterDefault(config.Llm, new EvaluationChatClient()); var service = new GatewayLlmExecutionService( config, - providerRegistry, + modelProfile, + modelselectionPolicy, new ProviderPolicyService(storagePath, NullLogger.Instance), new RuntimeEventStore(storagePath, NullLogger.Instance), new RuntimeMetrics(), From 4147514342e4992497c978851358451aec418fa4 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 9 Apr 2026 18:50:50 +0800 Subject: [PATCH 4/4] Support LlmProviderRegistry in Gateway constructors Add overloads to GatewayLlmExecutionService that accept an LlmProviderRegistry (and related simple ctor variants) and route through a new private ctor that accepts CompatibilityServices created via CreateCompatibilityServices. This centralizes compatibility service creation and simplifies DI. Update ModelProfileSelectionTests to construct and register a LlmProviderRegistry and pass it to the service (also add a Models config block), replacing the previous direct use of ConfiguredModelProfileRegistry and DefaultModelSelectionPolicy. --- .../GatewayLlmExecutionService.cs | 68 +++++++++++++++++++ .../ModelProfileSelectionTests.cs | 18 +++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs b/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs index d3bccce..c3c46a8 100644 --- a/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs +++ b/src/OpenClaw.Gateway/GatewayLlmExecutionService.cs @@ -42,6 +42,50 @@ private sealed class RouteState private readonly ILogger _logger; private readonly ConcurrentDictionary _routes = new(StringComparer.OrdinalIgnoreCase); + public GatewayLlmExecutionService( + GatewayConfig config, + LlmProviderRegistry providerRegistry, + ProviderPolicyService policyService, + RuntimeEventStore eventStore, + RuntimeMetrics runtimeMetrics, + ProviderUsageTracker providerUsage, + ILogger logger) + : this( + config, + CreateCompatibilityServices(config, providerRegistry), + policyService, + eventStore, + runtimeMetrics, + providerUsage, + new PromptCacheCoordinator(config, new PromptCacheTraceWriter(config)), + new PromptCacheWarmRegistry(), + logger) + { + } + + public GatewayLlmExecutionService( + GatewayConfig config, + LlmProviderRegistry providerRegistry, + ProviderPolicyService policyService, + RuntimeEventStore eventStore, + RuntimeMetrics runtimeMetrics, + ProviderUsageTracker providerUsage, + PromptCacheCoordinator promptCacheCoordinator, + PromptCacheWarmRegistry promptCacheWarmRegistry, + ILogger logger) + : this( + config, + CreateCompatibilityServices(config, providerRegistry), + policyService, + eventStore, + runtimeMetrics, + providerUsage, + promptCacheCoordinator, + promptCacheWarmRegistry, + logger) + { + } + public GatewayLlmExecutionService( GatewayConfig config, ConfiguredModelProfileRegistry modelProfiles, @@ -65,6 +109,30 @@ public GatewayLlmExecutionService( { } + private GatewayLlmExecutionService( + GatewayConfig config, + CompatibilityServices compatibilityServices, + ProviderPolicyService policyService, + RuntimeEventStore eventStore, + RuntimeMetrics runtimeMetrics, + ProviderUsageTracker providerUsage, + PromptCacheCoordinator promptCacheCoordinator, + PromptCacheWarmRegistry promptCacheWarmRegistry, + ILogger logger) + : this( + config, + compatibilityServices.Registry, + compatibilityServices.SelectionPolicy, + policyService, + eventStore, + runtimeMetrics, + providerUsage, + promptCacheCoordinator, + promptCacheWarmRegistry, + logger) + { + } + public GatewayLlmExecutionService( GatewayConfig config, ConfiguredModelProfileRegistry modelProfiles, diff --git a/src/OpenClaw.Tests/ModelProfileSelectionTests.cs b/src/OpenClaw.Tests/ModelProfileSelectionTests.cs index 63c2cd3..f0de021 100644 --- a/src/OpenClaw.Tests/ModelProfileSelectionTests.cs +++ b/src/OpenClaw.Tests/ModelProfileSelectionTests.cs @@ -412,16 +412,26 @@ public async Task GatewayExecutionService_CompatibilityConstructor_UsesInjectedP { Provider = "fake-injected-provider", Model = "legacy-model" + }, + Models = new ModelsConfig + { + DefaultProfile = "default", + Profiles = new List + { + new ModelProfileConfig + { + Id = "default", + Provider = "fake-injected-provider", + Model = "legacy-model" + } + } } }; - var modelProfile = new ConfiguredModelProfileRegistry(config, NullLogger.Instance); - var modelselectionPolicy = new DefaultModelSelectionPolicy(modelProfile); var providerRegistry = new LlmProviderRegistry(); providerRegistry.RegisterDefault(config.Llm, new EvaluationChatClient()); var service = new GatewayLlmExecutionService( config, - modelProfile, - modelselectionPolicy, + providerRegistry, new ProviderPolicyService(storagePath, NullLogger.Instance), new RuntimeEventStore(storagePath, NullLogger.Instance), new RuntimeMetrics(),