mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 21:20:58 +00:00
Compare commits
1 Commits
v0.11.2-pa
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40a3e19a78 |
@@ -40,6 +40,13 @@ type testResult struct {
|
||||
newAPIError *types.NewAPIError
|
||||
}
|
||||
|
||||
// testChannel executes a test request against the given channel using the provided testModel and optional endpointType,
|
||||
// and returns a testResult containing the test context and any encountered error information.
|
||||
// It selects or derives a model when testModel is empty, auto-detects the request endpoint (chat, responses, embeddings, images, rerank) when endpointType is not specified,
|
||||
// converts and relays the request to the upstream adapter, and parses the upstream response to collect usage and pricing information.
|
||||
// On upstream responses that indicate the chat/completions `messages` parameter is unsupported and endpointType was not specified, it will retry the test using the Responses API.
|
||||
// The function records consumption logs and returns a testResult with a populated context on success, or with localErr/newAPIError set on failure;
|
||||
// for channel types that are not supported for testing it returns a localErr explaining that the channel test is not supported.
|
||||
func testChannel(channel *model.Channel, testModel string, endpointType string) testResult {
|
||||
tik := time.Now()
|
||||
var unsupportedTestChannelTypes = []int{
|
||||
@@ -75,6 +82,8 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
|
||||
}
|
||||
}
|
||||
|
||||
originTestModel := testModel
|
||||
|
||||
requestPath := "/v1/chat/completions"
|
||||
|
||||
// 如果指定了端点类型,使用指定的端点类型
|
||||
@@ -84,6 +93,10 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
|
||||
}
|
||||
} else {
|
||||
// 如果没有指定端点类型,使用原有的自动检测逻辑
|
||||
if common.IsOpenAIResponseOnlyModel(testModel) {
|
||||
requestPath = "/v1/responses"
|
||||
}
|
||||
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
||||
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
||||
@@ -319,6 +332,13 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
|
||||
httpResp = resp.(*http.Response)
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
err := service.RelayErrorHandler(c.Request.Context(), httpResp, true)
|
||||
// 自动检测模式下,如果上游不支持 chat.completions 的 messages 参数,尝试切换到 Responses API 再测一次。
|
||||
if endpointType == "" && requestPath == "/v1/chat/completions" && err != nil {
|
||||
lowerErr := strings.ToLower(err.Error())
|
||||
if strings.Contains(lowerErr, "unsupported parameter") && strings.Contains(lowerErr, "messages") {
|
||||
return testChannel(channel, originTestModel, string(constant.EndpointTypeOpenAIResponse))
|
||||
}
|
||||
}
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
@@ -389,6 +409,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
|
||||
}
|
||||
}
|
||||
|
||||
// for embedding models, and otherwise a chat/completion request with model-specific token limit heuristics.
|
||||
func buildTestRequest(model string, endpointType string) dto.Request {
|
||||
// 根据端点类型构建不同的测试请求
|
||||
if endpointType != "" {
|
||||
@@ -417,9 +438,12 @@ func buildTestRequest(model string, endpointType string) dto.Request {
|
||||
}
|
||||
case constant.EndpointTypeOpenAIResponse:
|
||||
// 返回 OpenAIResponsesRequest
|
||||
maxOutputTokens := uint(10)
|
||||
return &dto.OpenAIResponsesRequest{
|
||||
Model: model,
|
||||
Input: json.RawMessage("\"hi\""),
|
||||
Model: model,
|
||||
Input: json.RawMessage(`[{"role":"user","content":"hi"}]`),
|
||||
MaxOutputTokens: maxOutputTokens,
|
||||
Stream: true,
|
||||
}
|
||||
case constant.EndpointTypeAnthropic, constant.EndpointTypeGemini, constant.EndpointTypeOpenAI:
|
||||
// 返回 GeneralOpenAIRequest
|
||||
@@ -442,6 +466,16 @@ func buildTestRequest(model string, endpointType string) dto.Request {
|
||||
}
|
||||
|
||||
// 自动检测逻辑(保持原有行为)
|
||||
if common.IsOpenAIResponseOnlyModel(model) {
|
||||
maxOutputTokens := uint(10)
|
||||
return &dto.OpenAIResponsesRequest{
|
||||
Model: model,
|
||||
Input: json.RawMessage(`[{"role":"user","content":"hi"}]`),
|
||||
MaxOutputTokens: maxOutputTokens,
|
||||
Stream: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(model), "embedding") ||
|
||||
strings.HasPrefix(model, "m3e") ||
|
||||
@@ -640,4 +674,4 @@ func AutomaticallyTestChannels() {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,8 @@ func GetAndValidateEmbeddingRequest(c *gin.Context, relayMode int) (*dto.Embeddi
|
||||
return embeddingRequest, nil
|
||||
}
|
||||
|
||||
// GetAndValidateResponsesRequest parses the HTTP request body into an OpenAIResponsesRequest and ensures the Model field is provided.
|
||||
// It returns the parsed request, or an error if the body cannot be parsed or the Model is empty.
|
||||
func GetAndValidateResponsesRequest(c *gin.Context) (*dto.OpenAIResponsesRequest, error) {
|
||||
request := &dto.OpenAIResponsesRequest{}
|
||||
err := common.UnmarshalBodyReusable(c, request)
|
||||
@@ -119,9 +121,6 @@ func GetAndValidateResponsesRequest(c *gin.Context) (*dto.OpenAIResponsesRequest
|
||||
if request.Model == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
if request.Input == nil {
|
||||
return nil, errors.New("input is required")
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
@@ -324,4 +323,4 @@ func GetAndValidateGeminiBatchEmbeddingRequest(c *gin.Context) (*dto.GeminiBatch
|
||||
return nil, err
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
}
|
||||
@@ -81,11 +81,24 @@ func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.Claude
|
||||
return claudeErr
|
||||
}
|
||||
|
||||
// RelayErrorHandler converts an HTTP error response into a structured types.NewAPIError.
|
||||
// It returns a NewAPIError initialized with the response status code and one of:
|
||||
// - an Err describing an absent or unreadable body,
|
||||
// - an Err containing the unmarshaled error message (or status + raw body when showBodyWhenFail is true), or
|
||||
// - an embedded OpenAI-style error when the response body contains a compatible error object.
|
||||
// The returned NewAPIError's status code reflects resp.StatusCode.
|
||||
func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
|
||||
newApiErr = types.InitOpenAIError(types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
|
||||
|
||||
if resp.Body == nil {
|
||||
newApiErr.Err = errors.New("response body is nil")
|
||||
return
|
||||
}
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
CloseResponseBodyGracefully(resp)
|
||||
newApiErr.Err = fmt.Errorf("read response body failed: %w", err)
|
||||
return
|
||||
}
|
||||
CloseResponseBodyGracefully(resp)
|
||||
@@ -156,4 +169,4 @@ func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError {
|
||||
}
|
||||
|
||||
return taskError
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user