Skip to content

Support Qiniu provider with OpenAI API compatibility#4256

Open
liangchaoboy wants to merge 4 commits intoQuantumNous:mainfrom
liangchaoboy:feat/qiniu-provider
Open

Support Qiniu provider with OpenAI API compatibility#4256
liangchaoboy wants to merge 4 commits intoQuantumNous:mainfrom
liangchaoboy:feat/qiniu-provider

Conversation

@liangchaoboy
Copy link
Copy Markdown

@liangchaoboy liangchaoboy commented Apr 15, 2026

变更描述 / Description

本 PR 为 Qiniu(七牛) 增加了一个独立的渠道类型(type=58),并将其按 OpenAI 兼容供应商接入现有转发链路:后端将 ChannelTypeQiniu 映射到 APITypeOpenAI,因此请求转发与鉴权逻辑复用 OpenAI adaptor(Authorization: Bearer <key>),无需额外的请求体/响应体转换。

同时,补齐了“从上游拉取模型列表”的能力:前端将 type=58 加入可拉取模型列表的白名单集合,使渠道编辑弹窗可以调用现有的 /v1/models 拉取流程;后端沿用统一的 /v1/models 解析逻辑,返回 data[].id 作为模型列表。新增单元测试使用 httptest 模拟上游 /v1/models,验证请求路径与 Bearer 鉴权头正确,并能正确解析模型 ID。

变更类型 / Type of change

  • Bug 修复 (Bug fix)
  • 新功能 (New feature)
  • 性能优化 / 重构 (Refactor)
  • 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

运行证明 / Proof of Work

本地测试通过(只跑本次新增覆盖项):

go test ./controller -run Qiniu
ok  	github.com/QuantumNous/new-api/controller	0.456s

Summary by CodeRabbit

  • New Features

    • Added Qiniu as a selectable channel provider in the UI, including icon/label, upstream model fetching, and enabled streaming options where supported.
    • Integrated Qiniu into model-mapping and upstream model management flows.
  • Tests

    • Added unit test coverage for fetching model listings from the Qiniu channel.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84a83fdd-3864-4764-ae8c-fe8fd295f3b0

📥 Commits

Reviewing files that changed from the base of the PR and between 11fe2c7 and f5daa13.

📒 Files selected for processing (3)
  • constant/channel.go
  • web/src/components/table/channels/modals/EditChannelModal.jsx
  • web/src/constants/channel.constants.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/constants/channel.constants.js
  • constant/channel.go

Walkthrough

Adds Qiniu as channel type 58: constant, base URL, name; maps it to OpenAI API type; enables stream options and upstream model fetching for Qiniu; updates frontend channel option and icon; and adds a unit test for fetching Qiniu /v1/models.

Changes

Cohort / File(s) Summary
Channel constants & mapping
constant/channel.go, common/api_type.go
Add ChannelTypeQiniu = 58, register base URL https://api.qnaigc.com, add name "Qiniu", and map channel type 58 → APITypeOpenAI.
Relay/runtime behavior
relay/common/relay_info.go, relay/channel/openai/adaptor.go
Mark Qiniu as stream-supported and make clearing of request.StreamOptions depend on SupportStreamOptions rather than explicit channel-type checks.
Backend test
controller/channel_upstream_update_qiniu_test.go
Add TestFetchChannelUpstreamModelIDs_Qiniu verifying GET /v1/models with Bearer auth and correct parsing of returned model IDs.
Frontend: constants, UI, icon
web/src/constants/channel.constants.js, web/src/components/table/channels/modals/EditChannelModal.jsx, web/src/helpers/render.jsx
Add Qiniu option (value: 58) to CHANNEL_OPTIONS, include 58 in MODEL_FETCHABLE_CHANNEL_TYPES/MODEL_FETCHABLE_TYPES, and render OpenAI icon for type 58.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Frontend
  participant Backend
  participant RelayAdaptor
  participant QiniuAPI

  User->>Frontend: Open channel edit / request model list
  Frontend->>Backend: GET /channels/:id/models (channelType=58)
  Backend->>RelayAdaptor: fetchChannelUpstreamModelIDs(channel)
  RelayAdaptor->>QiniuAPI: GET /v1/models (Authorization: Bearer <key>)
  QiniuAPI-->>RelayAdaptor: 200 JSON {data: [...]}
  RelayAdaptor-->>Backend: parsed model ID list
  Backend-->>Frontend: model list response
  Frontend-->>User: display upstream models
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • seefs001

Poem

🐇 I hopped a path of code tonight,

Type fifty-eight now gleams in light.
Qiniu joins the model queue,
Streams and icons—fresh and new.
A little hop for bytes in flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for Qiniu as a new OpenAI-compatible channel provider. It is concise, specific, and directly reflects the core objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/channel_upstream_update_qiniu_test.go`:
- Around line 17-25: The handler in channel_upstream_update_qiniu_test.go is
calling t.Fatalf from the server goroutine (lines checking r.Method, r.URL.Path,
and Authorization); instead, capture the observed values (method, path, auth
header, etc.) into local variables or send them over a channel/sync struct from
the handler, return a normal HTTP response, then after invoking
fetchChannelUpstreamModelIDs in the test goroutine assert those captured values
with t.Fatalf/require. Locate the inline HTTP handler in the test and change its
t.Fatalf calls to store values (or send them on a channel) and perform the
assertions in the test goroutine after fetchChannelUpstreamModelIDs returns.

In `@relay/common/relay_info.go`:
- Line 308: Remove constant.ChannelTypeQiniu from the streamSupportedChannels
map so Qiniu isn't treated as supporting stream_options; locate the map named
streamSupportedChannels and delete the entry "constant.ChannelTypeQiniu: true"
(or conditionally exclude ChannelTypeQiniu wherever streamSupportedChannels is
used) to prevent sending StreamOptions/include_usage to Qiniu's API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a75ab5aa-124a-48e2-b342-39d5094ca018

📥 Commits

Reviewing files that changed from the base of the PR and between 8c8661d and dc39c3e.

📒 Files selected for processing (8)
  • common/api_type.go
  • constant/channel.go
  • controller/channel_upstream_update_qiniu_test.go
  • relay/channel/openai/adaptor.go
  • relay/common/relay_info.go
  • web/src/components/table/channels/modals/EditChannelModal.jsx
  • web/src/constants/channel.constants.js
  • web/src/helpers/render.jsx

Comment on lines +17 to +25
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)
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Fatal*/FailNow usage in httptest handler contexts (read-only check).
rg -n --type=go -C4 'httptest\.NewServer\(http\.HandlerFunc\(func' controller/channel_upstream_update_qiniu_test.go
rg -n --type=go -C3 '\bt\.(Fatal|Fatalf|FailNow)\(' controller/channel_upstream_update_qiniu_test.go

Repository: QuantumNous/new-api

Length of output: 1291


🏁 Script executed:

#!/bin/bash
# Search for other httptest.NewServer patterns in test files to identify if this is systemic
rg -n --type=go 'httptest\.NewServer\(http\.HandlerFunc' --glob='*_test.go' | head -20
# Check how many test files use this pattern
rg -l --type=go 'httptest\.NewServer\(http\.HandlerFunc' --glob='*_test.go' | wc -l

Repository: QuantumNous/new-api

Length of output: 207


Avoid t.Fatalf inside the HTTP handler goroutine.

At lines 18, 21, and 24, t.Fatalf is invoked from the server handler goroutine. Fatalf/FailNow are only safe from the test goroutine; calling them from other goroutines can lead to unreliable test failures. Capture request fields in the handler and assert them after fetchChannelUpstreamModelIDs returns.

Suggested fix
 func TestFetchChannelUpstreamModelIDs_Qiniu(t *testing.T) {
 	t.Parallel()
 
+	type requestSnapshot struct {
+		method string
+		path   string
+		auth   string
+	}
+	reqCh := make(chan requestSnapshot, 1)
+
 	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)
-		}
+		reqCh <- requestSnapshot{
+			method: r.Method,
+			path:   r.URL.Path,
+			auth:   r.Header.Get("Authorization"),
+		}
 		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()
@@
 	got, err := fetchChannelUpstreamModelIDs(ch)
 	if err != nil {
 		t.Fatalf("fetchChannelUpstreamModelIDs returned error: %v", err)
 	}
+	req := <-reqCh
+	if req.method != http.MethodGet {
+		t.Fatalf("unexpected method: %s", req.method)
+	}
+	if req.path != "/v1/models" {
+		t.Fatalf("unexpected path: %s", req.path)
+	}
+	if req.auth != "Bearer test-key" {
+		t.Fatalf("unexpected Authorization header: %q", req.auth)
+	}
 	if len(got) != 2 || got[0] != "deepseek/deepseek-v3.1-terminus-thinking" || got[1] != "gpt-4" {
 		t.Fatalf("unexpected models: %#v", got)
 	}
 }
🤖 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 17 - 25, The
handler in channel_upstream_update_qiniu_test.go is calling t.Fatalf from the
server goroutine (lines checking r.Method, r.URL.Path, and Authorization);
instead, capture the observed values (method, path, auth header, etc.) into
local variables or send them over a channel/sync struct from the handler, return
a normal HTTP response, then after invoking fetchChannelUpstreamModelIDs in the
test goroutine assert those captured values with t.Fatalf/require. Locate the
inline HTTP handler in the test and change its t.Fatalf calls to store values
(or send them on a channel) and perform the assertions in the test goroutine
after fetchChannelUpstreamModelIDs returns.

Comment thread relay/common/relay_info.go Outdated
// 定义支持流式选项的通道类型
var streamSupportedChannels = map[int]bool{
constant.ChannelTypeOpenAI: true,
constant.ChannelTypeQiniu: true,
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:

Qiniu (七牛) OpenAI-compatible chat/completions API: does streaming support stream_options(especiallyinclude_usage)?

💡 Result:

No, Qiniu's OpenAI-compatible chat/completions API does not support the stream_options parameter (including include_usage) for streaming responses. Official documentation lists stream as supported but omits stream_options. OpenAPI specs and parameter tables confirm standard OpenAI parameters like model, messages, max_tokens, temperature are included, but stream_options is absent. Examples show streaming with stream: true without usage in final chunk. Separate usage query APIs exist (/v2/stat/usage). Some providers reject stream_options, causing 422 errors (similar to Mistral issues).

Citations:


Remove Qiniu from streamSupportedChannels—it does not support the stream_options parameter.

Qiniu's OpenAI-compatible API supports stream but explicitly omits stream_options (including include_usage). Including Qiniu at line 308 will cause upstream failures when StreamOptions are passed. Separate usage APIs exist instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/relay_info.go` at line 308, Remove constant.ChannelTypeQiniu
from the streamSupportedChannels map so Qiniu isn't treated as supporting
stream_options; locate the map named streamSupportedChannels and delete the
entry "constant.ChannelTypeQiniu: true" (or conditionally exclude
ChannelTypeQiniu wherever streamSupportedChannels is used) to prevent sending
StreamOptions/include_usage to Qiniu's API.

@liangchaoboy liangchaoboy force-pushed the feat/qiniu-provider branch 3 times, most recently from b8002ff to 89e5896 Compare April 15, 2026 11:40
Vincent added 2 commits April 15, 2026 19:43
Introduce Qiniu (OpenAI-compatible) channel type, enable upstream /v1/models fetching in UI, and add a unit test for model listing.

Made-with: Cursor
Replace hardcoded OpenAI/Azure channel type check with the
SupportStreamOptions flag so all channels registered in
streamSupportedChannels (including Qiniu) correctly preserve
StreamOptions in upstream requests.
@liangchaoboy liangchaoboy changed the title Feat/qiniu provider Support Qiniu provider with OpenAI API compatibility Apr 16, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)

133-136: Consolidate fetchable-type source of truth.

Line 133 redefines a fetchable-type set that is already centralized in MODEL_FETCHABLE_CHANNEL_TYPES. Keeping both lists invites future drift when new types are added. Prefer reusing the shared constant directly.

♻️ Suggested simplification
-const MODEL_FETCHABLE_TYPES = new Set([
-  1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
-  58,
-]);
+const MODEL_FETCHABLE_TYPES = MODEL_FETCHABLE_CHANNEL_TYPES;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/table/channels/modals/EditChannelModal.jsx` around lines
133 - 136, The local constant MODEL_FETCHABLE_TYPES in EditChannelModal.jsx
duplicates the centralized MODEL_FETCHABLE_CHANNEL_TYPES and should be removed;
replace the local definition by importing and using
MODEL_FETCHABLE_CHANNEL_TYPES (the shared source of truth) wherever
MODEL_FETCHABLE_TYPES is referenced (e.g., in validation or conditional checks
inside the EditChannelModal component), and ensure you add the appropriate
import for MODEL_FETCHABLE_CHANNEL_TYPES from its module and update any variable
name references to use the imported symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/components/table/channels/modals/EditChannelModal.jsx`:
- Around line 133-136: The local constant MODEL_FETCHABLE_TYPES in
EditChannelModal.jsx duplicates the centralized MODEL_FETCHABLE_CHANNEL_TYPES
and should be removed; replace the local definition by importing and using
MODEL_FETCHABLE_CHANNEL_TYPES (the shared source of truth) wherever
MODEL_FETCHABLE_TYPES is referenced (e.g., in validation or conditional checks
inside the EditChannelModal component), and ensure you add the appropriate
import for MODEL_FETCHABLE_CHANNEL_TYPES from its module and update any variable
name references to use the imported symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97a36106-6b29-4e81-8319-1f9324de2a6b

📥 Commits

Reviewing files that changed from the base of the PR and between 776e423 and 11fe2c7.

📒 Files selected for processing (8)
  • common/api_type.go
  • constant/channel.go
  • controller/channel_upstream_update_qiniu_test.go
  • relay/channel/openai/adaptor.go
  • relay/common/relay_info.go
  • web/src/components/table/channels/modals/EditChannelModal.jsx
  • web/src/constants/channel.constants.js
  • web/src/helpers/render.jsx
✅ Files skipped from review due to trivial changes (1)
  • web/src/constants/channel.constants.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • common/api_type.go
  • web/src/helpers/render.jsx
  • controller/channel_upstream_update_qiniu_test.go
  • relay/channel/openai/adaptor.go
  • relay/common/relay_info.go
  • constant/channel.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant