Files
AnotherReplayReader/Utils/AiSettings.cs
T
2026-09-08 18:39:04 +02:00

409 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace AnotherReplayReader
{
/// <summary>
/// 服务端点配置(如 DeepSeek 官方、NVIDIA NIM
/// </summary>
public class AiProvider
{
public string Name { get; set; } = string.Empty;
public string BaseUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public List<AiModel> Models { get; set; } = [];
public double DefaultTemperature { get; set; } = 0.35;
public double DefaultTopP { get; set; } = 0.95;
public int DefaultMaxTokens { get; set; } = 16384;
}
/// <summary>
/// AI 分析提示词配置。
/// </summary>
public class AiPromptSettings
{
public bool UseCustomSystemPrompt { get; set; }
public string CustomSystemPrompt { get; set; } = string.Empty;
public string AdditionalRules { get; set; } = string.Empty;
}
/// <summary>
/// 模型条目
/// </summary>
public class AiModel
{
public string ModelId { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public bool IsStream { get; set; }
public int ContextLength { get; set; } // 0 表示未知
/// <summary>
/// 一次请求的总 token 软上限(prompt + 输出/推理余量)。
/// null 或 0 表示使用档位默认值:≥1M 上下文 → 160K200K~256K → 100K;更小 → 0(不支持长录像)。
/// </summary>
public int? ContextBudget { get; set; }
public Dictionary<string, object> ExtraParameters { get; set; } = [];
/// <summary>
/// 构建最终请求参数(合并 Provider 默认值、模型特有参数和运行时覆盖)
/// </summary>
public Dictionary<string, object> BuildRequestParams(
AiProvider provider,
double? temperatureOverride = null,
double? topPOverride = null,
int? maxTokensOverride = null)
{
var parameters = new Dictionary<string, object>
{
["model"] = ModelId,
["temperature"] = temperatureOverride ?? provider.DefaultTemperature,
["top_p"] = topPOverride ?? provider.DefaultTopP,
["max_tokens"] = maxTokensOverride ?? provider.DefaultMaxTokens,
["stream"] = IsStream
};
foreach (var kv in this.ExtraParameters)
{
parameters[kv.Key] = kv.Value;
}
return parameters;
}
}
/// <summary>
/// 每次请求前的动态配置上下文(当前选中的 Provider 和 Model
/// </summary>
public record AiRequestContext(AiProvider Provider, AiModel Model)
{
public Dictionary<string, object> BuildRequestParams(
double? temperatureOverride = null,
double? topPOverride = null,
int? maxTokensOverride = null)
{
return Model.BuildRequestParams(Provider,
temperatureOverride, topPOverride, maxTokensOverride);
}
}
/// <summary>
/// 内置已知模型信息(提供商无关,纯模型参数模板)
/// </summary>
public static class KnownModels
{
public const int SimilarityThreshold = 80;
public static int GetSimilarity(string sourceModelId, string targetModelId)
{
// prefer exact match
if (sourceModelId.Equals(targetModelId, StringComparison.OrdinalIgnoreCase))
{
return 100;
}
// match the part after slash, e.g. "deepseek-ai/deepseek-v4-flash" vs "deepseek-v4-flash"
// if last part matches, return 90
var sourceModelIdLastPart = sourceModelId.Split('/').LastOrDefault() ?? sourceModelId;
var targetModelIdLastPart = targetModelId.Split('/').LastOrDefault() ?? targetModelId;
if (sourceModelIdLastPart.Equals(targetModelIdLastPart, StringComparison.OrdinalIgnoreCase))
{
return 90;
}
return 0;
}
/// <summary>
/// 返回一组已知模型,包含正确的 ExtraParameters。
/// 调用方可按需复制到 Provider 的 Models 列表中。
/// </summary>
public static List<AiModel> GetAll()
{
return
[
// DeepSeek 官方
new()
{
ModelId = "deepseek-v4-flash",
DisplayName = "DeepSeek V4 Flash",
IsStream = true,
ContextLength = 1_000_000,
ExtraParameters = new()
{
["thinking"] = new { type = "enabled" },
["reasoning_effort"] = "high"
}
},
new()
{
ModelId = "deepseek-v4-pro",
DisplayName = "DeepSeek V4 Pro",
IsStream = true,
ContextLength = 1_000_000,
ExtraParameters = new()
{
["thinking"] = new { type = "enabled" },
["reasoning_effort"] = "high"
}
},
// NVIDIA NIM 上的 DeepSeek 模型
new()
{
ModelId = "deepseek-ai/deepseek-v4-flash",
DisplayName = "DeepSeek V4 Flash (NIM)",
IsStream = true,
ContextLength = 1_000_000,
ExtraParameters = new()
{
// ["chat_template_kwargs"] = new { thinking = true },
["thinking"] = new { type = "enabled" },
["reasoning_effort"] = "high",
}
},
new()
{
ModelId = "deepseek-ai/deepseek-v4-pro",
DisplayName = "DeepSeek V4 Pro (NIM)",
IsStream = true,
ContextLength = 1_000_000,
ExtraParameters = new()
{
// ["chat_template_kwargs"] = new { thinking = true },
["thinking"] = new { type = "enabled" },
["reasoning_effort"] = "high",
}
},
// NVIDIA Nemotron
new()
{
ModelId = "nvidia/nemotron-3-super-120b-a12b",
DisplayName = "Nemotron Super 120B (NIM)",
IsStream = true,
ContextLength = 1_000_000,
ExtraParameters = new()
{
["reasoning_budget"] = 16384
}
},
// Minimax
new()
{
ModelId = "minimaxai/minimax-m3",
DisplayName = "MiniMax-M3 (NIM)",
IsStream = false,
ContextLength = 1_000_000,
ExtraParameters = []
},
// Kimi
new()
{
ModelId = "moonshotai/kimi-k2.6",
DisplayName = "Kimi-K2.6 (NIM)",
IsStream = false,
ContextLength = 256_000,
ExtraParameters = []
},
// Google DiffusionGemma
new()
{
ModelId = "google/diffusiongemma-26b-a4b-it",
DisplayName = "DiffusionGemma 26B A4B IT (NIM)",
IsStream = false,
ContextLength = 250_000,
ExtraParameters = new()
{
["chat_template_kwargs"] = new { enable_thinking = true },
}
},
// OpenAI 兼容
new()
{
ModelId = "openai/gpt-oss-120b",
DisplayName = "GPT OSS 120B (NIM)",
IsStream = true,
ContextLength = 128_000,
ExtraParameters = new()
{
["reasoning_effort"] = "medium"
}
}
];
}
}
/// <summary>
/// 从 OpenAI 兼容的 /v1/models 端点获取可用模型 ID 列表
/// </summary>
public static class AiModelFetcher
{
public static async Task<List<string>> FetchModelsAsync(
string baseUrl, string apiKey)
{
baseUrl = baseUrl.TrimEnd('/') + "/";
var models = new List<string>();
using var client = new HttpClient();
var request = new HttpRequestMessage(
HttpMethod.Get, new Uri(new(baseUrl), "models"));
request.Headers.Add("Authorization", $"Bearer {apiKey}");
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
throw new Exception(
$"获取模型列表失败: HTTP {(int)response.StatusCode}");
}
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("data", out var dataArray))
{
foreach (var item in dataArray.EnumerateArray())
{
if (item.TryGetProperty("id", out var idProp))
{
var id = idProp.GetString();
if (!string.IsNullOrEmpty(id))
{
models.Add(id!);
}
}
}
}
return models;
}
}
/// <summary>
/// 全局 AI 配置(多个 Provider)的持久化管理
/// </summary>
public class AiSettings
{
public List<AiProvider> Providers { get; set; } = [];
public AiPromptSettings Prompt { get; set; } = new();
/// <summary>
/// 上次选中的 Provider 名称(持久化;用于下次打开设置页时恢复)。
/// 按名称识别,Provider 被重命名/删除后自动回退到第一个 Provider。
/// </summary>
public string? CurrentProviderName { get; set; }
/// <summary>
/// 上次选中的模型 ID(持久化;与 <see cref="CurrentProviderName"/> 配合使用)。
/// 模型被删除后自动回退到该 Provider 的第一个模型。
/// </summary>
public string? CurrentModelId { get; set; }
/// <summary>
/// 更新并持久化当前选中的 Provider 与模型。
/// </summary>
public void SetCurrentSelection(AiProvider provider, AiModel model)
{
CurrentProviderName = provider.Name;
CurrentModelId = model.ModelId;
}
/// <summary>
/// 解析持久化的上次选择。任一标识缺失或对应 Provider/Model 已不存在时返回 null。
/// </summary>
public (AiProvider Provider, AiModel Model)? ResolveLastSelection()
{
var providerName = CurrentProviderName?.Trim();
var modelId = CurrentModelId?.Trim();
if (string.IsNullOrEmpty(providerName) || string.IsNullOrEmpty(modelId))
{
return null;
}
var provider = Providers.FirstOrDefault(p =>
string.Equals(p.Name, providerName, StringComparison.OrdinalIgnoreCase));
if (provider is null)
{
return null;
}
var model = provider.Models.FirstOrDefault(m =>
string.Equals(m.ModelId, modelId, StringComparison.OrdinalIgnoreCase));
if (model is null)
{
return null;
}
return (provider, model);
}
private static readonly string ConfigPath = Path.Combine(
AppContext.BaseDirectory,
"AnotherReplayReader.ai_settings.json");
public static AiSettings Load()
{
try
{
if (File.Exists(ConfigPath))
{
var json = File.ReadAllText(ConfigPath);
var settings = JsonSerializer.Deserialize<AiSettings>(json);
if (settings is { } value && value.Providers.Count > 0)
{
value.Prompt ??= new AiPromptSettings();
return settings;
}
}
}
catch (Exception ex)
{
// 返回默认配置
Debug.Instance.DebugMessage += $"加载 AI 配置失败: {ex}\r\n";
}
// 返回默认配置:包含两个常用 Provider,各附一个内置模型
var defaults = new AiSettings();
var nimProvider = new AiProvider
{
Name = "NVIDIA NIM",
BaseUrl = "https://integrate.api.nvidia.com/v1",
ApiKey = "",
Models =
[
KnownModels.GetAll().First(m => m.ModelId == "deepseek-ai/deepseek-v4-flash")
]
};
var deepseekProvider = new AiProvider
{
Name = "DeepSeek 官方",
BaseUrl = "https://api.deepseek.com",
ApiKey = "",
Models =
[
KnownModels.GetAll().First(m => m.ModelId == "deepseek-v4-flash")
]
};
defaults.Providers.Add(nimProvider);
defaults.Providers.Add(deepseekProvider);
return defaults;
}
public void Save()
{
var dir = Path.GetDirectoryName(ConfigPath);
if (dir is not null)
{
Directory.CreateDirectory(dir);
}
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(ConfigPath, json);
}
}
}