feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+101 -30
View File
@@ -14,6 +14,7 @@ import (
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relaykit/dto"
kitreasoning "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/QuantumNous/new-api/setting/reasoning"
@@ -30,7 +31,13 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
adaptor := claude.Adaptor{}
// A model modifier is rendered into DeepSeek's own thinking and effort
// fields below; Anthropic's model rules (budgets, adaptive thinking,
// max_tokens and sampling changes) must not see it.
state := info.ReasoningConversion
info.ReasoningConversion = nil
convertedRequest, err := adaptor.ConvertClaudeRequest(c, info, req)
info.ReasoningConversion = state
if err != nil {
return nil, err
}
@@ -55,6 +62,14 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
// DeepSeek runs Claude Code and Codex searches and page fetches
// server-side and bills them through its Search and Fetch APIs, whatever
// model name the client sent.
info.WebSearchBillingKey = SearchToolPriceKey
info.WebFetchBillingKey = FetchToolPriceKey
if info.RelayMode == constant.RelayModeResponses {
info.VendorToolUsage = responsesToolUsage
}
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
@@ -71,6 +86,10 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
return fmt.Sprintf("%s/completions", fimBaseUrl), nil
case constant.RelayModeResponses:
return fmt.Sprintf("%s/responses", info.ChannelBaseUrl), nil
case constant.RelayModeToolsSearch:
return fmt.Sprintf("%s/v1/tools/search", info.ChannelBaseUrl), nil
case constant.RelayModeToolsFetch:
return fmt.Sprintf("%s/v1/tools/fetch", info.ChannelBaseUrl), nil
default:
return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil
}
@@ -99,10 +118,7 @@ func applyDeepSeekV4OpenAIThinkingSuffix(info *relaycommon.RelayInfo, request *d
if info != nil && info.ChannelMeta != nil && info.UpstreamModelName != "" {
modelName = info.UpstreamModelName
}
if model_setting.ShouldPreserveThinkingSuffix(modelName) || info != nil && model_setting.ShouldPreserveThinkingSuffix(info.OriginModelName) {
return nil
}
baseModel, thinkingType, effort, ok := reasoning.ParseDeepSeekV4ThinkingSuffix(modelName)
baseModel, thinkingType, effort, ok := deepSeekThinking(info, modelName)
if !ok {
return nil
}
@@ -115,11 +131,15 @@ func applyDeepSeekV4OpenAIThinkingSuffix(info *relaycommon.RelayInfo, request *d
request.Model = baseModel
request.THINKING = thinking
request.ReasoningEffort = effort
if info.ReasoningState() != nil {
// DeepSeek Chat has no reasoning object; a budget modifier adds one.
request.Reasoning = nil
}
if info != nil {
if info.ChannelMeta != nil {
info.UpstreamModelName = baseModel
}
info.SetReasoningEffort(effort)
info.SetReasoningEffort(deepSeekRunEffort(thinkingType, effort))
}
return nil
}
@@ -129,31 +149,36 @@ func applyDeepSeekV4ClaudeThinkingSuffix(info *relaycommon.RelayInfo, request *d
if info != nil && info.ChannelMeta != nil && info.UpstreamModelName != "" {
modelName = info.UpstreamModelName
}
if model_setting.ShouldPreserveThinkingSuffix(modelName) || info != nil && model_setting.ShouldPreserveThinkingSuffix(info.OriginModelName) {
return nil
}
baseModel, thinkingType, effort, ok := reasoning.ParseDeepSeekV4ThinkingSuffix(modelName)
baseModel, thinkingType, effort, ok := deepSeekThinking(info, modelName)
if !ok {
return nil
}
request.Model = baseModel
request.Thinking = &dto.Thinking{Type: thinkingType}
if effort == "" {
request.OutputConfig = nil
} else {
outputConfig, err := common.Marshal(map[string]string{
"effort": effort,
})
// Only effort is set or removed; other output_config members stay.
outputConfig := make(map[string]any)
if len(request.OutputConfig) > 0 && common.GetJsonType(request.OutputConfig) == "object" {
if err := common.Unmarshal(request.OutputConfig, &outputConfig); err != nil {
return fmt.Errorf("invalid output_config: %w", err)
}
}
delete(outputConfig, "effort")
if effort != "" {
outputConfig["effort"] = effort
}
request.OutputConfig = nil
if len(outputConfig) > 0 {
encoded, err := common.Marshal(outputConfig)
if err != nil {
return fmt.Errorf("error marshalling output_config: %w", err)
}
request.OutputConfig = outputConfig
request.OutputConfig = encoded
}
if info != nil {
if info.ChannelMeta != nil {
info.UpstreamModelName = baseModel
}
info.SetReasoningEffort(effort)
info.SetReasoningEffort(deepSeekRunEffort(thinkingType, effort))
}
return nil
}
@@ -177,25 +202,71 @@ func applyDeepSeekV4ResponsesThinkingSuffix(info *relaycommon.RelayInfo, request
if info != nil && info.ChannelMeta != nil && info.UpstreamModelName != "" {
modelName = info.UpstreamModelName
}
if model_setting.ShouldPreserveThinkingSuffix(modelName) || info != nil && model_setting.ShouldPreserveThinkingSuffix(info.OriginModelName) {
baseModel, thinkingType, effort, ok := deepSeekThinking(info, modelName)
if !ok {
if info != nil && request.Reasoning != nil {
info.SetReasoningEffort(request.Reasoning.Effort)
}
return
}
baseModel, thinkingType, effort, ok := reasoning.ParseDeepSeekV4ThinkingSuffix(modelName)
if ok {
if thinkingType == "disabled" {
effort = "none"
}
request.Model = baseModel
if request.Reasoning == nil {
request.Reasoning = &dto.Reasoning{}
}
if thinkingType == "disabled" {
effort = "none"
}
request.Model = baseModel
if request.Reasoning == nil && effort != "" {
request.Reasoning = &dto.Reasoning{}
}
if request.Reasoning != nil {
request.Reasoning.Effort = effort
if info != nil && info.ChannelMeta != nil {
}
if info != nil {
if info.ChannelMeta != nil {
info.UpstreamModelName = baseModel
}
info.SetReasoningEffort(deepSeekRunEffort(thinkingType, effort))
}
if info != nil && request.Reasoning != nil {
info.SetReasoningEffort(request.Reasoning.Effort)
}
// deepSeekThinking returns the model name to send with DeepSeek's thinking
// switch and effort. An explicit @ modifier (the reasoning state) wins over a
// -none/-low/-max tail, which is still stripped from the name. A budget maps
// to an effort as on other effort-only targets; thinking on or adaptive leaves
// the effort to DeepSeek's default.
func deepSeekThinking(info *relaycommon.RelayInfo, modelName string) (baseModel string, thinkingType string, effort string, ok bool) {
baseModel = modelName
if !model_setting.ShouldPreserveThinkingSuffix(modelName) && (info == nil || !model_setting.ShouldPreserveThinkingSuffix(info.OriginModelName)) {
baseModel, thinkingType, effort, ok = reasoning.ParseDeepSeekV4ThinkingSuffix(modelName)
}
state := info.ReasoningState()
if state == nil {
return baseModel, thinkingType, effort, ok
}
intent := kitreasoning.IntentFromState(state)
switch runs := kitreasoning.EffectiveEffort(intent); {
case runs == "":
return baseModel, thinkingType, effort, ok
case runs == kitreasoning.EffortNone:
return baseModel, "disabled", "", true
case intent.Effort == "" && intent.BudgetTokens == nil:
return baseModel, "enabled", "", true
default:
return baseModel, "enabled", string(runs), true
}
}
// deepSeekRunEffort is the effort DeepSeek runs: none with thinking off, its
// default high when no effort is sent, and the levels it does not implement
// mapped as DeepSeek maps them (minimal to low, medium and xhigh to high).
func deepSeekRunEffort(thinkingType string, effort string) string {
switch {
case thinkingType == "disabled":
return string(kitreasoning.EffortNone)
case effort == "" || effort == "medium" || effort == "xhigh":
return "high"
case effort == "minimal":
return "low"
default:
return effort
}
}
+270
View File
@@ -0,0 +1,270 @@
package deepseek
import (
"fmt"
"strings"
"testing"
"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// The -none/-low/-max tails of deepseek-flash and deepseek-v4-* reach DeepSeek
// as its own thinking switch and effort in each upstream format, with the base
// name; a bare name keeps DeepSeek's default, and a blacklisted name is sent
// verbatim.
func TestThinkingSuffixUpstreamFields(t *testing.T) {
settings := model_setting.GetGlobalSettings()
original := append([]string(nil), settings.ThinkingModelBlacklist...)
t.Cleanup(func() { settings.ThinkingModelBlacklist = original })
settings.ThinkingModelBlacklist = append(original, "deepseek-v4-pro-max")
tests := []struct {
model, upstream, thinking, effort string
}{
{model: "deepseek-flash", upstream: "deepseek-flash"},
{model: "deepseek-flash-none", upstream: "deepseek-flash", thinking: "disabled"},
{model: "deepseek-flash-low", upstream: "deepseek-flash", thinking: "enabled", effort: "low"},
{model: "deepseek-flash-max", upstream: "deepseek-flash", thinking: "enabled", effort: "max"},
{model: "deepseek-v4-pro-none", upstream: "deepseek-v4-pro", thinking: "disabled"},
{model: "deepseek-v4-pro-low", upstream: "deepseek-v4-pro", thinking: "enabled", effort: "low"},
{model: "deepseek-v4-pro-max", upstream: "deepseek-v4-pro-max"},
}
newInfo := func(model string) *relaycommon.RelayInfo {
return &relaycommon.RelayInfo{OriginModelName: model, ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: model}}
}
for _, tt := range tests {
t.Run(tt.model, func(t *testing.T) {
chat := &dto.GeneralOpenAIRequest{Model: tt.model}
converted, err := (&Adaptor{}).ConvertOpenAIRequest(nil, newInfo(tt.model), chat)
require.NoError(t, err)
chat = converted.(*dto.GeneralOpenAIRequest)
assert.Equal(t, tt.upstream, chat.Model)
assert.Equal(t, tt.effort, chat.ReasoningEffort)
claude := &dto.ClaudeRequest{Model: tt.model}
require.NoError(t, applyDeepSeekV4ClaudeThinkingSuffix(newInfo(tt.model), claude))
assert.Equal(t, tt.upstream, claude.Model)
converted, err = (&Adaptor{}).ConvertOpenAIResponsesRequest(nil, newInfo(tt.model), dto.OpenAIResponsesRequest{Model: tt.model})
require.NoError(t, err)
responses := converted.(dto.OpenAIResponsesRequest)
assert.Equal(t, tt.upstream, responses.Model)
if tt.thinking == "" {
assert.Empty(t, chat.THINKING)
assert.Nil(t, claude.Thinking)
assert.Nil(t, claude.OutputConfig)
assert.Nil(t, responses.Reasoning)
return
}
assert.JSONEq(t, `{"type":"`+tt.thinking+`"}`, string(chat.THINKING))
require.NotNil(t, claude.Thinking)
assert.Equal(t, tt.thinking, claude.Thinking.Type)
require.NotNil(t, responses.Reasoning)
if tt.thinking == "disabled" {
assert.Nil(t, claude.OutputConfig)
assert.Equal(t, "none", responses.Reasoning.Effort)
return
}
assert.JSONEq(t, `{"effort":"`+tt.effort+`"}`, string(claude.OutputConfig))
assert.Equal(t, tt.effort, responses.Reasoning.Effort)
})
}
}
// DeepSeek runs Claude Code and Codex searches server-side and bills them as
// Search and Fetch API calls, whatever model name the client sent. On the
// Responses API the completed web_search_call items of the whole response
// replace the items counted while streaming: open_page is a Fetch call, a
// failed call is free, and an empty output keeps the streamed count.
func TestServerSideSearchBillingKeys(t *testing.T) {
for _, tc := range []struct {
format types.RelayFormat
mode int
}{
{types.RelayFormatClaude, constant.RelayModeUnknown},
{types.RelayFormatOpenAI, constant.RelayModeChatCompletions},
{types.RelayFormatOpenAIResponses, constant.RelayModeResponses},
} {
info := &relaycommon.RelayInfo{RelayFormat: tc.format, RelayMode: tc.mode, ChannelMeta: &relaycommon.ChannelMeta{}}
(&Adaptor{}).Init(info)
assert.Equal(t, SearchToolPriceKey, info.WebSearchBillingKey, tc.format)
assert.Equal(t, FetchToolPriceKey, info.WebFetchBillingKey, tc.format)
assert.Equal(t, tc.mode == constant.RelayModeResponses, info.VendorToolUsage != nil, tc.format)
}
const output = `[
{"type":"web_search_call","status":"completed","action":{"type":"search","queries":["deepseek","ws_call_id=call_01"]}},
{"type":"web_search_call","status":"completed","action":{"type":"open_page","url":"https://example.com/"}},
{"type":"web_search_call","status":"failed","action":{"type":"open_page","url":"https://api-docs.deepseek.com/"}},
{"type":"message","status":"completed"}]`
for _, raw := range []string{
`{"status":"completed","output":` + output + `}`,
`{"type":"response.completed","response":{"status":"completed","output":` + output + `}}`,
} {
info := &relaycommon.RelayInfo{
OriginModelName: "gpt-5.1-codex",
RelayFormat: types.RelayFormatOpenAIResponses,
RelayMode: constant.RelayModeResponses,
ChannelMeta: &relaycommon.ChannelMeta{},
}
(&Adaptor{}).Init(info)
for range 3 {
info.CountBillableToolCall(dto.BuildInCallWebSearchCall, "")
}
assert.Equal(t, map[string]int{SearchToolPriceKey: 3}, toolCounts(info), "streamed items bill under the Search key")
info.ApplyVendorToolUsage([]byte(`{"type":"response.failed","response":{"status":"failed","output":[]}}`))
assert.Equal(t, map[string]int{SearchToolPriceKey: 3}, toolCounts(info), "an empty output keeps the streamed count")
info.ApplyVendorToolUsage([]byte(raw))
assert.Equal(t, map[string]int{SearchToolPriceKey: 1, FetchToolPriceKey: 1}, toolCounts(info), raw)
}
}
func toolCounts(info *relaycommon.RelayInfo) map[string]int {
counts := map[string]int{}
for name, tool := range info.ResponsesUsageInfo.BuiltInTools {
counts[name] = tool.CallCount
}
return counts
}
// Explicit @ modifiers reach DeepSeek as its own thinking switch and effort in
// every format and override the client's reasoning fields: no budget_tokens,
// adaptive thinking, max_tokens raise or sampling change. An @ effort wins
// over a -none/-low/-max tail, a budget maps to an effort, and a request
// without a modifier passes through unchanged. The usage log records the
// effort DeepSeek runs at.
func TestModelModifiersUpstreamFields(t *testing.T) {
type client struct{ chat, claude, responses string }
tests := []struct {
name, model string
client client
thinking, effort string // thinking "" = untouched
logEffort string
}{
{name: "thinking on", model: "deepseek-flash@thinking:on", thinking: "enabled", logEffort: "high"},
{name: "thinking off", model: "deepseek-flash@thinking:off", thinking: "disabled", logEffort: "none"},
{name: "thinking zero budget", model: "deepseek-flash@thinking:0", thinking: "disabled", logEffort: "none"},
{name: "effort none", model: "deepseek-flash@effort:none", thinking: "disabled", logEffort: "none"},
{name: "effort low", model: "deepseek-flash@effort:low", thinking: "enabled", effort: "low", logEffort: "low"},
{name: "effort medium", model: "deepseek-flash@effort:medium", thinking: "enabled", effort: "medium", logEffort: "high"},
{name: "effort xhigh", model: "deepseek-flash@effort:xhigh", thinking: "enabled", effort: "xhigh", logEffort: "high"},
{name: "effort max on pro", model: "deepseek-v4-pro@effort:max", thinking: "enabled", effort: "max", logEffort: "max"},
{name: "budget maps to an effort", model: "deepseek-flash@thinking:8192", thinking: "enabled", effort: "medium", logEffort: "high"},
{name: "adaptive", model: "deepseek-flash@thinking:adaptive", thinking: "enabled", logEffort: "high"},
{name: "temperature with effort", model: "deepseek-flash@temperature:0.3@effort:low", thinking: "enabled", effort: "low", logEffort: "low"},
{name: "effort overrides client thinking off", model: "deepseek-flash@effort:max", thinking: "enabled", effort: "max", logEffort: "max",
client: client{chat: `"thinking":{"type":"disabled"}`, claude: `"thinking":{"type":"disabled"}`, responses: `"reasoning":{"effort":"none"}`}},
{name: "thinking off overrides client thinking", model: "deepseek-flash@thinking:off", thinking: "disabled", logEffort: "none",
client: client{chat: `"thinking":{"type":"enabled"},"reasoning_effort":"high"`,
claude: `"thinking":{"type":"enabled","budget_tokens":2048},"output_config":{"effort":"high","format":{"type":"json_schema"}}`,
responses: `"reasoning":{"effort":"high"}`}},
{name: "budget overrides client effort none", model: "deepseek-flash@thinking:8192", thinking: "enabled", effort: "medium", logEffort: "high",
client: client{responses: `"reasoning":{"effort":"none"}`}},
{name: "@ effort wins over a tail", model: "deepseek-flash-max@effort:low", thinking: "enabled", effort: "low", logEffort: "low"},
{name: "no modifier", model: "deepseek-flash",
client: client{chat: `"thinking":{"type":"disabled"},"reasoning_effort":"low"`, claude: `"thinking":{"type":"adaptive"},"output_config":{"effort":"low"}`, responses: `"reasoning":{"effort":"low"}`}},
}
extra := func(members string) string {
if members == "" {
return ""
}
return "," + members
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bodies := map[types.RelayFormat]string{
types.RelayFormatOpenAI: fmt.Sprintf(`{"model":%q,"messages":[{"role":"user","content":"hi"}]%s}`, tt.model, extra(tt.client.chat)),
types.RelayFormatClaude: fmt.Sprintf(`{"model":%q,"max_tokens":64,"temperature":0.7,"messages":[{"role":"user","content":"hi"}]%s}`, tt.model, extra(tt.client.claude)),
types.RelayFormatOpenAIResponses: fmt.Sprintf(`{"model":%q,"input":"hi"%s}`, tt.model, extra(tt.client.responses)),
}
for format, body := range bodies {
info := &relaycommon.RelayInfo{
OriginModelName: tt.model,
RelayFormat: format,
ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: tt.model},
}
var sent []byte
switch format {
case types.RelayFormatOpenAI:
original, outbound := &dto.GeneralOpenAIRequest{}, &dto.GeneralOpenAIRequest{}
require.NoError(t, common.UnmarshalJsonStr(body, original))
require.NoError(t, common.UnmarshalJsonStr(body, outbound))
info.Request = original
require.NoError(t, helper.ApplyReasoningModelSuffix(nil, info, outbound))
converted, err := (&Adaptor{}).ConvertOpenAIRequest(nil, info, outbound)
require.NoError(t, err)
sent, err = common.Marshal(converted)
require.NoError(t, err)
case types.RelayFormatClaude:
original, outbound := &dto.ClaudeRequest{}, &dto.ClaudeRequest{}
require.NoError(t, common.UnmarshalJsonStr(body, original))
require.NoError(t, common.UnmarshalJsonStr(body, outbound))
info.Request = original
require.NoError(t, helper.ApplyReasoningModelSuffix(nil, info, outbound))
converted, err := (&Adaptor{}).ConvertClaudeRequest(nil, info, outbound)
require.NoError(t, err)
sent, err = common.Marshal(converted)
require.NoError(t, err)
default:
original, outbound := &dto.OpenAIResponsesRequest{}, &dto.OpenAIResponsesRequest{}
require.NoError(t, common.UnmarshalJsonStr(body, original))
require.NoError(t, common.UnmarshalJsonStr(body, outbound))
info.Request = original
require.NoError(t, helper.ApplyReasoningModelSuffix(nil, info, outbound))
converted, err := (&Adaptor{}).ConvertOpenAIResponsesRequest(nil, info, *outbound)
require.NoError(t, err)
sent, err = common.Marshal(converted)
require.NoError(t, err)
}
wire := gjson.ParseBytes(sent)
wantModel := "deepseek-flash"
if strings.HasPrefix(tt.model, "deepseek-v4-pro") {
wantModel = "deepseek-v4-pro"
}
assert.Equal(t, wantModel, wire.Get("model").String(), format)
if tt.thinking == "" {
// No modifier: the client's reasoning fields go out as sent.
for _, member := range []string{"thinking", "reasoning_effort", "output_config", "reasoning"} {
assert.Equal(t, gjson.Get(body, member).Raw, wire.Get(member).Raw, "%s %s", format, member)
}
continue
}
switch format {
case types.RelayFormatOpenAI:
assert.JSONEq(t, `{"type":"`+tt.thinking+`"}`, wire.Get("thinking").Raw, format)
assert.Equal(t, tt.effort, wire.Get("reasoning_effort").String(), format)
assert.False(t, wire.Get("reasoning").Exists(), format)
case types.RelayFormatClaude:
assert.JSONEq(t, `{"type":"`+tt.thinking+`"}`, wire.Get("thinking").Raw, format)
assert.Equal(t, tt.effort, wire.Get("output_config.effort").String(), format)
assert.Equal(t, gjson.Get(body, "output_config.format").Raw, wire.Get("output_config.format").Raw, format)
assert.Equal(t, int64(64), wire.Get("max_tokens").Int(), format)
wantTemperature := 0.7
if strings.Contains(tt.model, "@temperature:0.3") {
wantTemperature = 0.3
}
assert.Equal(t, wantTemperature, wire.Get("temperature").Float(), format)
default:
wantEffort := tt.effort
if tt.thinking == "disabled" {
wantEffort = "none"
}
assert.Equal(t, wantEffort, wire.Get("reasoning.effort").String(), format)
}
assert.Equal(t, tt.logEffort, info.ReasoningEffort, format)
}
})
}
}
+13 -3
View File
@@ -1,9 +1,19 @@
package deepseek
import "github.com/QuantumNous/new-api/constant"
var ModelList = []string{
"deepseek-chat", "deepseek-reasoner",
"deepseek-v4-flash", "deepseek-v4-flash-none", "deepseek-v4-flash-max",
"deepseek-v4-pro", "deepseek-v4-pro-none", "deepseek-v4-pro-max",
"deepseek-flash", "deepseek-flash-none", "deepseek-flash-low", "deepseek-flash-max",
"deepseek-v4-pro", "deepseek-v4-pro-none", "deepseek-v4-pro-low", "deepseek-v4-pro-max",
constant.DeepSeekSearchModel, constant.DeepSeekFetchModel,
}
var ChannelName = "deepseek"
// Tool-price keys of the DeepSeek Search and Fetch APIs. They price the
// server-side searches and page fetches DeepSeek runs on its Anthropic and
// Responses endpoints.
const (
SearchToolPriceKey = "deepseek_search"
FetchToolPriceKey = "deepseek_fetch"
)
+37
View File
@@ -0,0 +1,37 @@
package deepseek
import (
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/tidwall/gjson"
)
// responsesToolUsage bills the Search and Fetch API calls DeepSeek runs for
// the Responses web_search tool from the web_search_call items of the whole
// response (output on non-stream bodies, response.output on terminal stream
// frames); its usage reports tokens only. An open_page action is a Fetch call,
// any other action a Search call, and a failed call is free. The counts
// replace the web_search_call items counted under the Search key while the
// stream was running. An empty output proves nothing, so those counts stay.
func responsesToolUsage(raw []byte) map[string]int {
output := gjson.GetBytes(raw, "output")
if !output.Exists() {
output = gjson.GetBytes(raw, "response.output")
}
items := output.Array()
if !output.IsArray() || len(items) == 0 {
return nil
}
counts := map[string]int{SearchToolPriceKey: 0, FetchToolPriceKey: 0}
for _, item := range items {
if item.Get("type").String() != dto.BuildInCallWebSearchCall || item.Get("status").String() != "completed" {
continue
}
if item.Get("action.type").String() == "open_page" {
counts[FetchToolPriceKey]++
} else {
counts[SearchToolPriceKey]++
}
}
return counts
}