using System; namespace AnotherReplayReader.Utils { /// /// 上下文预算策略:每模型软上限、输出余量、估算安全系数与请求护栏。 /// internal static class AiContextBudget { public const int Tier1MBudget = 160_000; public const int Tier256KBudget = 100_000; public const double EstimatorSafetyFactor = 1.2; public const double HardUsageRatio = 0.9; /// /// 获取模型的一次请求总 token 软上限。0 表示该模型不支持长录像(只能短录像单 slice)。 /// public static int GetContextBudget(AiModel model) { if (model.ContextBudget is { } explicitBudget && explicitBudget > 0) { return explicitBudget; } if (model.ContextLength >= 1_000_000) { return Tier1MBudget; } if (model.ContextLength >= 200_000) { return Tier256KBudget; } return 0; } /// 为输出/推理 tokens 预留的余量。 public static int GetOutputHeadroom(AiProvider provider, AiModel model) { var maxTokens = provider.DefaultMaxTokens; return Math.Max(2 * maxTokens, 32_000); } /// 带安全系数的 token 估算(对中文偏乐观的 bytes/2.2 估算 × 1.2)。 public static int EstimateTokens(string text) => (int)Math.Ceiling(AIAnalyze.EstimateTokenCount(text).EstimatedTokenCount * EstimatorSafetyFactor); /// 请求护栏:只按 prompt 估算检查(保留给旧调用/测试使用)。 public static ContextCheckResult CheckPromptUsage( int estimatedPromptTokens, AiProvider provider, AiModel model) { return CheckUsage(estimatedPromptTokens, provider, model, includeOutputHeadroom: false); } /// 请求护栏:把输出/推理余量也算入总用量。 public static ContextCheckResult CheckRequestUsage( int estimatedPromptTokens, AiProvider provider, AiModel model) { return CheckUsage(estimatedPromptTokens, provider, model, includeOutputHeadroom: true); } private static ContextCheckResult CheckUsage( int estimatedPromptTokens, AiProvider provider, AiModel model, bool includeOutputHeadroom) { var headroom = includeOutputHeadroom ? GetOutputHeadroom(provider, model) : 0; var estimatedTotal = estimatedPromptTokens + headroom; if (model.ContextLength > 0) { var hardLimit = (int)(model.ContextLength * HardUsageRatio); if (estimatedTotal > hardLimit) { return new ContextCheckResult( true, includeOutputHeadroom ? $"估算用量(输入 {estimatedPromptTokens:N0} + 输出余量 {headroom:N0} = {estimatedTotal:N0})超过模型上下文 {model.ContextLength:N0} 的 90%,已拒绝发起请求。请改用更长上下文的模型,或缩短操作记录。" : $"估算输入 {estimatedPromptTokens:N0} token 超过模型上下文 {model.ContextLength:N0} 的 90%,已拒绝发起请求。请改用更长上下文的模型,或缩短操作记录。"); } } var budget = GetContextBudget(model); if (budget > 0 && estimatedTotal > budget) { return new ContextCheckResult( false, includeOutputHeadroom ? $"估算用量(输入 {estimatedPromptTokens:N0} + 输出余量 {headroom:N0} = {estimatedTotal:N0})超过上下文预算 {budget:N0}(可在模型设置中调整)。" : $"估算输入 {estimatedPromptTokens:N0} token 超过上下文预算 {budget:N0}(可在模型设置中调整),长录像将自动分段,超出部分会被压缩。"); } return ContextCheckResult.Ok; } } internal sealed record ContextCheckResult(bool Block, string Message) { public static ContextCheckResult Ok { get; } = new(false, string.Empty); public bool IsOk => !Block && string.IsNullOrEmpty(Message); } }