mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 08:35:33 +08:00
* fix(model): find channels for @-modified model names without the memory cache
With the channel memory cache off (no Redis and MEMORY_CACHE_ENABLED
unset), channel selection matched abilities by the exact requested name,
so a name such as deepseek-flash@effort:max found no channel. Retry with
the routing name when the exact name has no channel, as the cached path
already does.
* feat(deepseek): support V4.1 Flash, the Search and Fetch APIs, and @ reasoning modifiers
Follows the DeepSeek API update of 2026-10-17.
- Default models: deepseek-flash and deepseek-v4-pro, each with -none,
-low and -max, plus deepseek-search and deepseek-fetch. The retired
deepseek-chat, deepseek-reasoner and deepseek-v4-flash names are no
longer listed.
- Thinking: the -none/-low/-max tails also work on deepseek-flash.
@thinking and @effort now reach DeepSeek's own fields on Chat
(thinking, reasoning_effort), Anthropic (thinking, output_config.effort)
and Responses (reasoning.effort), and take precedence over the tails.
- Built-in prices: peak/off-peak expressions for deepseek-flash (also the
deepseek-v4-flash aliases) and deepseek-v4-pro, and $0.75 per 1,000
requests for deepseek-search and deepseek-fetch.
- Server-side search: searches and page fetches DeepSeek runs for Claude
Code and Codex bill at $0.75 per 1,000 under deepseek_search and
deepseek_fetch, whatever model name the client sent. Searches whose
results already arrived stay billed when a stream ends before its final
usage; this also applies to Alibaba's Anthropic endpoint.
- New endpoints POST /v1/tools/search and /v1/tools/fetch relay the
DeepSeek APIs like /v1/alpha/search. A call is charged only when
DeepSeek returns results or page content.
* fix(deepseek): price /v1/tools/search and /v1/tools/fetch by endpoint
A model named in the request body chose the billing model, so a body such
as {"query":"...","model":"deepseek-flash"} was priced as a chat model with
no tokens and the search or fetch cost nothing. Both endpoints now always
use their routing name, deepseek-search or deepseek-fetch. DeepSeek ignores
the body field, so the body is still forwarded unchanged.
* fix(deepseek): bill Claude-format page fetches at the DeepSeek Fetch price
usage.server_tool_use.web_fetch_requests always went to the web_fetch
tool price, which has no built-in value, while only searches used the
DeepSeek key. A channel can now name its own fetch key next to the search
key, and DeepSeek channels bill reported fetches under deepseek_fetch. A
channel retry clears it like the search key.
220 lines
6.8 KiB
Go
220 lines
6.8 KiB
Go
package common
|
|
|
|
import (
|
|
"cmp"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
)
|
|
|
|
// GoogleSearchGroundedPromptTool bills Gemini 2.5-and-older Google Search
|
|
// grounding, charged once per grounded prompt.
|
|
const GoogleSearchGroundedPromptTool = "google_search_grounded_prompt"
|
|
|
|
// reservedBillableToolNames are hosted-tool keys without a built-in price
|
|
// (Anthropic server tools reported in usage.server_tool_use). They and every
|
|
// tool name the built-in price seed prices
|
|
// (operation_setting.IsBuiltInToolPriceKey) bill hosted tools; a client
|
|
// function or tool_use with the same name is never counted under them.
|
|
var reservedBillableToolNames = map[string]struct{}{
|
|
"web_fetch": {},
|
|
"code_execution": {},
|
|
"tool_search": {},
|
|
}
|
|
|
|
// MaxBillableToolCallCount bounds every tool count at settlement, whatever its
|
|
// source, before it becomes a surcharge multiplier (see
|
|
// .agents/rules/billing.md).
|
|
const MaxBillableToolCallCount = 10_000
|
|
|
|
// CountBillableToolCall is the single entry point for per-call tool billing counts.
|
|
// Built-in call types always count; custom function/tool_use names only count when priced.
|
|
func (info *RelayInfo) CountBillableToolCall(itemType string, functionName string) {
|
|
if info == nil {
|
|
return
|
|
}
|
|
info.ensureBuiltInTools()
|
|
|
|
switch itemType {
|
|
case dto.BuildInCallWebSearchCall:
|
|
info.incrementBillableToolCall(cmp.Or(info.WebSearchBillingKey, resolveWebSearchToolName(info.ResponsesUsageInfo.BuiltInTools)))
|
|
case dto.BuildInCallFileSearchCall:
|
|
info.incrementBillableToolCall(dto.BuildInToolFileSearch)
|
|
case dto.BuildInCallFunctionCall, dto.BuildInCallToolUse:
|
|
if functionName == "" {
|
|
return
|
|
}
|
|
if _, reserved := reservedBillableToolNames[functionName]; reserved || operation_setting.IsBuiltInToolPriceKey(functionName) {
|
|
return
|
|
}
|
|
if operation_setting.GetToolPriceForModel(functionName, info.GetBillingModelName()) <= 0 {
|
|
return
|
|
}
|
|
info.incrementBillableToolCall(functionName)
|
|
}
|
|
}
|
|
|
|
// SetBillableToolCount records an upstream-reported count for one tool-price
|
|
// key, replacing whatever per-item counting produced for the same key. Zero
|
|
// is a valid value: a vendor billing field that reports zero suppresses the
|
|
// protocol items counted under that key. Negative counts are ignored.
|
|
func (info *RelayInfo) SetBillableToolCount(name string, count int) {
|
|
if info == nil || name == "" {
|
|
return
|
|
}
|
|
if count < 0 {
|
|
common.SysError(common.LogText("billable tool count ignored: tool=%s count=%d", name, count))
|
|
return
|
|
}
|
|
info.ensureBuiltInTools()
|
|
if existing, ok := info.ResponsesUsageInfo.BuiltInTools[name]; ok && existing != nil {
|
|
existing.CallCount = count
|
|
return
|
|
}
|
|
info.ResponsesUsageInfo.BuiltInTools[name] = &BuildInToolInfo{ToolName: name, CallCount: count}
|
|
}
|
|
|
|
// ApplyVendorToolUsage writes the vendor-billed counts of one upstream
|
|
// response into BuiltInTools. Protocol items counted earlier for the same key
|
|
// are replaced; keys the vendor does not mention are kept.
|
|
func (info *RelayInfo) ApplyVendorToolUsage(raw []byte) {
|
|
if info == nil || info.VendorToolUsage == nil || len(raw) == 0 {
|
|
return
|
|
}
|
|
for name, count := range info.VendorToolUsage(raw) {
|
|
info.SetBillableToolCount(name, count)
|
|
}
|
|
}
|
|
|
|
// RecordRequestInferredToolCall bills one call of key that the request itself
|
|
// proves: a forced hosted search on an upstream whose response reports no
|
|
// search usage. The record belongs to the current channel attempt;
|
|
// InitChannelMeta drops it when the request moves to another channel.
|
|
func (info *RelayInfo) RecordRequestInferredToolCall(key string) {
|
|
if info == nil || key == "" {
|
|
return
|
|
}
|
|
info.SetBillableToolCount(key, 1)
|
|
info.requestInferredToolKey = key
|
|
}
|
|
|
|
func (info *RelayInfo) ensureBuiltInTools() {
|
|
if info.ResponsesUsageInfo == nil {
|
|
info.ResponsesUsageInfo = &ResponsesUsageInfo{}
|
|
}
|
|
if info.ResponsesUsageInfo.BuiltInTools == nil {
|
|
info.ResponsesUsageInfo.BuiltInTools = make(map[string]*BuildInToolInfo)
|
|
}
|
|
}
|
|
|
|
func resolveWebSearchToolName(tools map[string]*BuildInToolInfo) string {
|
|
if _, ok := tools[dto.BuildInToolWebSearchPreview]; ok {
|
|
return dto.BuildInToolWebSearchPreview
|
|
}
|
|
if _, ok := tools[dto.BuildInToolWebSearch]; ok {
|
|
return dto.BuildInToolWebSearch
|
|
}
|
|
return dto.BuildInToolWebSearchPreview
|
|
}
|
|
|
|
func (info *RelayInfo) incrementBillableToolCall(name string) {
|
|
if existing, ok := info.ResponsesUsageInfo.BuiltInTools[name]; ok && existing != nil {
|
|
existing.CallCount++
|
|
return
|
|
}
|
|
info.ResponsesUsageInfo.BuiltInTools[name] = &BuildInToolInfo{
|
|
ToolName: name,
|
|
CallCount: 1,
|
|
}
|
|
}
|
|
|
|
// ImageGenerationCallCounter counts completed Responses image_generation_call
|
|
// outputs with stream-safe identity deduplication.
|
|
type ImageGenerationCallCounter struct {
|
|
seen map[string]struct{}
|
|
count int
|
|
}
|
|
|
|
// Observe records one completed final image output when billable.
|
|
// outputIndex may be nil; when set and nonnegative it participates in dedup.
|
|
func (c *ImageGenerationCallCounter) Observe(item *dto.ResponsesOutput, outputIndex *int) {
|
|
if c == nil || item == nil {
|
|
return
|
|
}
|
|
if item.Type != dto.ResponsesOutputTypeImageGenerationCall {
|
|
return
|
|
}
|
|
if strings.TrimSpace(item.Result) == "" {
|
|
return
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(item.Status)) {
|
|
case "failed", "cancelled", "canceled", "incomplete", "partial":
|
|
return
|
|
}
|
|
|
|
aliases := make([]string, 0, 4)
|
|
if item.ID != "" {
|
|
aliases = append(aliases, "id:"+item.ID)
|
|
}
|
|
if item.CallId != "" {
|
|
aliases = append(aliases, "call:"+item.CallId)
|
|
}
|
|
if outputIndex != nil && *outputIndex >= 0 {
|
|
aliases = append(aliases, fmt.Sprintf("index:%d", *outputIndex))
|
|
}
|
|
sum := sha256.Sum256([]byte(item.Result))
|
|
aliases = append(aliases, "result:"+hex.EncodeToString(sum[:]))
|
|
|
|
if c.seen == nil {
|
|
c.seen = make(map[string]struct{})
|
|
}
|
|
for _, alias := range aliases {
|
|
if _, ok := c.seen[alias]; ok {
|
|
return
|
|
}
|
|
}
|
|
for _, alias := range aliases {
|
|
c.seen[alias] = struct{}{}
|
|
}
|
|
c.count++
|
|
}
|
|
|
|
// Count returns the deduplicated completed image output count before commit capping.
|
|
func (c *ImageGenerationCallCounter) Count() int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
return c.count
|
|
}
|
|
|
|
// Commit writes the capped completed-output count into RelayInfo once.
|
|
// Request tool declarations alone must not become billable calls.
|
|
func (c *ImageGenerationCallCounter) Commit(info *RelayInfo) {
|
|
if info == nil {
|
|
return
|
|
}
|
|
info.ensureBuiltInTools()
|
|
|
|
count := 0
|
|
if c != nil {
|
|
count = c.count
|
|
}
|
|
if count > dto.MaxImageN {
|
|
count = dto.MaxImageN
|
|
}
|
|
|
|
if existing, ok := info.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolImageGeneration]; ok && existing != nil {
|
|
existing.CallCount = count
|
|
return
|
|
}
|
|
info.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolImageGeneration] = &BuildInToolInfo{
|
|
ToolName: dto.BuildInToolImageGeneration,
|
|
CallCount: count,
|
|
}
|
|
}
|