From 5ef628396fca0fbefc7d4aba30602d232d44c4ab Mon Sep 17 00:00:00 2001 From: Seven <88984809@qq.com> Date: Tue, 15 Sep 2026 19:19:32 +0800 Subject: [PATCH] feat: add DaoXE as an OpenAI-compatible model provider (#19560) Adds DaoXE (https://daoxe.com) as an OpenAI-compatible model provider Signed-off-by: seven7763 --- conf/llm_factories.json | 8 +++ conf/models/daoxe.json | 13 +++++ docs/guides/models/supported_models.mdx | 1 + internal/entity/models/daoxe.go | 43 ++++++++++++++++ internal/entity/models/daoxe_test.go | 68 +++++++++++++++++++++++++ internal/entity/models/factory.go | 2 + rag/llm/chat_model.py | 18 +++++++ rag/llm/embedding_model.py | 12 +++++ rag/llm/model_meta.py | 12 +++++ web/src/assets/svg/llm/daoxe.svg | 3 ++ web/src/components/svg-icon.tsx | 1 + web/src/constants/llm.ts | 3 ++ 12 files changed, 184 insertions(+) create mode 100644 conf/models/daoxe.json create mode 100644 internal/entity/models/daoxe.go create mode 100644 internal/entity/models/daoxe_test.go create mode 100644 web/src/assets/svg/llm/daoxe.svg diff --git a/conf/llm_factories.json b/conf/llm_factories.json index 764a3ce552..172049ab65 100644 --- a/conf/llm_factories.json +++ b/conf/llm_factories.json @@ -2047,6 +2047,14 @@ "status": "1", "llm": [] }, + { + "name": "DaoXE", + "logo": "", + "tags": "LLM,TEXT EMBEDDING", + "status": "1", + "llm": [], + "rank": "885" + }, { "name": "DeepSeek", "logo": "", diff --git a/conf/models/daoxe.json b/conf/models/daoxe.json new file mode 100644 index 0000000000..86607433d4 --- /dev/null +++ b/conf/models/daoxe.json @@ -0,0 +1,13 @@ +{ + "name": "DaoXE", + "url": { + "default": "https://api.daoxe.com/v1" + }, + "url_suffix": { + "chat": "chat/completions", + "models": "models", + "embedding": "embeddings" + }, + "class": "daoxe", + "models": [] +} diff --git a/docs/guides/models/supported_models.mdx b/docs/guides/models/supported_models.mdx index b34d5e2c65..ee2cb9903f 100644 --- a/docs/guides/models/supported_models.mdx +++ b/docs/guides/models/supported_models.mdx @@ -33,6 +33,7 @@ A complete list of model providers supported by RAGFlow, which will continue to | Bedrock | `https://aws.amazon.com/bedrock/` | | Cohere | `https://cohere.com` | | CometAPI | `https://cometapi.com` | +| DaoXE | `https://daoxe.com` | | DeepInfra | `https://deepinfra.com` | | DeepSeek | `https://www.deepseek.com` | | DeerAPI | `https://deerapi.com` | diff --git a/internal/entity/models/daoxe.go b/internal/entity/models/daoxe.go new file mode 100644 index 0000000000..65628fc2d6 --- /dev/null +++ b/internal/entity/models/daoxe.go @@ -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 + +// DaoxeModel implements DaoXE's OpenAI-compatible chat and model-list APIs. +// DaoXE speaks the OpenAI wire format without deviation, so the embedded +// OpenAIAPICompatibleModel covers every call and nothing is overridden beyond +// the driver's own identity. The live catalog is account-scoped, so model +// listing reads GET /v1/models from the gateway instead of a static list. +type DaoxeModel struct { + *OpenAIAPICompatibleModel +} + +// NewDaoxeModel creates a DaoXE model driver. +func NewDaoxeModel(baseURL map[string]string, urlSuffix URLSuffix) *DaoxeModel { + return &DaoxeModel{ + OpenAIAPICompatibleModel: NewOpenAIAPICompatibleModel(baseURL, urlSuffix), + } +} + +// Name returns the model driver name. +func (m *DaoxeModel) Name() string { + return "DaoXE" +} + +// NewInstance creates a new DaoXE driver bound to the given base URL. +func (m *DaoxeModel) NewInstance(baseURL map[string]string) ModelDriver { + return NewDaoxeModel(baseURL, m.baseModel.URLSuffix) +} diff --git a/internal/entity/models/daoxe_test.go b/internal/entity/models/daoxe_test.go new file mode 100644 index 0000000000..4b30c7248c --- /dev/null +++ b/internal/entity/models/daoxe_test.go @@ -0,0 +1,68 @@ +package models + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "ragflow/internal/common" + "testing" +) + +func newDaoxeForTest(baseURL string) *DaoxeModel { + return NewDaoxeModel( + map[string]string{"default": baseURL}, + URLSuffix{Chat: "chat/completions", Models: "models", Embedding: "embeddings"}, + ) +} + +func TestDaoxeName(t *testing.T) { + if got := newDaoxeForTest("http://unused").Name(); got != "DaoXE" { + t.Errorf("Name()=%q", got) + } +} + +func TestDaoxeFactory(t *testing.T) { + driver, err := NewModelFactory().CreateModelDriver("daoxe", map[string]string{"default": "http://unused"}, URLSuffix{}) + if err != nil { + t.Fatalf("CreateModelDriver: %v", err) + } + if _, ok := driver.(*DaoxeModel); !ok { + t.Fatalf("driver type=%T, want *DaoxeModel", driver) + } + if _, ok := driver.NewInstance(map[string]string{"default": "http://other"}).(*DaoxeModel); !ok { + t.Fatal("NewInstance did not return *DaoxeModel") + } +} + +// DaoXE is a hosted gateway, so unlike a local server it must receive the +// tenant's key as a bearer token on every call. +func TestDaoxeChatSendsAPIKey(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, want := r.Header.Get("Authorization"), "Bearer "+testAPIKey; got != want { + t.Errorf("Authorization=%q, want %q", got, want) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "chat-daoxe", + "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 := newDaoxeForTest(srv.URL).ChatWithMessages( + t.Context(), "gpt-4o", []Message{{Role: "user", Content: "ping"}}, + &APIConfig{ApiKey: &testAPIKey}, 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) +} diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index 537348318c..d7e0fc9f59 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -87,6 +87,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewCoHereModel(baseURL, urlSuffix), nil case "cometapi": return NewCometAPIModel(baseURL, urlSuffix), nil + case "daoxe": + return NewDaoxeModel(baseURL, urlSuffix), nil case "fish audio": return NewFishAudioModel(baseURL, urlSuffix), nil case "mistral": diff --git a/rag/llm/chat_model.py b/rag/llm/chat_model.py index 4ff504ffb3..a9ec216f47 100644 --- a/rag/llm/chat_model.py +++ b/rag/llm/chat_model.py @@ -3015,6 +3015,24 @@ class RAGconChat(Base): super().__init__(key, model_name, base_url, **kwargs) +class DaoXEChat(Base): + """DaoXE OpenAI-compatible chat adapter. + + DaoXE is a hosted multi-model gateway speaking the OpenAI wire format, so + the standard OpenAI client path covers every call. The base URL is + tenant-configurable because DaoXE is also self-hostable behind custom + domains. + """ + + _FACTORY_NAME = "DaoXE" + + def __init__(self, key, model_name, base_url, **kwargs): + if not base_url: + raise ValueError("url cannot be None") + model_name = model_name.split("___")[0] + super().__init__(key, model_name, base_url, **kwargs) + + class NewAPIChat(Base): _FACTORY_NAME = "New API" diff --git a/rag/llm/embedding_model.py b/rag/llm/embedding_model.py index 0399f2ad11..c41cb31d78 100644 --- a/rag/llm/embedding_model.py +++ b/rag/llm/embedding_model.py @@ -1491,6 +1491,18 @@ class PerplexityEmbed(Base): return np.array(embds[0]), cnt +class DaoXEEmbed(OpenAIEmbed): + """DaoXE OpenAI-compatible embeddings.""" + + _FACTORY_NAME = "DaoXE" + + def __init__(self, key, model_name, base_url): + if not base_url: + raise ValueError("url cannot be None") + self.client = OpenAI(api_key=key, base_url=base_url) + self.model_name = model_name.split("___")[0] + + class NewAPIEmbed(OpenAIEmbed): _FACTORY_NAME = "New API" diff --git a/rag/llm/model_meta.py b/rag/llm/model_meta.py index bf93bd6184..00e7109007 100644 --- a/rag/llm/model_meta.py +++ b/rag/llm/model_meta.py @@ -1187,6 +1187,18 @@ class ApiRoute(OpenAIAPICompatible): return f"{self._BASE_URL}/models" +class DaoXE(OpenAIAPICompatible): + """DaoXE catalog lister. + + The live catalog is account-scoped and changes over time, so the list is + read from the gateway's own ``/v1/models`` endpoint (inherited behavior) + rather than pinned in ``conf/models/daoxe.json``, which stays empty on + purpose. + """ + + _FACTORY_NAME = "DaoXE" + + class NewAPI(OpenAIAPICompatible): _FACTORY_NAME = "New API" diff --git a/web/src/assets/svg/llm/daoxe.svg b/web/src/assets/svg/llm/daoxe.svg new file mode 100644 index 0000000000..3638a28adc --- /dev/null +++ b/web/src/assets/svg/llm/daoxe.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/components/svg-icon.tsx b/web/src/components/svg-icon.tsx index 71d40ae91c..7763c886ec 100644 --- a/web/src/components/svg-icon.tsx +++ b/web/src/components/svg-icon.tsx @@ -95,6 +95,7 @@ const svgIcons = [ LLMFactory.RAGcon, LLMFactory.SoMark, LLMFactory.NewAPI, + LLMFactory.DaoXE, LLMFactory.Astraflow, LLMFactory.AstraflowCN, LLMFactory.FuturMix, diff --git a/web/src/constants/llm.ts b/web/src/constants/llm.ts index dff4e24e1a..367797cafe 100644 --- a/web/src/constants/llm.ts +++ b/web/src/constants/llm.ts @@ -103,6 +103,7 @@ export enum LLMFactory { Qiniu = 'Qiniu', TokenHub = 'TokenHub', NewAPI = 'New API', + DaoXE = 'DaoXE', FunASR = 'FunASR', AIMLAPI = 'aimlapi.com', GreenPT = 'GreenPT', @@ -193,6 +194,7 @@ export const IconMap = { [LLMFactory.TokenHub]: 'tokenhub', [LLMFactory.SoMark]: 'somark', [LLMFactory.NewAPI]: 'new-api', + [LLMFactory.DaoXE]: 'daoxe', [LLMFactory.FunASR]: 'funasr', [LLMFactory.AIMLAPI]: 'aimlapi', [LLMFactory.GreenPT]: 'greenpt', @@ -225,6 +227,7 @@ export const APIMapUrl = { [LLMFactory.GreenPT]: 'https://greenpt.ai', [LLMFactory.Synthorai]: 'https://synthorai.io', [LLMFactory.ApiRoute]: 'https://www.api-route.com', + [LLMFactory.DaoXE]: 'https://daoxe.com', [LLMFactory.MWS]: 'https://mws.ru/docs/cloud-platform/gpt/general/inference-text.html', [LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys',