Skip to content

Commit b9d5807

Browse files
committed
Address review feedback: duplicate-row guards, repair persistence, Anthropic /v1 edge, resume legacy fallback, cancellation propagation, endpoint validation
1 parent 5f216b9 commit b9d5807

6 files changed

Lines changed: 56 additions & 16 deletions

File tree

TelegramSearchBot.LLM/Service/AI/LLM/AnthropicService.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ private AnthropicClient CreateClient(LLMChannel channel, LLMApiBinding? binding
101101
if (!string.IsNullOrWhiteSpace(endpoint)) {
102102
// Binding URL 已含 /v1(如 https://opencode.ai/zen/v1),SDK 会再追加 /v1/messages;
103103
// 剥离尾部 /v1 使 SDK 追加后命中精确 binding 路径。legacy channel.Gateway 保持字节一致。
104-
if (binding != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase)) {
105-
endpoint = endpoint.Substring(0, endpoint.Length - 3);
104+
var trimmed = endpoint.TrimEnd('/');
105+
if (binding != null && trimmed.EndsWith("/v1", StringComparison.OrdinalIgnoreCase)) {
106+
trimmed = trimmed.Substring(0, trimmed.Length - 3);
106107
}
107-
options.BaseUrl = endpoint.TrimEnd('/');
108+
options.BaseUrl = trimmed;
108109
}
109110
return new AnthropicClient(options);
110111
}

TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,13 @@ public async IAsyncEnumerable<string> ResumeFromSnapshotAsync(
147147
.ToListAsync();
148148
var route = LlmRouteResolver.Resolve(channel, snapshot.ModelName, modelRows, _logger);
149149
if (route == null) {
150-
_logger.LogError("Cannot resume: model {Model} has no route on channel {ChannelId}", snapshot.ModelName, channel.Id);
150+
// 六.8 legacy 回退:模型行缺失/软删时按渠道 Provider/Gateway 继续,不丢弃排队中的续聊
151+
_logger.LogWarning("Cannot resume: model {Model} has no route on channel {ChannelId}, falling back to legacy provider route", snapshot.ModelName, channel.Id);
152+
var legacyService = _LLMFactory.GetLLMService(channel.Provider);
153+
await foreach (var item in legacyService.ResumeFromSnapshotAsync(snapshot, channel, null, executionContext, cancellationToken)
154+
.WithCancellation(cancellationToken)) {
155+
yield return item;
156+
}
151157
yield break;
152158
}
153159

@@ -223,7 +229,7 @@ orderby s.Priority descending
223229
}
224230

225231
// 6. 按解析路由执行
226-
await foreach (var e in operation(service, channel, route.Binding, new CancellationToken())) {
232+
await foreach (var e in operation(service, channel, route.Binding, cancellationToken)) {
227233
yield return e;
228234
}
229235
yield break;

TelegramSearchBot.LLM/Service/AI/LLM/OllamaService.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,14 @@ public async Task<float[]> GenerateEmbeddingsAsync(string text, string modelName
446446
modelName = "bge-m3";
447447
}
448448

449+
var endpoint = LlmBindingSupport.ResolveEndpoint(channel, binding);
450+
if (channel == null || string.IsNullOrWhiteSpace(endpoint)) {
451+
_logger.LogError("{ServiceName}: Channel or Gateway is not configured.", ServiceName);
452+
throw new InvalidOperationException($"Error: {ServiceName} channel/gateway is not configured.");
453+
}
454+
449455
var httpClient = _httpClientFactory?.CreateClient() ?? new HttpClient();
450-
httpClient.BaseAddress = new Uri(LlmBindingSupport.ResolveEndpoint(channel, binding));
456+
httpClient.BaseAddress = new Uri(endpoint);
451457
var ollama = new OllamaApiClient(httpClient, modelName);
452458

453459
if (!await CheckAndPullModelAsync(ollama, modelName)) {
@@ -479,8 +485,14 @@ public async Task<string> AnalyzeImageAsync(string photoPath, string modelName,
479485

480486
prompt = string.IsNullOrWhiteSpace(prompt) ? GeneralLLMService.DefaultAltPhotoPrompt : prompt;
481487

488+
var endpoint = LlmBindingSupport.ResolveEndpoint(channel, binding);
489+
if (channel == null || string.IsNullOrWhiteSpace(endpoint)) {
490+
_logger.LogError("{ServiceName}: Channel or Gateway is not configured.", ServiceName);
491+
return $"Error: {ServiceName} channel/gateway is not configured.";
492+
}
493+
482494
var httpClient = _httpClientFactory?.CreateClient() ?? new HttpClient();
483-
httpClient.BaseAddress = new Uri(LlmBindingSupport.ResolveEndpoint(channel, binding));
495+
httpClient.BaseAddress = new Uri(endpoint);
484496
var ollama = new OllamaApiClient(httpClient, modelName);
485497
ollama.SelectedModel = modelName;
486498
var chat = new Chat(ollama);

TelegramSearchBot.LLM/Service/AI/LLM/OpenAIResponsesService.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -753,13 +753,20 @@ public async Task<float[]> GenerateEmbeddingsAsync(string text, string modelName
753753
}
754754

755755
public async Task<float[]> GenerateEmbeddingsAsync(string text, string modelName, LLMChannel channel, LLMApiBinding binding) {
756+
var endpoint = LlmBindingSupport.ResolveEndpoint(channel, binding);
757+
var apiKey = LlmBindingSupport.ResolveApiKey(channel, binding);
758+
if (channel == null || string.IsNullOrWhiteSpace(endpoint) || (binding?.AuthProfile != LlmAuthProfile.None && string.IsNullOrWhiteSpace(apiKey))) {
759+
_logger.LogError("{ServiceName}: Channel, Gateway, or ApiKey is not configured.", ServiceName);
760+
throw new InvalidOperationException($"Error: {ServiceName} channel/gateway/apikey is not configured.");
761+
}
762+
756763
using var httpClient = _httpClientFactory.CreateClient();
757764
var clientOptions = new OpenAIClientOptions {
758-
Endpoint = new Uri(LlmBindingSupport.ResolveEndpoint(channel, binding)),
765+
Endpoint = new Uri(endpoint),
759766
Transport = new HttpClientPipelineTransport(httpClient),
760767
};
761-
var apiKey = new ApiKeyCredential(LlmBindingSupport.ResolveApiKey(channel, binding));
762-
OpenAIClient client = new(apiKey, clientOptions);
768+
var credential = new ApiKeyCredential(apiKey);
769+
OpenAIClient client = new(credential, clientOptions);
763770

764771
try {
765772
var embeddingClient = client.GetEmbeddingClient(modelName);

TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,16 +1997,21 @@ public async Task<float[]> GenerateEmbeddingsAsync(string text, string modelName
19971997
}
19981998

19991999
public async Task<float[]> GenerateEmbeddingsAsync(string text, string modelName, LLMChannel channel, LLMApiBinding binding) {
2000-
2000+
var endpoint = LlmBindingSupport.ResolveEndpoint(channel, binding);
2001+
var apiKey = LlmBindingSupport.ResolveApiKey(channel, binding);
2002+
if (channel == null || string.IsNullOrWhiteSpace(endpoint) || (binding?.AuthProfile != LlmAuthProfile.None && string.IsNullOrWhiteSpace(apiKey))) {
2003+
_logger.LogError("{ServiceName}: Channel, Gateway, or ApiKey is not configured.", ServiceName);
2004+
throw new InvalidOperationException($"Error: {ServiceName} channel/gateway/apikey is not configured.");
2005+
}
20012006

20022007
using var httpClient = _httpClientFactory.CreateClient();
20032008

20042009
var clientOptions = new OpenAIClientOptions {
2005-
Endpoint = new Uri(NormalizeOpenAIEndpoint(channel, LlmBindingSupport.ResolveEndpoint(channel, binding))),
2010+
Endpoint = new Uri(NormalizeOpenAIEndpoint(channel, endpoint)),
20062011
Transport = new HttpClientPipelineTransport(httpClient),
20072012
};
20082013

2009-
var apikey = new ApiKeyCredential(LlmBindingSupport.ResolveApiKey(channel, binding));
2014+
var apikey = new ApiKeyCredential(apiKey);
20102015
OpenAIClient client = new(apikey, clientOptions);
20112016

20122017
try {

TelegramSearchBot/Service/Manage/EditLLMConfHelper.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,13 @@ public async Task<int> RefreshAllChannel() {
161161
_logger.LogInformation("通道 {ChannelName} 标记删除消失的模型 {ModelName}", channel.Name, record.ModelName);
162162
}
163163

164-
// 添加全新的模型(同 binding 内忽略大小写去重,blueprint §七.6)
164+
// 添加全新的模型(同 binding 内忽略大小写去重,blueprint §七.6);
165+
// 去重覆盖同 binding 的全部行(任意 AuthorizationSource,含 Manual);
166+
// null-FK 旧库行按 §六.8 也解释为默认 binding 路由。
167+
// 防止手工行与刷新插入的 Discovered 行重复。
165168
var toAdd = modelSet
166-
.Where(m => !scopedRecords.Any(r => r.ModelName.Equals(m, StringComparison.OrdinalIgnoreCase)))
169+
.Where(m => !existingRecords.Any(r => (r.ApiBindingId ?? defaultBinding?.Id) == defaultBinding?.Id
170+
&& r.ModelName.Equals(m, StringComparison.OrdinalIgnoreCase)))
167171
.Select(m => new ChannelWithModel {
168172
LLMChannelId = channel.Id,
169173
ModelName = m,
@@ -323,6 +327,9 @@ public async Task<bool> AddModelWithChannel(int channelId, List<string> modelNam
323327
return false;
324328
}
325329

330+
// 忽略大小写去重,批内同名(如 "gpt-4o"+"GPT-4O")只建一行(blueprint §七.6)
331+
modelNames = modelNames.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
332+
326333
// 管理员手工添加 = Manual 授权(blueprint §四.5);新行关联默认 binding
327334
var defaultBinding = await DataContext.LLMApiBindings
328335
.Where(b => b.LLMChannelId == channelId && b.IsDefault)
@@ -523,6 +530,7 @@ private static string FormatModelDisplay(ChannelWithModel row) {
523530
foreach (var other in defaults.Skip(1)) {
524531
other.IsDefault = false;
525532
}
533+
await DataContext.SaveChangesAsync();
526534
}
527535
if (defaults.Count >= 1) {
528536
return defaults[0];
@@ -643,7 +651,8 @@ internal static bool IsOpenCodeBinding(LLMApiBinding? binding) {
643651
return binding != null
644652
&& Uri.TryCreate(binding.Endpoint, UriKind.Absolute, out var uri)
645653
&& string.Equals(uri.Host, "opencode.ai", StringComparison.OrdinalIgnoreCase)
646-
&& uri.AbsolutePath.StartsWith("/zen/", StringComparison.OrdinalIgnoreCase);
654+
&& (string.Equals(uri.AbsolutePath, "/zen", StringComparison.OrdinalIgnoreCase)
655+
|| uri.AbsolutePath.StartsWith("/zen/", StringComparison.OrdinalIgnoreCase));
647656
}
648657

649658
/// <summary>

0 commit comments

Comments
 (0)