feat(models): add New API model driver (#21226)

This commit is contained in:
Tim Wang authored and GitHub committed 2026-10-21 15:28:40 +08:00
1 parent 572b00d546
commit aae7f15080
4 files changed
+201

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
{
"name": "New API",
"url_suffix": {
"chat": "chat/completions",
"models": "models",
"embedding": "embeddings",
"rerank": "rerank",
"tts": "audio/speech",
"asr": "audio/transcriptions"
},
"url_hint": "http://host.docker.internal:3000/v1",
"class": "local"
}
+2
View File
@@ -194,6 +194,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string
return NewOpperModel(baseURL, urlSuffix), nil
case "empiriolabs ai":
return NewEmpirioLabsModel(baseURL, urlSuffix), nil
case "new api":
return NewNewAPIModel(baseURL, urlSuffix), nil
default:
return NewDummyModel(baseURL, urlSuffix), nil
}
+43
View File
@@ -0,0 +1,43 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package models
// NewAPIModel drives New API (https://github.com/QuantumNous/new-api) and
// compatible aggregation gateways such as one-api. These gateways expose every
// upstream model behind a single OpenAI-compatible endpoint, so the embedded
// OpenAIAPICompatibleModel covers chat, embedding, rerank, TTS and ASR calls as
// well as dynamic model discovery via /models.
type NewAPIModel struct {
*OpenAIAPICompatibleModel
}
// NewNewAPIModel creates a New API driver.
func NewNewAPIModel(baseURL map[string]string, urlSuffix URLSuffix) *NewAPIModel {
return &NewAPIModel{
OpenAIAPICompatibleModel: NewOpenAIAPICompatibleModel(baseURL, urlSuffix),
}
}
// Name returns the driver name.
func (m *NewAPIModel) Name() string {
return "New API"
}
// NewInstance returns a driver bound to the given base URL.
func (m *NewAPIModel) NewInstance(baseURL map[string]string) ModelDriver {
return NewNewAPIModel(baseURL, m.baseModel.URLSuffix)
}
+143
View File
@@ -0,0 +1,143 @@
package models
import (
"encoding/json"
"net/http"
"net/http/httptest"
"ragflow/internal/common"
"testing"
)
var newAPITestAPIKey = "sk-newapi-test"
func newNewAPIForTest(baseURL string) *NewAPIModel {
return NewNewAPIModel(
map[string]string{"default": baseURL},
URLSuffix{Chat: "chat/completions", Models: "models", Embedding: "embeddings", Rerank: "rerank"},
)
}
func TestNewAPIName(t *testing.T) {
if got := newNewAPIForTest("http://unused").Name(); got != "New API" {
t.Errorf("Name()=%q", got)
}
}
func TestNewAPIFactory(t *testing.T) {
driver, err := NewModelFactory().CreateModelDriver("new api", map[string]string{"default": "http://unused"}, URLSuffix{})
if err != nil {
t.Fatalf("CreateModelDriver: %v", err)
}
if _, ok := driver.(*NewAPIModel); !ok {
t.Fatalf("driver type=%T, want *NewAPIModel", driver)
}
if _, ok := driver.NewInstance(map[string]string{"default": "http://other"}).(*NewAPIModel); !ok {
t.Fatal("NewInstance did not return *NewAPIModel")
}
}
// New API is an aggregation gateway, so it must receive the tenant's key as a
// bearer token on every call.
func TestNewAPIChatSendsAPIKey(t *testing.T) {
withSSRFBypass(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/chat/completions" {
t.Errorf("path=%s", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer sk-newapi-test" {
t.Errorf("Authorization=%q", got)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "chat-newapi",
"choices": []map[string]any{{"message": map[string]any{"content": "pong"}}},
"usage": map[string]any{"prompt_tokens": 3, "completion_tokens": 5, "total_tokens": 8},
})
}))
defer srv.Close()
usage := &common.ModelUsage{}
resp, err := newNewAPIForTest(srv.URL).ChatWithMessages(
t.Context(),
"gpt-4o",
[]Message{{Role: "user", Content: "ping"}},
&APIConfig{ApiKey: &newAPITestAPIKey},
nil,
usage,
)
if err != nil {
t.Fatalf("ChatWithMessages: %v", err)
}
if *resp.Answer != "pong" {
t.Errorf("Answer=%q", *resp.Answer)
}
assertModelUsage(t, usage, 3, 5, 8)
}
// New API exposes whatever models its upstreams relay, so RAGFlow discovers them
// dynamically through /models rather than from a shipped catalogue.
func TestNewAPIListModelsAndCheckConnection(t *testing.T) {
withSSRFBypass(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/models" {
t.Errorf("%s %s", r.Method, r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer sk-newapi-test" {
t.Errorf("Authorization=%q", got)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]any{
{"id": "gpt-4o"},
{"id": "text-embedding-3-large"},
},
})
}))
defer srv.Close()
model := newNewAPIForTest(srv.URL)
list, err := model.ListModels(t.Context(), &APIConfig{ApiKey: &newAPITestAPIKey})
if err != nil {
t.Fatalf("ListModels: %v", err)
}
if joinModelNames(list, ",") != "gpt-4o,text-embedding-3-large" {
t.Errorf("models=%v", list)
}
if err := model.CheckConnection(t.Context(), &APIConfig{ApiKey: &newAPITestAPIKey}); err != nil {
t.Fatalf("CheckConnection: %v", err)
}
}
// The shipped config must resolve to the New API driver and expose the OpenAI
// endpoint suffixes for every model type the gateway relays. Unlike a hosted
// provider with a fixed catalogue, New API ships no static models and advertises
// a url_hint so the operator points it at their own gateway.
func TestNewAPIProviderConfig(t *testing.T) {
dir, restore := setupProviderTestDir(t, "new-api.json")
defer restore()
if err := InitProviderManager(dir); err != nil {
t.Fatalf("InitProviderManager: %v", err)
}
provider := GetProviderManager().FindProvider("New API")
if provider == nil {
t.Fatal("New API provider not found")
}
if _, ok := provider.ModelDriver.(*NewAPIModel); !ok {
t.Fatalf("ModelDriver=%T, want *models.NewAPIModel", provider.ModelDriver)
}
if provider.URLSuffix.Chat != "chat/completions" ||
provider.URLSuffix.Models != "models" ||
provider.URLSuffix.Embedding != "embeddings" ||
provider.URLSuffix.Rerank != "rerank" {
t.Errorf("URLSuffix=%+v", provider.URLSuffix)
}
if provider.URLSuffix.TTS != "audio/speech" || provider.URLSuffix.ASR != "audio/transcriptions" {
t.Errorf("audio URLSuffix=%+v", provider.URLSuffix)
}
if provider.URLHint == "" {
t.Error("New API should advertise a url_hint for the operator's gateway")
}
if len(provider.Models) != 0 {
t.Errorf("New API ships %d static models, want 0 (dynamic discovery)", len(provider.Models))
}
}