wip
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user