Skip to content

Commit 90c8512

Browse files
fix: harden unavailable/local chat fallback behavior
- Make UnavailableChatService throw ChatBackendUnavailableException for both completion APIs. - Use per-entry MemoryCacheEntryOptions in LocalChatService history cache writes. - Wrap response body-read transport/timeouts as ChatBackendUnavailableException. - Replace project-wide EXTEXP0001 suppression with targeted pragma around RemoveAllResilienceHandlers usage.
1 parent a9bd3ce commit 90c8512

4 files changed

Lines changed: 28 additions & 15 deletions

File tree

EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
<PropertyGroup>
44
<TargetFramework>net10.0</TargetFramework>
5-
<!-- Suppress experimental API warning from RemoveAllResilienceHandlers() used to opt out
6-
the local AI HttpClient from the global standard resilience handler. -->
7-
<NoWarn>$(NoWarn);EXTEXP0001</NoWarn>
85
</PropertyGroup>
96

107
<ItemGroup>

EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public static IServiceCollection AddConfiguredChatServices(this IServiceCollecti
142142
return services;
143143
}
144144

145+
#pragma warning disable EXTEXP0001
145146
services.AddHttpClient(LocalChatHttpClientName, client =>
146147
{
147148
client.BaseAddress = localEndpointUri;
@@ -152,6 +153,7 @@ public static IServiceCollection AddConfiguredChatServices(this IServiceCollecti
152153
// cut off long local-LLM completions. We set HttpClient.Timeout directly instead.
153154
// Retries are also wrong for LLM calls (non-idempotent, partial responses).
154155
.RemoveAllResilienceHandlers();
156+
#pragma warning restore EXTEXP0001
155157
services.AddSingleton<IChatCompletionService, LocalChatService>();
156158
Console.WriteLine("[AI] Selected backend: Local (Ollama/OpenAI-compatible).");
157159
return services;

EssentialCSharp.Chat.Shared/Services/LocalChatService.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Linq;
2+
using System.IO;
23
using System.Net.Http.Headers;
34
using System.Text;
45
using System.Text.Json;
@@ -20,9 +21,6 @@ public partial class LocalChatService : IChatCompletionService, IDisposable
2021
private readonly IHttpClientFactory _HttpClientFactory;
2122
private readonly ILogger<LocalChatService> _Logger;
2223
private readonly MemoryCache _ConversationHistory = new(new MemoryCacheOptions { SizeLimit = MaxConversationEntries });
23-
private static readonly MemoryCacheEntryOptions _HistoryEntryOptions = new MemoryCacheEntryOptions()
24-
.SetSlidingExpiration(_HistoryTtl)
25-
.SetSize(1);
2624

2725
public bool IsAvailable => true;
2826

@@ -76,7 +74,23 @@ public void Dispose()
7674

7775
using (response)
7876
{
79-
var body = await response.Content.ReadAsStringAsync(cancellationToken);
77+
string body;
78+
try
79+
{
80+
body = await response.Content.ReadAsStringAsync(cancellationToken);
81+
}
82+
catch (HttpRequestException ex)
83+
{
84+
throw new ChatBackendUnavailableException("Local AI backend is unavailable while reading response.", ex);
85+
}
86+
catch (IOException ex)
87+
{
88+
throw new ChatBackendUnavailableException("Local AI backend connection closed while reading response.", ex);
89+
}
90+
catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested)
91+
{
92+
throw new ChatBackendUnavailableException("Local AI backend timed out while reading response.", ex);
93+
}
8094

8195
if (!response.IsSuccessStatusCode)
8296
{
@@ -89,7 +103,10 @@ public void Dispose()
89103
var (text, responseId) = ParseResponse(body);
90104
history.Add(new LocalChatMessage("user", prompt));
91105
history.Add(new LocalChatMessage("assistant", text));
92-
_ConversationHistory.Set(responseId, history.TakeLast(MaxConversationMessages).ToList(), _HistoryEntryOptions);
106+
var entryOptions = new MemoryCacheEntryOptions()
107+
.SetSlidingExpiration(_HistoryTtl)
108+
.SetSize(1);
109+
_ConversationHistory.Set(responseId, history.TakeLast(MaxConversationMessages).ToList(), entryOptions);
93110
return (text, responseId);
94111
}
95112
catch (Exception ex) when (ex is JsonException || ex is InvalidOperationException || ex is NotSupportedException)

EssentialCSharp.Chat.Shared/Services/UnavailableChatService.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,10 @@ public class UnavailableChatService : IChatCompletionService
2020
string? endUserId = null,
2121
CancellationToken cancellationToken = default)
2222
{
23-
return Task.FromResult<(string response, string responseId)>(
24-
("Chat service is unavailable in this environment.", Guid.NewGuid().ToString("N")));
23+
throw new ChatBackendUnavailableException("Chat service is unavailable in this environment.");
2524
}
2625

27-
public async IAsyncEnumerable<(string text, string? responseId)> GetChatCompletionStream(
26+
public IAsyncEnumerable<(string text, string? responseId)> GetChatCompletionStream(
2827
string prompt,
2928
string? systemPrompt = null,
3029
string? previousResponseId = null,
@@ -35,10 +34,8 @@ public class UnavailableChatService : IChatCompletionService
3534
#pragma warning restore OPENAI001
3635
bool enableContextualSearch = false,
3736
string? endUserId = null,
38-
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
37+
CancellationToken cancellationToken = default)
3938
{
40-
yield return ("Chat service is unavailable in this environment.", responseId: null);
41-
yield return (string.Empty, Guid.NewGuid().ToString("N"));
42-
await Task.CompletedTask;
39+
throw new ChatBackendUnavailableException("Chat service is unavailable in this environment.");
4340
}
4441
}

0 commit comments

Comments
 (0)