Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bin/time_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ times=()

for ((i=1; i<=count; i++)); do
result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \
https://"$domain"/v1/chat/completions \
https://"$domain"/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $key" \
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}')
-d '{"input": [{"role": "user", "content": "echo hi"}], "model": "'"$model"'", "stream": false, "max_output_tokens": 1}')
http_code=$(echo "$result" | awk '{print $1}')
time=$(echo "$result" | awk '{print $2}')
echo "HTTP status code: $http_code, Time taken: $time"
Expand Down
4 changes: 2 additions & 2 deletions common/endpoint_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import "github.com/QuantumNous/new-api/constant"
// 目前均为 POST,后续可扩展
//
// json 标签用于直接序列化到 API 输出
// 例如:{"path":"/v1/chat/completions","method":"POST"}
// 例如:{"path":"/v1/responses","method":"POST"}

type EndpointInfo struct {
Path string `json:"path"`
Expand All @@ -17,7 +17,7 @@ type EndpointInfo struct {

// defaultEndpointInfoMap 保存内置端点的默认 Path 与 Method
var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
constant.EndpointTypeOpenAI: {Path: "/v1/chat/completions", Method: "POST"},
constant.EndpointTypeOpenAI: {Path: "/v1/responses", Method: "POST"},
constant.EndpointTypeOpenAIResponse: {Path: "/v1/responses", Method: "POST"},
constant.EndpointTypeAnthropic: {Path: "/v1/messages", Method: "POST"},
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
Expand Down
69 changes: 28 additions & 41 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
}
}

requestPath := "/v1/chat/completions"
requestPath := "/v1/responses"

// 如果指定了端点类型,使用指定的端点类型
if endpointType != "" {
Expand Down Expand Up @@ -137,7 +137,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
// 根据指定的端点类型设置 relayFormat
switch constant.EndpointType(endpointType) {
case constant.EndpointTypeOpenAI:
relayFormat = types.RelayFormatOpenAI
relayFormat = types.RelayFormatOpenAIResponses
case constant.EndpointTypeOpenAIResponse:
relayFormat = types.RelayFormatOpenAIResponses
case constant.EndpointTypeAnthropic:
Expand All @@ -155,7 +155,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
}
} else {
// 根据请求路径自动检测
relayFormat = types.RelayFormatOpenAI
relayFormat = types.RelayFormatOpenAIResponses
if c.Request.URL.Path == "/v1/embeddings" {
relayFormat = types.RelayFormatEmbedding
}
Expand All @@ -176,7 +176,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
}
}

request := buildTestRequest(testModel, endpointType)
request := buildTestRequest(testModel, endpointType, requestPath)

info, err := relaycommon.GenRelayInfo(c, relayFormat, request, nil)

Expand Down Expand Up @@ -389,7 +389,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
}
}

func buildTestRequest(model string, endpointType string) dto.Request {
func buildTestRequest(model string, endpointType string, requestPath string) dto.Request {
// 根据端点类型构建不同的测试请求
if endpointType != "" {
switch constant.EndpointType(endpointType) {
Expand All @@ -415,14 +415,16 @@ func buildTestRequest(model string, endpointType string) dto.Request {
Documents: []any{"Deep Learning is a subset of machine learning.", "Machine learning is a field of artificial intelligence."},
TopN: 2,
}
case constant.EndpointTypeOpenAIResponse:
case constant.EndpointTypeOpenAIResponse, constant.EndpointTypeOpenAI:
// 返回 OpenAIResponsesRequest
return &dto.OpenAIResponsesRequest{
Model: model,
Input: json.RawMessage("\"hi\""),
Model: model,
Input: json.RawMessage(`[{"role":"user","content":"hi"}]`),
MaxOutputTokens: 10,
Stream: false,
}
case constant.EndpointTypeAnthropic, constant.EndpointTypeGemini, constant.EndpointTypeOpenAI:
// 返回 GeneralOpenAIRequest
case constant.EndpointTypeAnthropic, constant.EndpointTypeGemini:
// 返回 GeneralOpenAIRequest(兼容现有适配逻辑)
maxTokens := uint(10)
if constant.EndpointType(endpointType) == constant.EndpointTypeGemini {
maxTokens = 3000
Expand All @@ -441,43 +443,28 @@ func buildTestRequest(model string, endpointType string) dto.Request {
}
}

// 自动检测逻辑(保持原有行为)
// 先判断是否为 Embedding 模型
if strings.Contains(strings.ToLower(model), "embedding") ||
strings.HasPrefix(model, "m3e") ||
strings.Contains(model, "bge-") {
// 返回 EmbeddingRequest
// 自动检测逻辑(按最终请求路径构造)
switch requestPath {
case "/v1/embeddings":
return &dto.EmbeddingRequest{
Model: model,
Input: []any{"hello world"},
}
}

// Chat/Completion 请求 - 返回 GeneralOpenAIRequest
testRequest := &dto.GeneralOpenAIRequest{
Model: model,
Stream: false,
Messages: []dto.Message{
{
Role: "user",
Content: "hi",
},
},
}

if strings.HasPrefix(model, "o") {
testRequest.MaxCompletionTokens = 10
} else if strings.Contains(model, "thinking") {
if !strings.Contains(model, "claude") {
testRequest.MaxTokens = 50
case "/v1/images/generations":
return &dto.ImageRequest{
Model: model,
Prompt: "a cute cat",
N: 1,
Size: "1024x1024",
}
default:
return &dto.OpenAIResponsesRequest{
Model: model,
Input: json.RawMessage(`[{"role":"user","content":"hi"}]`),
MaxOutputTokens: 10,
Stream: false,
}
} else if strings.Contains(model, "gemini") {
testRequest.MaxTokens = 3000
} else {
testRequest.MaxTokens = 10
}

return testRequest
}

func TestChannel(c *gin.Context) {
Expand Down
19 changes: 9 additions & 10 deletions controller/playground.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"errors"
"fmt"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"

"github.com/gin-gonic/gin"
Expand All @@ -29,13 +30,11 @@ func Playground(c *gin.Context) {
return
}

relayInfo, err := relaycommon.GenRelayInfo(c, types.RelayFormatOpenAI, nil, nil)
if err != nil {
newAPIError = types.NewError(err, types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
return
}

userId := c.GetInt("id")
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
if usingGroup == "" {
usingGroup = common.GetContextKeyString(c, constant.ContextKeyUserGroup)
}

// Write user context to ensure acceptUnsetRatio is available
userCache, err := model.GetUserCache(userId)
Expand All @@ -47,10 +46,10 @@ func Playground(c *gin.Context) {

tempToken := &model.Token{
UserId: userId,
Name: fmt.Sprintf("playground-%s", relayInfo.UsingGroup),
Group: relayInfo.UsingGroup,
Name: fmt.Sprintf("playground-%s", usingGroup),
Group: usingGroup,
}
_ = middleware.SetupContextForToken(c, tempToken)

Relay(c, types.RelayFormatOpenAI)
Relay(c, types.RelayFormatOpenAIResponses)
}
104 changes: 1 addition & 103 deletions docs/openapi/relay.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,68 +183,6 @@
]
}
},
"/v1/chat/completions": {
"post": {
"summary": "创建聊天对话",
"deprecated": false,
"description": "根据对话历史创建模型响应。支持流式和非流式响应。\n\n兼容 OpenAI Chat Completions API。\n",
"operationId": "createChatCompletion",
"tags": [
"OpenAI格式(Chat)"
],
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatCompletionRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "成功创建响应",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatCompletionResponse"
}
}
},
"headers": {}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"headers": {}
},
"429": {
"description": "请求频率限制",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"headers": {}
}
},
"security": [
{
"BearerAuth": []
}
]
}
},
"/v1/responses": {
"post": {
"summary": "创建响应 (OpenAI Responses API)",
Expand Down Expand Up @@ -1511,46 +1449,6 @@
]
}
},
"/v1/completions": {
"post": {
"summary": "创建文本补全",
"deprecated": false,
"description": "基于给定提示创建文本补全",
"operationId": "createCompletion",
"tags": [
"文本补全(Completions)"
],
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CompletionRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "成功创建响应",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CompletionResponse"
}
}
},
"headers": {}
}
},
"security": [
{
"BearerAuth": []
}
]
}
},
"/v1/audio/transcriptions": {
"post": {
"summary": "音频转录",
Expand Down Expand Up @@ -7138,4 +7036,4 @@
"BearerAuth": []
}
]
}
}
7 changes: 7 additions & 0 deletions dto/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type OpenAIErrorWithStatusCode struct {

type GeneralErrorResponse struct {
Error json.RawMessage `json:"error"`
Detail string `json:"detail"`
Message string `json:"message"`
Msg string `json:"msg"`
Err string `json:"err"`
Expand Down Expand Up @@ -56,6 +57,9 @@ func (e GeneralErrorResponse) ToMessage() string {
if err == nil && openAIError.Message != "" {
return openAIError.Message
}
// Some upstreams return an object without a "message" field.
// Falling back to the raw JSON is better than returning an empty string.
return string(e.Error)
case "string":
var msg string
err := common.Unmarshal(e.Error, &msg)
Expand All @@ -69,6 +73,9 @@ func (e GeneralErrorResponse) ToMessage() string {
if e.Message != "" {
return e.Message
}
if e.Detail != "" {
return e.Detail
}
if e.Msg != "" {
return e.Msg
}
Expand Down
43 changes: 43 additions & 0 deletions dto/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dto

import (
"encoding/json"
"testing"
)

func TestGeneralErrorResponse_ToMessage_ObjectWithoutMessageFallsBackToRaw(t *testing.T) {
resp := GeneralErrorResponse{
Error: json.RawMessage(`{"code":"model_not_found"}`),
}
if got, want := resp.ToMessage(), `{"code":"model_not_found"}`; got != want {
t.Fatalf("ToMessage() = %q, want %q", got, want)
}
}

func TestGeneralErrorResponse_ToMessage_ObjectWithMessageUsesMessage(t *testing.T) {
resp := GeneralErrorResponse{
Error: json.RawMessage(`{"message":"nope","type":"invalid_request_error"}`),
}
if got, want := resp.ToMessage(), "nope"; got != want {
t.Fatalf("ToMessage() = %q, want %q", got, want)
}
}

func TestGeneralErrorResponse_ToMessage_StringErrorUsesString(t *testing.T) {
resp := GeneralErrorResponse{
Error: json.RawMessage(`"nope"`),
}
if got, want := resp.ToMessage(), "nope"; got != want {
t.Fatalf("ToMessage() = %q, want %q", got, want)
}
}

func TestGeneralErrorResponse_ToMessage_DetailUsesDetail(t *testing.T) {
var resp GeneralErrorResponse
if err := json.Unmarshal([]byte(`{"detail":"Unsupported parameter: messages"}`), &resp); err != nil {
t.Fatalf("json.Unmarshal() error: %v", err)
}
if got, want := resp.ToMessage(), "Unsupported parameter: messages"; got != want {
t.Fatalf("ToMessage() = %q, want %q", got, want)
}
}
Loading