Files
AX-Copilot-Codex/src/AxCopilot/Services/TokenEstimator.cs
lacvet a2c952879d 모델 프로파일 기반 Cowork/Code 루프와 진행 UX 고도화 반영
- 등록 모델 실행 프로파일을 검증 게이트, 문서 fallback, post-tool verification까지 확장 적용

- Cowork/Code 진행 카드에 계획/도구/검증/압축/폴백/재시도 단계 메타를 추가해 대기 상태 가시성 강화

- OpenAI/vLLM tool 요청에 병렬 도구 호출 힌트를 추가하고 회귀 프롬프트 문서를 프로파일 기준으로 전면 정리

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-08 13:41:57 +09:00

90 lines
3.5 KiB
C#

namespace AxCopilot.Services;
/// <summary>
/// 모델별 토큰 추정 및 비용 계산 서비스.
/// tiktoken 없이 문자 기반 가중치 추정을 사용합니다.
/// </summary>
public static class TokenEstimator
{
/// <summary>텍스트의 토큰 수를 추정합니다 (CJK/영문 가중치 적용).</summary>
public static int Estimate(string text)
{
if (string.IsNullOrEmpty(text)) return 0;
int cjkChars = 0;
int totalChars = text.Length;
foreach (var c in text)
{
if (c >= 0xAC00 && c <= 0xD7A3) cjkChars++; // 한글
else if (c >= 0x3000 && c <= 0x9FFF) cjkChars++; // CJK
}
double ratio = totalChars > 0 ? (double)cjkChars / totalChars : 0;
// 영문: ~4글자/토큰, 한글: ~2글자/토큰
double charsPerToken = 4.0 - ratio * 2.0;
return Math.Max(1, (int)(totalChars / charsPerToken));
}
/// <summary>메시지 리스트의 총 토큰 수를 추정합니다.</summary>
public static int EstimateMessages(IEnumerable<Models.ChatMessage> messages)
{
int total = 0;
foreach (var m in messages)
total += Estimate(m.Content) + 4; // 메시지 오버헤드
return total;
}
/// <summary>비용을 추정합니다 (USD 기준).</summary>
public static (double InputCost, double OutputCost) EstimateCost(
int promptTokens, int completionTokens, string service, string model)
{
var (inputPrice, outputPrice) = GetPricing(service, model);
return (promptTokens / 1_000_000.0 * inputPrice,
completionTokens / 1_000_000.0 * outputPrice);
}
/// <summary>모델별 1M 토큰 가격 (USD). (inputPrice, outputPrice)</summary>
public static (double Input, double Output) GetPricing(string service, string model)
{
var key = $"{service}:{model}".ToLowerInvariant();
// 2026년 기준 가격표
return key switch
{
_ when key.Contains(string.Concat("cl", "aude-opus")) => (15.0, 75.0),
_ when key.Contains(string.Concat("cl", "aude-sonnet")) => (3.0, 15.0),
_ when key.Contains(string.Concat("cl", "aude-haiku")) => (0.25, 1.25),
_ when key.Contains("gemini-2.5-pro") => (1.25, 10.0),
_ when key.Contains("gemini-2.5-flash") => (0.15, 0.6),
_ when key.Contains("gemini-2.0") => (0.1, 0.4),
_ when key.Contains("ollama") => (0, 0), // 로컬
_ when key.Contains("vllm") => (0, 0), // 로컬
_ => (1.0, 3.0), // 기본 추정
};
}
/// <summary>토큰 수를 읽기 쉬운 문자열로 포맷합니다. 이진 단위(1K=1024) 사용.</summary>
public static string Format(int count) => count switch
{
>= 1_048_576 => $"{count / 1_048_576.0:0.#}M", // 1024*1024
>= 1_024 => $"{count / 1_024.0:0.#}K", // 1024
_ => count.ToString(),
};
/// <summary>비용을 읽기 쉬운 문자열로 포맷합니다.</summary>
public static string FormatCost(double usd) => usd switch
{
0 => "무료",
< 0.01 => $"${usd:F4}",
< 1.0 => $"${usd:F3}",
_ => $"${usd:F2}",
};
/// <summary>컨텍스트 사용률을 계산합니다 (0.0~1.0).</summary>
public static double GetContextUsage(int currentTokens, int maxContextTokens)
{
if (maxContextTokens <= 0) return 0;
return Math.Min(1.0, (double)currentTokens / maxContextTokens);
}
}