mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +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.
128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
package relay
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
relayconstant "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/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// DeepSeekToolsHelper relays the DeepSeek Search and Fetch APIs like
|
|
// /v1/alpha/search: the raw body is forwarded, the response is returned
|
|
// unchanged, and the call is billed at the routing name's request price with
|
|
// no tokens.
|
|
func DeepSeekToolsHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) {
|
|
info.InitChannelMeta(c)
|
|
|
|
switch info.ChannelType {
|
|
case constant.ChannelTypeDeepSeek,
|
|
constant.ChannelTypeNewAPI,
|
|
constant.ChannelTypeSub2API,
|
|
constant.ChannelTypeAdvancedCustom:
|
|
default:
|
|
// Allow retry onto another channel that may support this endpoint.
|
|
return types.NewError(
|
|
fmt.Errorf("channel does not support %s", c.Request.URL.Path),
|
|
types.ErrorCodeInvalidRequest,
|
|
)
|
|
}
|
|
|
|
request, ok := info.Request.(*dto.DeepSeekToolsRequest)
|
|
if !ok {
|
|
return types.NewErrorWithStatusCode(
|
|
fmt.Errorf("invalid request type, expected *dto.DeepSeekToolsRequest, got %T", info.Request),
|
|
types.ErrorCodeInvalidRequest,
|
|
http.StatusBadRequest,
|
|
types.ErrOptionWithSkipRetry(),
|
|
)
|
|
}
|
|
|
|
err := helper.ModelMappedHelper(c, info, request)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeChannelModelMappedError, types.ErrOptionWithSkipRetry())
|
|
}
|
|
|
|
jsonData, err := buildAlphaSearchRequestBody(request.RawBody, info.OriginModelName, info.UpstreamModelName)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
|
}
|
|
|
|
if len(info.ParamOverride) > 0 {
|
|
jsonData, err = relaycommon.ApplyParamOverrideWithRelayInfo(jsonData, info)
|
|
if err != nil {
|
|
return newAPIErrorFromParamOverride(err)
|
|
}
|
|
}
|
|
|
|
logger.LogDebug(c, "requestBody: %s", jsonData)
|
|
body, closer, err := relaycommon.NewOutboundJSONBody(jsonData)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
|
}
|
|
defer closer.Close()
|
|
|
|
adaptor := GetAdaptor(info.ApiType)
|
|
if adaptor == nil {
|
|
return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
|
|
}
|
|
adaptor.Init(info)
|
|
|
|
resp, err := adaptor.DoRequest(c, info, body)
|
|
if err != nil {
|
|
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
|
}
|
|
|
|
statusCodeMappingStr := c.GetString("status_code_mapping")
|
|
httpResp, ok := resp.(*http.Response)
|
|
if !ok || httpResp == nil {
|
|
return types.NewOpenAIError(errors.New("invalid http response"), types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
|
newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
|
|
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
|
|
return newAPIError
|
|
}
|
|
|
|
responseBody, err := io.ReadAll(httpResp.Body)
|
|
if err != nil {
|
|
return types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError, types.ErrOptionWithSkipRetry())
|
|
}
|
|
service.IOCopyBytesGracefully(c, httpResp, responseBody)
|
|
|
|
// DeepSeek bills a successful request only when it returns something: a
|
|
// non-empty results array for Search, page content for Fetch. A billable
|
|
// call settles at the routing name's request price; any other response
|
|
// releases the reservation.
|
|
var billable bool
|
|
switch info.RelayMode {
|
|
case relayconstant.RelayModeToolsFetch:
|
|
billable = gjson.GetBytes(responseBody, "content").String() != ""
|
|
default:
|
|
results := gjson.GetBytes(responseBody, "results")
|
|
billable = results.IsArray() && len(results.Array()) > 0
|
|
}
|
|
if !billable {
|
|
if info.Billing != nil {
|
|
info.Billing.Refund(c)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
service.PostTextConsumeQuota(c, info, &dto.Usage{}, nil)
|
|
return nil
|
|
}
|