diff --git a/common/api_type.go b/common/api_type.go
index 44841e11b8fc..3d87153208ae 100644
--- a/common/api_type.go
+++ b/common/api_type.go
@@ -81,6 +81,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeSub2API
case constant.ChannelTypeNewAPI:
apiType = constant.APITypeNewAPI
+ case constant.ChannelTypeNadir:
+ apiType = constant.APITypeNadir
}
if apiType == -1 {
return constant.APITypeOpenAI, false
diff --git a/common/endpoint_type.go b/common/endpoint_type.go
index 126df3c8e761..e120705435c0 100644
--- a/common/endpoint_type.go
+++ b/common/endpoint_type.go
@@ -24,7 +24,7 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
fallthrough
case constant.ChannelTypeGemini:
endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI}
- case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点
+ case constant.ChannelTypeOpenRouter, constant.ChannelTypeNadir: // OpenRouter / Nadir 只支持 OpenAI 端点
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
case constant.ChannelTypeXai:
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse}
diff --git a/constant/api_type.go b/constant/api_type.go
index 2a561c6d2bfd..fd3512cc67dc 100644
--- a/constant/api_type.go
+++ b/constant/api_type.go
@@ -39,5 +39,6 @@ const (
APITypeAdvancedCustom
APITypeSub2API
APITypeNewAPI
+ APITypeNadir
APITypeDummy // this one is only for count, do not add any channel after this
)
diff --git a/constant/channel.go b/constant/channel.go
index 2a6c4a31c138..c071a006cc1b 100644
--- a/constant/channel.go
+++ b/constant/channel.go
@@ -58,6 +58,7 @@ const (
ChannelTypeAdvancedCustom = 58
ChannelTypeSub2API = 59
ChannelTypeNewAPI = 60
+ ChannelTypeNadir = 61
ChannelTypeDummy // this one is only for count, do not add any channel after this
)
@@ -124,6 +125,7 @@ var ChannelBaseURLs = []string{
"", //58
"", //59
"", //60
+ "https://api.getnadir.com", //61
}
var ChannelTypeNames = map[int]string{
@@ -184,6 +186,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeAdvancedCustom: "Advanced Custom",
ChannelTypeSub2API: "Sub2API",
ChannelTypeNewAPI: "New API",
+ ChannelTypeNadir: "Nadir",
}
func GetChannelTypeName(channelType int) string {
diff --git a/controller/model_owned_by_test.go b/controller/model_owned_by_test.go
index bc2ef32f135c..d79520fe1b37 100644
--- a/controller/model_owned_by_test.go
+++ b/controller/model_owned_by_test.go
@@ -31,6 +31,11 @@ func TestChannelOwnerNameUsesAdaptorChannelName(t *testing.T) {
channelType: constant.ChannelTypeOpenRouter,
expected: "openrouter",
},
+ {
+ name: "nadir",
+ channelType: constant.ChannelTypeNadir,
+ expected: "nadir",
+ },
{
name: "azure fallback",
channelType: constant.ChannelTypeAzure,
diff --git a/relay/channel/nadir/constant.go b/relay/channel/nadir/constant.go
new file mode 100644
index 000000000000..fc4088481b0f
--- /dev/null
+++ b/relay/channel/nadir/constant.go
@@ -0,0 +1,14 @@
+package nadir
+
+// ModelList is the set of model ids this channel exposes.
+//
+// Nadir is an OpenAI-compatible router: send "auto" and it classifies the
+// prompt, then routes to the cheapest model that clears its quality bar. The
+// response `model` field reports the model it actually routed to. Only "auto"
+// is listed, because pinning a concrete model here would bypass the routing
+// that is the whole point of the channel.
+var ModelList = []string{"auto"}
+
+// ChannelName is the adaptor name reported for this channel, and is what
+// surfaces as the model owner in /v1/models responses.
+var ChannelName = "nadir"
diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go
index 4f1c42863dba..9c1647bcb378 100644
--- a/relay/channel/openai/adaptor.go
+++ b/relay/channel/openai/adaptor.go
@@ -19,6 +19,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/ai360"
"github.com/QuantumNous/new-api/relay/channel/lingyiwanwu"
+ "github.com/QuantumNous/new-api/relay/channel/nadir"
"github.com/QuantumNous/new-api/relaykit/dto"
//"github.com/QuantumNous/new-api/relay/channel/minimax"
@@ -680,6 +681,8 @@ func (a *Adaptor) GetModelList() []string {
return xinference.ModelList
case constant.ChannelTypeOpenRouter:
return openrouter.ModelList
+ case constant.ChannelTypeNadir:
+ return nadir.ModelList
default:
return ModelList
}
@@ -697,6 +700,8 @@ func (a *Adaptor) GetChannelName() string {
return xinference.ChannelName
case constant.ChannelTypeOpenRouter:
return openrouter.ChannelName
+ case constant.ChannelTypeNadir:
+ return nadir.ChannelName
default:
return ChannelName
}
diff --git a/relay/channel/openai/nadir_test.go b/relay/channel/openai/nadir_test.go
new file mode 100644
index 000000000000..e183897a909d
--- /dev/null
+++ b/relay/channel/openai/nadir_test.go
@@ -0,0 +1,72 @@
+package openai
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/QuantumNous/new-api/common"
+ "github.com/QuantumNous/new-api/constant"
+ relaycommon "github.com/QuantumNous/new-api/relay/common"
+ relayconstant "github.com/QuantumNous/new-api/relay/constant"
+ "github.com/QuantumNous/new-api/relaykit/types"
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// nadirRelayInfo builds the minimal RelayInfo a Nadir request carries, so the
+// tests below exercise URL construction and header setup the same way the
+// relay does at runtime.
+func nadirRelayInfo() *relaycommon.RelayInfo {
+ return &relaycommon.RelayInfo{
+ RelayFormat: types.RelayFormatOpenAI,
+ RelayMode: relayconstant.RelayModeChatCompletions,
+ RequestURLPath: "/v1/chat/completions",
+ OriginModelName: "auto",
+ ChannelMeta: &relaycommon.ChannelMeta{
+ ApiKey: "sk-test",
+ ChannelBaseUrl: constant.ChannelBaseURLs[constant.ChannelTypeNadir],
+ ChannelType: constant.ChannelTypeNadir,
+ UpstreamModelName: "auto",
+ },
+ }
+}
+
+// TestNadirChannelRelaysToOpenAIChatCompletionsWithBearerAuth pins the wire
+// contract: the request goes to /v1/chat/completions and carries the key
+// as a Bearer token, which is what lets Nadir reuse the shared OpenAI adaptor
+// with no request translation.
+func TestNadirChannelRelaysToOpenAIChatCompletionsWithBearerAuth(t *testing.T) {
+ adaptor := &Adaptor{}
+ info := nadirRelayInfo()
+ adaptor.Init(info)
+
+ requestURL, err := adaptor.GetRequestURL(info)
+ require.NoError(t, err)
+ assert.Equal(t, "https://api.getnadir.com/v1/chat/completions", requestURL)
+
+ gin.SetMode(gin.TestMode)
+ c, _ := gin.CreateTestContext(httptest.NewRecorder())
+ c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
+ header := http.Header{}
+
+ require.NoError(t, adaptor.SetupRequestHeader(c, &header, info))
+ assert.Equal(t, "Bearer sk-test", header.Get("Authorization"))
+}
+
+// TestNadirChannelExposesAutoModelOverOpenAIEndpointOnly pins that the channel
+// advertises exactly the "auto" model and claims only the OpenAI endpoint type.
+// Claiming more would offer callers endpoints the router does not serve.
+func TestNadirChannelExposesAutoModelOverOpenAIEndpointOnly(t *testing.T) {
+ adaptor := &Adaptor{}
+ adaptor.Init(nadirRelayInfo())
+
+ assert.Equal(t, "nadir", adaptor.GetChannelName())
+ assert.Equal(t, []string{"auto"}, adaptor.GetModelList())
+ assert.Equal(
+ t,
+ []constant.EndpointType{constant.EndpointTypeOpenAI},
+ common.GetEndpointTypesByChannelType(constant.ChannelTypeNadir, "auto"),
+ )
+}
diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go
index e6298dc034f3..3997d895a362 100644
--- a/relay/relay_adaptor.go
+++ b/relay/relay_adaptor.go
@@ -129,6 +129,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &sub2api.Adaptor{}
case constant.APITypeNewAPI:
return &newapi.Adaptor{}
+ case constant.APITypeNadir:
+ return &openai.Adaptor{}
}
return nil
}
diff --git a/web/scripts/sync-i18n.mjs b/web/scripts/sync-i18n.mjs
index 51dd38974dae..0e5e1b415001 100644
--- a/web/scripts/sync-i18n.mjs
+++ b/web/scripts/sync-i18n.mjs
@@ -64,6 +64,7 @@ const BRAND_AND_LITERAL_KEYS = new Set([
'Mistral',
'MokaAI',
'Moonshot',
+ 'Nadir',
'New API',
'New API <noreply@example.com>',
'NewAPI',
diff --git a/web/src/features/channels/constants.ts b/web/src/features/channels/constants.ts
index a3cb726d5418..e11a0a28674a 100644
--- a/web/src/features/channels/constants.ts
+++ b/web/src/features/channels/constants.ts
@@ -81,12 +81,13 @@ export const CHANNEL_TYPES = {
58: 'Advanced Custom',
59: 'Sub2API',
60: 'New API',
+ 61: 'Nadir',
} as const
const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [
- 1, 14, 33, 24, 43, 3, 41, 48, 60, 58, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15,
- 46, 23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 59, 22, 21, 44, 2,
- 5, 36, 50, 51, 52, 53, 54, 55, 56,
+ 1, 14, 33, 24, 43, 3, 41, 48, 60, 58, 42, 34, 20, 61, 4, 40, 27, 25, 17, 26,
+ 15, 46, 23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 59, 22, 21, 44,
+ 2, 5, 36, 50, 51, 52, 53, 54, 55, 56,
]
export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => {
diff --git a/web/src/features/channels/lib/channel-type-config.ts b/web/src/features/channels/lib/channel-type-config.ts
index 8a05b86ee449..341ddb7f8218 100644
--- a/web/src/features/channels/lib/channel-type-config.ts
+++ b/web/src/features/channels/lib/channel-type-config.ts
@@ -123,6 +123,16 @@ export const CHANNEL_TYPE_CONFIGS: Record = {
models: 'Use model IDs from OpenRouter',
},
},
+ 61: {
+ id: 61,
+ name: CHANNEL_TYPES[61],
+ icon: 'OpenAI',
+ defaultBaseUrl: 'https://api.getnadir.com',
+ hints: {
+ key: 'Nadir API Key',
+ models: 'Use "auto" to let Nadir pick the model',
+ },
+ },
56: {
id: 56,
name: CHANNEL_TYPES[56],
diff --git a/web/src/features/channels/lib/channel-utils.ts b/web/src/features/channels/lib/channel-utils.ts
index 9424a8521b6f..e5d60d245e2a 100644
--- a/web/src/features/channels/lib/channel-utils.ts
+++ b/web/src/features/channels/lib/channel-utils.ts
@@ -93,6 +93,7 @@ export function getChannelTypeIcon(type: number): string {
40: 'SiliconCloud', // SiliconFlow
44: 'OpenAI', // MokaAI
20: 'OpenRouter', // OpenRouter
+ 61: 'OpenAI', // Nadir (OpenAI-compatible router, no dedicated icon)
// Image/Video generation
2: 'Midjourney', // MjProxy
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 1d85923ad139..89e3e09a4bee 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL is a production-ready relational database. Keep your credentials secure.",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.",
"N/A": "N/A",
+ "Nadir": "Nadir",
"Name": "Name",
"Name *": "Name *",
"Name for this redemption code (1-20 characters)": "Name for this redemption code (1-20 characters)",
diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json
index c83ffc5dae25..825611f25d37 100644
--- a/web/src/i18n/locales/fr.json
+++ b/web/src/i18n/locales/fr.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL est une base de données relationnelle prête pour la production. Gardez vos identifiants en sécurité.",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQL est prêt pour la production. Assurez-vous que des sauvegardes automatisées et un utilisateur dédié avec les privilèges minimaux requis sont configurés.",
"N/A": "N/A",
+ "Nadir": "Nadir",
"Name": "Nom",
"Name *": "Nom *",
"Name for this redemption code (1-20 characters)": "Nom pour ce code d'échange (1-20 caractères)",
diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json
index 82a5aec82b10..beb19eefa180 100644
--- a/web/src/i18n/locales/ja.json
+++ b/web/src/i18n/locales/ja.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL は本番環境対応のリレーショナルデータベースです。認証情報を安全に管理してください。",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQLは本番環境に対応しています。自動バックアップと、最小限必要な権限を持つ専用ユーザーが設定されていることを確認してください。",
"N/A": "該当なし",
+ "Nadir": "Nadir",
"Name": "名称",
"Name *": "名前 *",
"Name for this redemption code (1-20 characters)": "この引き換えコードの名前(1~20文字)",
diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json
index 07d6a0cd1109..8a8554070150 100644
--- a/web/src/i18n/locales/ru.json
+++ b/web/src/i18n/locales/ru.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL — реляционная база данных, готовая к продакшену. Храните учётные данные в безопасности.",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQL готов к работе. Убедитесь, что настроены автоматические резервные копии и выделенный пользователь с минимально необходимыми привилегиями.",
"N/A": "Н/Д",
+ "Nadir": "Nadir",
"Name": "Название",
"Name *": "Имя *",
"Name for this redemption code (1-20 characters)": "Имя для этого кода активации (1-20 символов)",
diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json
index 3c84d1205a38..fcbaad7de252 100644
--- a/web/src/i18n/locales/vi.json
+++ b/web/src/i18n/locales/vi.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL là cơ sở dữ liệu quan hệ sẵn sàng cho production. Hãy giữ thông tin đăng nhập an toàn.",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQL đã sẵn sàng để triển khai sản xuất. Đảm bảo sao lưu tự động và một người dùng chuyên dụng với các đặc quyền tối thiểu cần thiết được cấu hình.",
"N/A": "N/A",
+ "Nadir": "Nadir",
"Name": "Tên",
"Name *": "Tên *",
"Name for this redemption code (1-20 characters)": "Tên cho mã đổi quà này (1-20 ký tự)",
diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json
index d3e12b35ac1b..7ed89ade3253 100644
--- a/web/src/i18n/locales/zh-TW.json
+++ b/web/src/i18n/locales/zh-TW.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL 是生產就緒的關聯式資料庫。請確保憑證安全。",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQL 已準備好投入生產環境。請確保設定了自動備份以及具有最低所需權限的專用用戶。",
"N/A": "不適用",
+ "Nadir": "Nadir",
"Name": "名稱",
"Name *": "名稱 *",
"Name for this redemption code (1-20 characters)": "此兌換碼的名稱(1-20 個字元)",
diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json
index f81d71999bb8..58f7c6f9ebb1 100644
--- a/web/src/i18n/locales/zh.json
+++ b/web/src/i18n/locales/zh.json
@@ -2797,6 +2797,7 @@
"MySQL is a production-ready relational database. Keep your credentials secure.": "MySQL 是生产就绪的关系数据库。请确保凭据安全。",
"MySQL is production ready. Ensure automated backups and a dedicated user with the minimal required privileges are configured.": "MySQL 已准备好投入生产环境。请确保配置了自动备份以及具有最低所需权限的专用用户。",
"N/A": "不适用",
+ "Nadir": "Nadir",
"Name": "名称",
"Name *": "名称 *",
"Name for this redemption code (1-20 characters)": "此兑换码的名称(1-20 个字符)",