Files
AnotherReplayReader/Utils/AiSettings.cs
T
2026-07-06 13:48:26 +02:00

345 lines
12 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.75;
public double DefaultTopP { get; set; } = 0.95;
public int DefaultMaxTokens { get; set; } = 16384;
}
/// <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 表示未知
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; } = [];
// 以下两个不持久化,由 UI 层维护当前选中项
[System.Text.Json.Serialization.JsonIgnore]
public int CurrentProviderIndex { get; set; }
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)
{
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);
}
}
}