Skip to content
Open
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
1 change: 1 addition & 0 deletions .env-template
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ INTERNAL_KEY=<internal key for worker-to-backend authentication>
# GOOGLE_API_KEY=<your-google-api-key>
# GROQ_API_KEY=<your-groq-api-key>
# NOVITA_API_KEY=<your-novita-api-key>
# QIANFAN_API_KEY=<your-qianfan-api-key>
# OPEN_ROUTER_API_KEY=<your-openrouter-api-key>

# Remote Embeddings (Optional - for using a remote embeddings API instead of local SentenceTransformer)
Expand Down
16 changes: 16 additions & 0 deletions application/core/model_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
OPENROUTER_ATTACHMENTS = IMAGE_ATTACHMENTS

NOVITA_ATTACHMENTS = IMAGE_ATTACHMENTS
QIANFAN_ATTACHMENTS = []


OPENAI_MODELS = [
Expand Down Expand Up @@ -234,6 +235,21 @@
),
]

QIANFAN_MODELS = [
AvailableModel(
id="ernie-5.0",
provider=ModelProvider.QIANFAN,
display_name="ERNIE 5.0",
description="Baidu Qianfan flagship text chat model",
capabilities=ModelCapabilities(
supports_tools=False,
supports_structured_output=False,
supported_attachment_types=QIANFAN_ATTACHMENTS,
context_window=128000,
),
),
]


AZURE_OPENAI_MODELS = [
AvailableModel(
Expand Down
20 changes: 20 additions & 0 deletions application/core/model_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ModelProvider(str, Enum):
PREMAI = "premai"
SAGEMAKER = "sagemaker"
NOVITA = "novita"
QIANFAN = "qianfan"


@dataclass
Expand Down Expand Up @@ -118,6 +119,10 @@ def _load_models(self):
settings.LLM_PROVIDER == "novita" and settings.API_KEY
):
self._add_novita_models(settings)
if settings.QIANFAN_API_KEY or (
settings.LLM_PROVIDER == "qianfan" and settings.API_KEY
):
self._add_qianfan_models(settings)
if settings.HUGGINGFACE_API_KEY or (
settings.LLM_PROVIDER == "huggingface" and settings.API_KEY
):
Expand Down Expand Up @@ -264,6 +269,21 @@ def _add_novita_models(self, settings):
for model in NOVITA_MODELS:
self.models[model.id] = model

def _add_qianfan_models(self, settings):
from application.core.model_configs import QIANFAN_MODELS

if settings.QIANFAN_API_KEY:
for model in QIANFAN_MODELS:
self.models[model.id] = model
return
if settings.LLM_PROVIDER == "qianfan" and settings.LLM_NAME:
for model in QIANFAN_MODELS:
if model.id == settings.LLM_NAME:
self.models[model.id] = model
return
for model in QIANFAN_MODELS:
self.models[model.id] = model

def _add_docsgpt_models(self, settings):
model_id = "docsgpt-local"
model = AvailableModel(
Expand Down
1 change: 1 addition & 0 deletions application/core/model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def get_api_key_for_provider(provider: str) -> Optional[str]:
"openai": settings.OPENAI_API_KEY,
"openrouter": settings.OPEN_ROUTER_API_KEY,
"novita": settings.NOVITA_API_KEY,
"qianfan": settings.QIANFAN_API_KEY,
"anthropic": settings.ANTHROPIC_API_KEY,
"google": settings.GOOGLE_API_KEY,
"groq": settings.GROQ_API_KEY,
Expand Down
2 changes: 2 additions & 0 deletions application/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Settings(BaseSettings):
HUGGINGFACE_API_KEY: Optional[str] = None
OPEN_ROUTER_API_KEY: Optional[str] = None
NOVITA_API_KEY: Optional[str] = None
QIANFAN_API_KEY: Optional[str] = None

OPENAI_API_BASE: Optional[str] = None # azure openai api base url
OPENAI_API_VERSION: Optional[str] = None # azure openai api version
Expand Down Expand Up @@ -164,6 +165,7 @@ class Settings(BaseSettings):
"GROQ_API_KEY",
"HUGGINGFACE_API_KEY",
"NOVITA_API_KEY",
"QIANFAN_API_KEY",
"EMBEDDINGS_KEY",
"FALLBACK_LLM_API_KEY",
"QDRANT_API_KEY",
Expand Down
1 change: 1 addition & 0 deletions application/llm/handlers/handler_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class LLMHandlerCreator:
"openai": OpenAILLMHandler,
"google": GoogleLLMHandler,
"novita": OpenAILLMHandler, # Novita uses OpenAI-compatible API
"qianfan": OpenAILLMHandler, # Qianfan uses an OpenAI-compatible chat API
"default": OpenAILLMHandler,
}

Expand Down
2 changes: 2 additions & 0 deletions application/llm/llm_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from application.llm.llama_cpp import LlamaCpp
from application.llm.novita import NovitaLLM
from application.llm.openai import AzureOpenAILLM, OpenAILLM
from application.llm.qianfan import QianfanLLM
from application.llm.premai import PremAILLM
from application.llm.sagemaker import SagemakerAPILLM
from application.llm.open_router import OpenRouterLLM
Expand All @@ -26,6 +27,7 @@ class LLMCreator:
"groq": GroqLLM,
"google": GoogleLLM,
"novita": NovitaLLM,
"qianfan": QianfanLLM,
"openrouter": OpenRouterLLM,
}

Expand Down
27 changes: 27 additions & 0 deletions application/llm/qianfan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from application.core.settings import settings
from application.llm.openai import OpenAILLM

QIANFAN_BASE_URL = "https://qianfan.baidubce.com/v2"


class QianfanLLM(OpenAILLM):
def __init__(self, api_key=None, user_api_key=None, base_url=None, *args, **kwargs):
super().__init__(
api_key=api_key or settings.QIANFAN_API_KEY or settings.API_KEY,
user_api_key=user_api_key,
base_url=base_url or QIANFAN_BASE_URL,
*args,
**kwargs,
)

def get_supported_attachment_types(self):
"""Keep the first Qianfan integration text-only until attachment support is verified."""
return []

def _supports_tools(self):
"""Disable tools until Qianfan tool-calling is verified end-to-end."""
return False

def _supports_structured_output(self):
"""Disable structured output until JSON schema support is verified end-to-end."""
return False
1 change: 1 addition & 0 deletions application/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def get_gpt_model() -> str:
"anthropic": "claude-2",
"groq": "llama3-8b-8192",
"novita": "deepseek/deepseek-r1",
"qianfan": "ernie-5.0",
}
return settings.LLM_NAME or model_map.get(settings.LLM_PROVIDER, "")

Expand Down
19 changes: 19 additions & 0 deletions docs/content/Deploying/DocsGPT-Settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Here are some of the most fundamental settings you'll likely want to configure:
- `google`: Use Google's Vertex AI or Gemini models.
- `anthropic`: Use Anthropic's Claude models.
- `groq`: Use Groq's models.
- `qianfan`: Use Baidu Qianfan models.
- `huggingface`: Use HuggingFace Inference API.
- `azure_openai`: Use Azure OpenAI Service.
- `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally.
Expand All @@ -54,8 +55,26 @@ Here are some of the most fundamental settings you'll likely want to configure:
- **Examples:**
- For `LLM_PROVIDER=openai`: `gpt-4o`
- For `LLM_PROVIDER=google`: `gemini-2.0-flash`
- For `LLM_PROVIDER=qianfan`: `ernie-5.0`
- For local models (e.g., Ollama): `llama3.2:1b` (or any model name available in your setup).

### Example for Cloud API Provider (Baidu Qianfan)

To use Baidu Qianfan's `ernie-5.0` model, configure your `.env` file like this:

```env
LLM_PROVIDER=qianfan
QIANFAN_API_KEY=YOUR_QIANFAN_API_KEY
LLM_NAME=ernie-5.0
```

You can also reuse the generic `API_KEY` variable with `LLM_PROVIDER=qianfan`, but `QIANFAN_API_KEY` is clearer when you configure multiple providers in the same deployment.

The first Qianfan integration is intentionally conservative:

- Text chat is supported.
- Attachments, tool calling, and structured output are left disabled until they are verified end-to-end against the Qianfan API.

- **`EMBEDDINGS_NAME`**: This setting defines which embedding model DocsGPT will use to generate vector embeddings for your documents. Embeddings are numerical representations of text that allow DocsGPT to understand the semantic meaning of your documents for efficient search and retrieval.

- **Default value:** `huggingface_sentence-transformers/all-mpnet-base-v2` (a good general-purpose embedding model).
Expand Down
5 changes: 4 additions & 1 deletion docs/content/Models/cloud-providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ DocsGPT offers direct, streamlined support for the following cloud LLM providers
| Prem AI | `premai` | (See Prem AI docs) |
| AWS SageMaker | `sagemaker` | (See SageMaker docs) |
| Novita AI | `novita` | (See Novita docs) |
| Baidu Qianfan | `qianfan` | `ernie-5.0` |

The first Qianfan integration in DocsGPT is scoped to text chat with `ernie-5.0`. Attachment handling, tool calling, and structured output are intentionally disabled until they are verified end-to-end.

## Connecting to OpenAI-Compatible Cloud APIs

Expand All @@ -55,4 +58,4 @@ Remember to consult the documentation of your chosen OpenAI-compatible cloud pro

## Adding Support for Other Cloud Providers

If you wish to connect to a cloud provider that is not explicitly listed above or doesn't offer OpenAI API compatibility, you can extend DocsGPT to support it. Within the DocsGPT repository, navigate to the `application/llm` directory. Here, you will find Python files defining the existing LLM integrations. You can use these files as examples to create a new module for your desired cloud provider. After creating your new LLM module, you will need to register it within the `llm_creator.py` file. This process involves some coding, but it allows for virtually unlimited extensibility to connect to any cloud-based LLM service with an accessible API.
If you wish to connect to a cloud provider that is not explicitly listed above or doesn't offer OpenAI API compatibility, you can extend DocsGPT to support it. Within the DocsGPT repository, navigate to the `application/llm` directory. Here, you will find Python files defining the existing LLM integrations. You can use these files as examples to create a new module for your desired cloud provider. After creating your new LLM module, you will need to register it within the `llm_creator.py` file. This process involves some coding, but it allows for virtually unlimited extensibility to connect to any cloud-based LLM service with an accessible API.
48 changes: 48 additions & 0 deletions tests/core/test_model_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_all_providers_exist(self):
assert ModelProvider.PREMAI == "premai"
assert ModelProvider.LLAMA_CPP == "llama.cpp"
assert ModelProvider.AZURE_OPENAI == "azure_openai"
assert ModelProvider.QIANFAN == "qianfan"


class TestModelCapabilities:
Expand Down Expand Up @@ -195,6 +196,7 @@ def test_load_models_with_openai_key(self):
mock_settings.GROQ_API_KEY = None
mock_settings.OPEN_ROUTER_API_KEY = None
mock_settings.NOVITA_API_KEY = None
mock_settings.QIANFAN_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
mock_settings.LLM_PROVIDER = "openai"
mock_settings.LLM_NAME = ""
Expand All @@ -215,6 +217,7 @@ def test_load_models_custom_openai_base_url(self):
mock_settings.GROQ_API_KEY = None
mock_settings.OPEN_ROUTER_API_KEY = None
mock_settings.NOVITA_API_KEY = None
mock_settings.QIANFAN_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
mock_settings.LLM_PROVIDER = "openai"
mock_settings.LLM_NAME = "llama3,gemma"
Expand Down Expand Up @@ -293,6 +296,18 @@ def test_add_novita_models_with_key(self):
reg._add_novita_models(mock_settings)
assert len(reg.models) > 0

@pytest.mark.unit
def test_add_qianfan_models_with_key(self):
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models = {}
mock_settings = MagicMock()
mock_settings.QIANFAN_API_KEY = "qianfan-test"
mock_settings.LLM_PROVIDER = ""
mock_settings.LLM_NAME = ""
reg._add_qianfan_models(mock_settings)
assert len(reg.models) > 0

@pytest.mark.unit
def test_add_azure_openai_models_specific(self):
with patch.object(ModelRegistry, "_load_models"):
Expand Down Expand Up @@ -328,6 +343,7 @@ def test_default_model_fallback_to_first(self):
mock_settings.GROQ_API_KEY = None
mock_settings.OPEN_ROUTER_API_KEY = None
mock_settings.NOVITA_API_KEY = None
mock_settings.QIANFAN_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
mock_settings.LLM_PROVIDER = ""
mock_settings.LLM_NAME = ""
Expand All @@ -351,6 +367,7 @@ def test_default_model_from_provider_fallback(self):
mock_settings.GROQ_API_KEY = None
mock_settings.OPEN_ROUTER_API_KEY = None
mock_settings.NOVITA_API_KEY = None
mock_settings.QIANFAN_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
mock_settings.LLM_PROVIDER = "openai"
mock_settings.LLM_NAME = None
Expand Down Expand Up @@ -408,6 +425,18 @@ def test_add_novita_models_no_key_with_provider(self):
reg._add_novita_models(mock_settings)
assert len(reg.models) > 0

@pytest.mark.unit
def test_add_qianfan_models_no_key_with_provider(self):
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models = {}
mock_settings = MagicMock()
mock_settings.QIANFAN_API_KEY = None
mock_settings.LLM_PROVIDER = "qianfan"
mock_settings.LLM_NAME = "nonexistent"
reg._add_qianfan_models(mock_settings)
assert len(reg.models) > 0

@pytest.mark.unit
def test_to_dict_disabled_model(self):
model = AvailableModel(
Expand Down Expand Up @@ -543,6 +572,24 @@ def test_add_novita_no_key_matching_name(self):
reg._add_novita_models(mock_settings)
assert len(reg.models) >= 1

@pytest.mark.unit
def test_add_qianfan_no_key_matching_name(self):
"""Cover qianfan fallback with matching name."""
from application.core.model_configs import QIANFAN_MODELS

with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models = {}
mock_settings = MagicMock()
mock_settings.QIANFAN_API_KEY = None
mock_settings.LLM_PROVIDER = "qianfan"
if QIANFAN_MODELS:
mock_settings.LLM_NAME = QIANFAN_MODELS[0].id
else:
mock_settings.LLM_NAME = "nonexistent"
reg._add_qianfan_models(mock_settings)
assert len(reg.models) >= 1

@pytest.mark.unit
def test_load_models_default_from_llm_name_exact_match(self):
"""Cover line 136/147: exact LLM_NAME match for default model."""
Expand All @@ -555,6 +602,7 @@ def test_load_models_default_from_llm_name_exact_match(self):
mock_settings.GROQ_API_KEY = None
mock_settings.OPEN_ROUTER_API_KEY = None
mock_settings.NOVITA_API_KEY = None
mock_settings.QIANFAN_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
mock_settings.LLM_PROVIDER = "openai"
mock_settings.API_KEY = None
Expand Down
10 changes: 10 additions & 0 deletions tests/core/test_model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ def test_novita_key(self):

assert get_api_key_for_provider("novita") == "sk-novita"

@pytest.mark.unit
def test_qianfan_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.QIANFAN_API_KEY = "sk-qianfan"
mock_settings.API_KEY = "sk-fallback"

from application.core.model_utils import get_api_key_for_provider

assert get_api_key_for_provider("qianfan") == "sk-qianfan"

@pytest.mark.unit
def test_huggingface_key(self):
with patch("application.core.settings.settings") as mock_settings:
Expand Down
1 change: 1 addition & 0 deletions tests/llm/handlers/test_handler_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def test_handlers_registry(self):
"openai": OpenAILLMHandler,
"google": GoogleLLMHandler,
"novita": OpenAILLMHandler,
"qianfan": OpenAILLMHandler,
"default": OpenAILLMHandler,
}

Expand Down
Loading
Loading