feat: 添加 Token 费用估算与自定义忽略规则

- 在 LLM 客户端中捕获流式响应的 Token 使用量
- 内置多模型官方定价表,支持自定义输入/输出单价覆盖
- 新增 `--price-input` 和 `--price-output` 配置项
- 支持通过 `.airignore` 文件自定义排除评审文件
- 更新中英日文文档,补充费用估算与忽略规则说明
This commit is contained in:
unclesam committed 2026-08-21 15:00:21 +08:00
1 parent 02e2e4edd3
commit 26b0342934
12 files changed
+658 -63

No files matched your search

+150 -14
View File
@@ -12,16 +12,20 @@ import (
// Config 大模型统一配置
type Config struct {
Provider string // "gemini", "deepseek", "openai", "ollama", "custom"
APIKey string // 密钥 (Ollama 本地可填任意字符或留空)
BaseURL string // 自定义接口地址 (非必填,会自动根据 Provider 设置默认值)
Model string // 模型名称 (非必填,会自动设置默认主力模型)
Provider string // "gemini", "deepseek", "openai", "ollama", "custom"
APIKey string // 密钥 (Ollama 本地可填任意字符或留空)
BaseURL string // 自定义接口地址 (非必填,会自动根据 Provider 设置默认值)
Model string // 模型名称 (非必填,会自动设置默认主力模型)
PriceInput float64 // 用户自定义每 1M 输入 Token 美元价格 (可选,覆盖内置价格表)
PriceOutput float64 // 用户自定义每 1M 输出 Token 美元价格 (可选,覆盖内置价格表)
}
// Client 统一大模型客户端
type Client struct {
client *openai.Client
model string
client *openai.Client
model string
priceInput float64
priceOutput float64
}
// NewClient 初始化客户端
@@ -227,8 +231,10 @@ func NewClient(cfg Config) (*Client, error) {
clientConfig.BaseURL = baseURL
return &Client{
client: openai.NewClientWithConfig(clientConfig),
model: model,
client: openai.NewClientWithConfig(clientConfig),
model: model,
priceInput: cfg.PriceInput,
priceOutput: cfg.PriceOutput,
}, nil
}
@@ -254,8 +260,122 @@ func (c *Client) Review(ctx context.Context, systemPrompt, diff string) (string,
return resp.Choices[0].Message.Content, nil
}
// ReviewStream 流式逐字返回评审结果(打字机效果)
func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, onChunk func(chunk string)) error {
// Usage 记录一次调用的 Token 消耗
type Usage struct {
InputTokens int
OutputTokens int
Model string
Provider string
CustomPriceInput float64 // 用户自定义输入单价(每 1M tokens 美元,>0 优先使用)
CustomPriceOutput float64 // 用户自定义输出单价(每 1M tokens 美元,>0 优先使用)
}
// reviewPriceTable 各模型每 1M token 的美元单价(输入/输出)
// 数据来源:各厂商官方定价页面,更新于 2026-08
// 如实际价格有变动,可在 config.yaml 中通过 price_input/price_output 字段自定义覆盖
var reviewPriceTable = map[string][2]float64{
// ── Google Gemini ─────────────────────────────────────────────
// 3.x 系列(当前主力)
"gemini-3.7-flash": {0.75, 3.75}, // 推介价至 2026-12-31,之后 1.50/7.50
"gemini-3.6-flash": {1.50, 7.50},
"gemini-3.5-flash": {1.50, 9.00},
"gemini-3.5-flash-lite": {0.30, 2.50},
"gemini-3.1-flash-lite": {0.25, 1.50},
"gemini-3.1-pro": {2.00, 12.00},
// 2.5 系列(2026-10 退役)
"gemini-2.5-pro": {1.25, 10.00},
"gemini-2.5-flash": {0.30, 2.50},
"gemini-2.5-flash-lite": {0.10, 0.40},
// 旧款(兼容)
"gemini-2.0-flash": {0.10, 0.40},
// ── DeepSeek ─────────────────────────────────────────────────
// 注意:DeepSeek 已启用峰谷定价,以下为非高峰标准价
"deepseek-v4-flash": {0.22, 0.66},
"deepseek-chat": {0.22, 0.66}, // V4-Flash 别名
"deepseek-reasoner": {0.55, 2.19},
// ── Anthropic Claude ──────────────────────────────────────────
"claude-opus-5": {5.00, 25.00},
"claude-sonnet-5": {2.00, 10.00}, // 推介价至 2026-08-31
"anthropic/claude-sonnet-5": {2.00, 10.00},
"claude-haiku-4-5": {1.00, 5.00},
"anthropic/claude-haiku-4-5": {1.00, 5.00},
// 旧款(兼容)
"anthropic/claude-3.7-sonnet": {3.00, 15.00},
"anthropic/claude-3.5-sonnet": {3.00, 15.00},
"anthropic/claude-3.5-haiku": {0.80, 4.00},
// ── xAI Grok ─────────────────────────────────────────────────
"grok-4.6": {2.00, 6.00}, // >200k tokens 翻倍
"grok-4.3": {1.25, 2.50},
"grok-2-latest": {2.00, 6.00}, // 旧款映射
// ── OpenAI ───────────────────────────────────────────────────
"gpt-5.6-sol": {5.00, 30.00},
"gpt-5.6-terra": {2.00, 12.00},
"gpt-5.6-luna": {0.20, 1.20},
"gpt-4o": {2.50, 10.00}, // 旧款保留
"gpt-4o-mini": {0.15, 0.60},
"o3-mini": {1.10, 4.40},
// ── SiliconFlow(聚合平台,按 DeepSeek V4 主力估算)────────────
"deepseek-ai/DeepSeek-V3": {0.22, 0.66},
"deepseek-ai/DeepSeek-R1": {0.55, 2.19},
// ── Qwen / 阿里通义千问 ───────────────────────────────────────
"qwen-3.8-max": {2.00, 6.00},
"qwen-max": {2.00, 6.00},
"qwen-plus": {0.40, 1.20},
"qwen-3.7-flash": {0.03, 0.13},
// ── Kimi / Moonshot AI ────────────────────────────────────────
"kimi-k3": {3.00, 15.00},
"moonshot-v1-8k": {0.95, 4.00},
"moonshot-v1-32k": {1.90, 8.00},
"moonshot-v1-128k": {3.50, 15.00},
// ── 智谱 GLM ─────────────────────────────────────────────────
"glm-5.3": {1.40, 4.40},
"glm-5.2": {1.40, 4.40},
"glm-4-plus": {1.40, 4.40},
// ── Doubao / 字节跳动 ─────────────────────────────────────────
"doubao-seed-2.1-pro": {0.88, 4.42},
"doubao-pro-32k": {0.88, 4.42},
// ── Groq(超快推理,按量计费)────────────────────────────────
"llama-3.3-70b-versatile": {0.59, 0.79},
"llama-3.1-8b-instant": {0.05, 0.08},
// ── 本地模型(完全免费)──────────────────────────────────────
"ollama": {0, 0},
"local-model": {0, 0},
}
// EstimateCost 估算费用(美元),返回 -1 表示无定价数据
func (u *Usage) EstimateCost() float64 {
// 1. 优先使用用户在 config.yaml 中配置的自定义单价(每 1M tokens 美元)
if u.CustomPriceInput > 0 || u.CustomPriceOutput > 0 {
inputCost := float64(u.InputTokens) / 1_000_000 * u.CustomPriceInput
outputCost := float64(u.OutputTokens) / 1_000_000 * u.CustomPriceOutput
return inputCost + outputCost
}
// 2. 回退使用内置官方模型价格表
prices, ok := reviewPriceTable[u.Model]
if !ok {
return -1
}
// Ollama / 本地模型 price = 0,直接返回 0
inputCost := float64(u.InputTokens) / 1_000_000 * prices[0]
outputCost := float64(u.OutputTokens) / 1_000_000 * prices[1]
return inputCost + outputCost
}
// ReviewStream 流式逐字返回评审结果(打字机效果),同时返回 Token 使用量
func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, onChunk func(chunk string)) (*Usage, error) {
stream, err := c.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: c.model,
Messages: []openai.ChatCompletionMessage{
@@ -264,18 +384,27 @@ func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, on
},
Temperature: 0.2,
Stream: true,
StreamOptions: &openai.StreamOptions{
IncludeUsage: true,
},
})
if err != nil {
return fmt.Errorf("创建流式请求失败: %w", err)
return nil, fmt.Errorf("创建流式请求失败: %w", err)
}
if stream != nil {
defer stream.Close()
}
usage := &Usage{
Model: c.model,
CustomPriceInput: c.priceInput,
CustomPriceOutput: c.priceOutput,
}
for {
select {
case <-ctx.Done():
return ctx.Err()
return nil, ctx.Err()
default:
}
@@ -284,8 +413,15 @@ func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, on
break
}
if err != nil {
return fmt.Errorf("流式读取异常: %w", err)
return nil, fmt.Errorf("流式读取异常: %w", err)
}
// 捕获最后一个 chunk 中携带的 Usage 信息
if response.Usage.TotalTokens > 0 {
usage.InputTokens = response.Usage.PromptTokens
usage.OutputTokens = response.Usage.CompletionTokens
}
if len(response.Choices) > 0 {
chunk := response.Choices[0].Delta.Content
if chunk != "" {
@@ -294,5 +430,5 @@ func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, on
}
}
return nil
return usage, nil
}