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
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
switch channelType {
case constant.ChannelTypeOpenAI:
apiType = constant.APITypeOpenAI
case constant.ChannelTypeQiniu:
apiType = constant.APITypeOpenAI
case constant.ChannelTypeAnthropic:
apiType = constant.APITypeAnthropic
case constant.ChannelTypeBaidu:
Expand Down
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
ChannelTypeSora = 55
ChannelTypeReplicate = 56
ChannelTypeCodex = 57
ChannelTypeQiniu = 58
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -118,6 +119,7 @@ var ChannelBaseURLs = []string{
"https://api.openai.com", //55
"https://api.replicate.com", //56
"https://chatgpt.com", //57
"https://api.qnaigc.com", //58
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "Codex",
ChannelTypeQiniu: "Qiniu",
}

func GetChannelTypeName(channelType int) string {
Expand Down
47 changes: 47 additions & 0 deletions controller/channel_upstream_update_qiniu_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package controller

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
)

func TestFetchChannelUpstreamModelIDs_Qiniu(t *testing.T) {
t.Parallel()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("unexpected method: %s", r.Method)
}
if r.URL.Path != "/v1/models" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer test-key" {
t.Fatalf("unexpected Authorization header: %q", got)
}
Comment on lines +16 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Go testing package docs: can t.Fatalf (or t.FailNow) be called from a goroutine created by the test, such as an httptest server handler goroutine?

💡 Result:

No. In Go tests, t.Fatalf/t.FailNow (and t.Fatal/t.FailNow/t.SkipNow variants) must be called only from the goroutine running the test function; calling them from other goroutines created by the test (for example, an httptest server handler goroutine) is explicitly disallowed and can lead to race conditions and incorrect behavior. What the docs say: FailNow “stops its execution by calling runtime.Goexit” in the current goroutine, and “FailNow must be called from the goroutine running the test or benchmark function, not from other goroutines created during the test. Calling FailNow does not stop those other goroutines.” The general “test ends when … calls any of … T.FailNow, T.Fatal, T.Fatalf …” plus “must be called only from the goroutine running the Test function” language likewise applies. Safe patterns: - From goroutine(s) spawned during the test, use t.Errorf/t.Fail (non-fatal) or otherwise report via channels/WaitGroups and have the main test goroutine call Fatalf/FailNow. - For httptest handlers, avoid calling t.Fatal/Fatalf directly in the handler; instead signal failure back to the test goroutine (e.g., send an error on a channel), then fail in the test goroutine. If you need, I can show a small channel-based example for httptest handlers.

Citations:


Replace t.Fatalf calls in the handler goroutine with HTTP error responses.

Calling t.Fatalf from within the httptest handler goroutine violates Go testing semantics. Per the testing package documentation, t.Fatalf and t.FailNow must only be called from the test goroutine itself; calling them from spawned goroutines (including handlers) can cause race conditions and unreliable test failures. Return an HTTP error status from the handler and perform assertions in the main test goroutine instead.

Suggested adjustment
  srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
-     t.Fatalf("unexpected method: %s", r.Method)
+     http.Error(w, "unexpected method", http.StatusBadRequest)
+     return
    }
    if r.URL.Path != "/v1/models" {
-     t.Fatalf("unexpected path: %s", r.URL.Path)
+     http.Error(w, "unexpected path", http.StatusBadRequest)
+     return
    }
    if got := r.Header.Get("Authorization"); got != "Bearer test-key" {
-     t.Fatalf("unexpected Authorization header: %q", got)
+     http.Error(w, "unexpected Authorization header", http.StatusUnauthorized)
+     return
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/channel_upstream_update_qiniu_test.go` around lines 16 - 25, The
handler goroutine inside srv := httptest.NewServer(http.HandlerFunc(...) ) uses
t.Fatalf which must not be called from spawned goroutines; instead return HTTP
error responses (e.g., http.Error with 400/500) when checks on r.Method,
r.URL.Path or r.Header.Get("Authorization") fail, and capture the actual values
(method, path, auth header) into variables or send them on a channel so the main
test goroutine can perform the assertions after the request completes; replace
each t.Fatalf in the handler with an appropriate http.Error call and move the
t.Fatalf/assert checks into the main test goroutine using the captured values or
channel result.

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"object":"list","data":[{"id":"deepseek/deepseek-v3.1-terminus-thinking"},{"id":"gpt-4"}]}`))
}))
defer srv.Close()

ch := &model.Channel{
Id: 123,
Type: constant.ChannelTypeQiniu,
Key: "test-key",
Status: common.ChannelStatusEnabled,
BaseURL: common.GetPointer[string](srv.URL),
}

got, err := fetchChannelUpstreamModelIDs(ch)
if err != nil {
t.Fatalf("fetchChannelUpstreamModelIDs returned error: %v", err)
}
if len(got) != 2 || got[0] != "deepseek/deepseek-v3.1-terminus-thinking" || got[1] != "gpt-4" {
t.Fatalf("unexpected models: %#v", got)
}
}

2 changes: 1 addition & 1 deletion relay/channel/openai/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if request == nil {
return nil, errors.New("request is nil")
}
if info.ChannelType != constant.ChannelTypeOpenAI && info.ChannelType != constant.ChannelTypeAzure {
if !info.SupportStreamOptions {
request.StreamOptions = nil
}
if info.ChannelType == constant.ChannelTypeOpenRouter {
Expand Down
1 change: 1 addition & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ func (info *RelayInfo) ToString() string {
// 定义支持流式选项的通道类型
var streamSupportedChannels = map[int]bool{
constant.ChannelTypeOpenAI: true,
constant.ChannelTypeQiniu: true,
constant.ChannelTypeAnthropic: true,
constant.ChannelTypeAws: true,
constant.ChannelTypeGemini: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan';
// 支持并且已适配通过接口获取模型列表的渠道类型
const MODEL_FETCHABLE_TYPES = new Set([
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
58,
]);

function type2secretPrompt(type) {
Expand Down
6 changes: 6 additions & 0 deletions web/src/constants/channel.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,17 @@ export const CHANNEL_OPTIONS = [
color: 'blue',
label: 'Codex (OpenAI OAuth)',
},
{
value: 58,
color: 'green',
label: 'Qiniu',
},
];

// Channel types that support upstream model list fetching in UI.
export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
58,
]);

export const MODEL_TABLE_PAGE_SIZE = 10;
1 change: 1 addition & 0 deletions web/src/helpers/render.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export function getChannelIcon(channelType) {
case 1: // OpenAI
case 3: // Azure OpenAI
case 57: // Codex
case 58: // Qiniu (OpenAI compatible)
return <OpenAI size={iconSize} />;
case 2: // Midjourney Proxy
case 5: // Midjourney Proxy Plus
Expand Down