feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user