WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package provider
import (
"fmt"
"strings"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// AliyunChatBaseURL 阿里云 DashScope Chat/Embedding 的默认 BaseURL
AliyunChatBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
// AliyunRerankBaseURL 阿里云 DashScope Rerank 的默认 BaseURL
AliyunRerankBaseURL = "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank"
)
// AliyunProvider 实现阿里云 DashScope 的 Provider 接口
type AliyunProvider struct{}
func init() {
Register(&AliyunProvider{})
}
// Info 返回阿里云 provider 的元数据
func (p *AliyunProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderAliyun,
DisplayName: "阿里云 DashScope",
Description: "qwen-plus, tongyi-embedding-vision-plus, qwen3-rerank, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: AliyunChatBaseURL,
types.ModelTypeEmbedding: AliyunChatBaseURL,
types.ModelTypeRerank: AliyunRerankBaseURL,
types.ModelTypeVLLM: AliyunChatBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证阿里云 provider 配置
func (p *AliyunProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Aliyun DashScope")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
// IsQwenThinkingModel 检查模型名是否为支持思维链的 Qwen 模型
// 支持思维链的模型需要特殊处理 enable_thinking 参数
func IsQwenThinkingModel(modelName string) bool {
lowerName := strings.ToLower(modelName)
return strings.HasPrefix(lowerName, "qwen3") ||
strings.HasPrefix(lowerName, "qwen-plus") ||
strings.HasPrefix(lowerName, "qwen-max") ||
strings.HasPrefix(lowerName, "qwen-turbo")
}
// IsQwen3Model checks whether the model belongs to the Qwen3 family only.
func IsQwen3Model(modelName string) bool {
return strings.HasPrefix(strings.ToLower(modelName), "qwen3")
}
// IsDeepSeekModel 检查模型名是否为 DeepSeek 模型
// DeepSeek 模型不支持 tool_choice 参数
func IsDeepSeekModel(modelName string) bool {
return strings.Contains(strings.ToLower(modelName), "deepseek")
}
+41
View File
@@ -0,0 +1,41 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const AnthropicBaseURL = "https://api.anthropic.com/v1"
// AnthropicProvider implements native Anthropic Messages API metadata.
type AnthropicProvider struct{}
func init() {
Register(&AnthropicProvider{})
}
func (p *AnthropicProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderAnthropic,
DisplayName: "Anthropic",
Description: "Claude models via native Anthropic Messages API",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: AnthropicBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
},
RequiresAuth: true,
}
}
func (p *AnthropicProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Anthropic provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+61
View File
@@ -0,0 +1,61 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
// AzureOpenAIProvider 实现 Azure OpenAI 的 Provider 接口
type AzureOpenAIProvider struct{}
func init() {
Register(&AzureOpenAIProvider{})
}
// Info 返回 Azure OpenAI provider 的元数据
func (p *AzureOpenAIProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderAzureOpenAI,
DisplayName: "Azure OpenAI",
Description: "gpt-4o, gpt-4, text-embedding-ada-002, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: "https://{resource}.openai.azure.com",
types.ModelTypeEmbedding: "https://{resource}.openai.azure.com",
types.ModelTypeRerank: "https://{resource}.openai.azure.com",
types.ModelTypeVLLM: "https://{resource}.openai.azure.com",
types.ModelTypeASR: "https://{resource}.openai.azure.com",
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeVLLM,
types.ModelTypeASR,
},
RequiresAuth: true,
ExtraFields: []ExtraFieldConfig{
{
Key: "api_version",
Label: "API Version",
Type: "string",
Required: false,
Default: "2024-10-21",
Placeholder: "e.g. 2024-10-21",
},
},
}
}
// ValidateConfig 验证 Azure OpenAI provider 配置
func (p *AzureOpenAIProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Azure OpenAI provider")
}
if config.ModelName == "" {
return fmt.Errorf("deployment name (model name) is required")
}
if config.BaseURL == "" {
return fmt.Errorf("Azure resource endpoint (base URL) is required")
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// DeepSeekBaseURL DeepSeek 官方 API BaseURL
DeepSeekBaseURL = "https://api.deepseek.com/v1"
)
// DeepSeekProvider 实现 DeepSeek 的 Provider 接口
type DeepSeekProvider struct{}
func init() {
Register(&DeepSeekProvider{})
}
// Info 返回 DeepSeek provider 的元数据
func (p *DeepSeekProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderDeepSeek,
DisplayName: "DeepSeek",
Description: "deepseek-chat, deepseek-reasoner, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: DeepSeekBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 DeepSeek provider 配置
func (p *DeepSeekProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for DeepSeek provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// GeminiBaseURL Google Gemini API BaseURL
GeminiBaseURL = "https://generativelanguage.googleapis.com/v1beta"
// GeminiOpenAICompatBaseURL Gemini OpenAI 兼容模式 BaseURL
GeminiOpenAICompatBaseURL = "https://generativelanguage.googleapis.com/v1beta/openai"
)
// GeminiProvider 实现 Google Gemini 的 Provider 接口
type GeminiProvider struct{}
func init() {
Register(&GeminiProvider{})
}
// Info 返回 Gemini provider 的元数据
func (p *GeminiProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderGemini,
DisplayName: "Google Gemini",
Description: "gemini-3-flash-preview, gemini-2.5-pro, gemini-embedding-2, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: GeminiOpenAICompatBaseURL,
types.ModelTypeEmbedding: GeminiBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 Gemini provider 配置
func (p *GeminiProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Google Gemini provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+43
View File
@@ -0,0 +1,43 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
// GenericProvider 实现通用 OpenAI 兼容的 Provider 接口
type GenericProvider struct{}
func init() {
Register(&GenericProvider{})
}
// Info 返回通用 provider 的元数据
func (p *GenericProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderGeneric,
DisplayName: "自定义 (OpenAI兼容接口)",
Description: "Generic API endpoint (OpenAI-compatible)",
DefaultURLs: map[types.ModelType]string{}, // 需要用户自行配置填写
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
types.ModelTypeASR,
},
RequiresAuth: false, // 可能需要也可能不需要
}
}
// ValidateConfig 验证通用 provider 配置
func (p *GenericProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for generic provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+59
View File
@@ -0,0 +1,59 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// GPUStackBaseURL GPUStack API BaseURL (OpenAI 兼容模式)
GPUStackBaseURL = "http://your_gpustack_server_url/v1-openai"
// GPUStackRerankBaseURL GPUStack Rerank API 虽然兼容OpenAI,但路径不同 (/v1/rerank 而非 /v1-openai/rerank)
GPUStackRerankBaseURL = "http://your_gpustack_server_url/v1"
)
// GPUStackProvider 实现 GPUStack 的 Provider 接口
type GPUStackProvider struct{}
func init() {
Register(&GPUStackProvider{})
}
// Info 返回 GPUStack provider 的元数据
func (p *GPUStackProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderGPUStack,
DisplayName: "GPUStack",
Description: "Choose your deployed model on GPUStack",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: GPUStackBaseURL,
types.ModelTypeEmbedding: GPUStackBaseURL,
types.ModelTypeRerank: GPUStackRerankBaseURL,
types.ModelTypeVLLM: GPUStackBaseURL,
types.ModelTypeASR: GPUStackBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
types.ModelTypeASR,
},
RequiresAuth: true, // GPUStack 需要 API Key
}
}
// ValidateConfig 验证 GPUStack provider 配置
func (p *GPUStackProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for GPUStack provider")
}
if config.APIKey == "" {
return fmt.Errorf("API key is required for GPUStack provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// HunyuanBaseURL 腾讯混元 API BaseURL (OpenAI 兼容模式)
HunyuanBaseURL = "https://api.hunyuan.cloud.tencent.com/v1"
)
// HunyuanProvider 实现腾讯混元的 Provider 接口
type HunyuanProvider struct{}
func init() {
Register(&HunyuanProvider{})
}
// Info 返回腾讯混元 provider 的元数据
func (p *HunyuanProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderHunyuan,
DisplayName: "腾讯混元 Hunyuan",
Description: "hunyuan-pro, hunyuan-standard, hunyuan-embedding, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: HunyuanBaseURL,
types.ModelTypeEmbedding: HunyuanBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证腾讯混元 provider 配置
func (p *HunyuanProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Hunyuan provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+44
View File
@@ -0,0 +1,44 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
JinaBaseURL = "https://api.jina.ai/v1"
)
// JinaProvider 实现 Jina AI 的 Provider 接口
type JinaProvider struct{}
func init() {
Register(&JinaProvider{})
}
// Info 返回 Jina AI provider 的元数据
func (p *JinaProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderJina,
DisplayName: "Jina",
Description: "jina-clip-v1, jina-embeddings-v2-base-zh, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeEmbedding: JinaBaseURL,
types.ModelTypeRerank: JinaBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeEmbedding,
types.ModelTypeRerank,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 Jina AI provider 配置
func (p *JinaProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Jina AI provider")
}
return nil
}
+55
View File
@@ -0,0 +1,55 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// LiteLLMBaseURL is a placeholder the user must replace with a reachable
// LiteLLM proxy. Loopback defaults (localhost:4000) are rejected by SSRF
// unless the host is added to SSRF_WHITELIST; Docker also cannot reach a
// proxy on the host via localhost. The hostname contains "litellm" so
// DetectProvider recognises the catalog default.
LiteLLMBaseURL = "http://your_litellm_proxy/v1"
)
// LiteLLMProvider implements the Provider interface for LiteLLM
// (https://github.com/BerriAI/litellm). LiteLLM exposes a single
// OpenAI-compatible endpoint that routes to 100+ providers (OpenAI, Anthropic,
// Gemini, Bedrock, Vertex, Azure, ...) behind one base URL and key, so it plugs
// into WeKnora's OpenAI-compatible transport like the other gateway providers.
type LiteLLMProvider struct{}
func init() {
Register(&LiteLLMProvider{})
}
// Info returns the metadata for the LiteLLM provider.
func (p *LiteLLMProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderLiteLLM,
DisplayName: "LiteLLM",
Description: "Self-hosted LiteLLM proxy: one OpenAI-compatible endpoint to 100+ providers.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: LiteLLMBaseURL,
types.ModelTypeEmbedding: LiteLLMBaseURL,
types.ModelTypeVLLM: LiteLLMBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig validates the LiteLLM provider configuration.
func (p *LiteLLMProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for LiteLLM provider")
}
return nil
}
+69
View File
@@ -0,0 +1,69 @@
package provider
import (
"fmt"
"strings"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// LKEAPBaseURL 腾讯云知识引擎原子能力 (LKEAP) 兼容 OpenAI 协议的 BaseURL
LKEAPBaseURL = "https://api.lkeap.cloud.tencent.com/v1"
// LKEAPRerankBaseURL 腾讯云知识引擎原子能力 Rerank API 域名(TC3 签名)
LKEAPRerankBaseURL = "https://lkeap.tencentcloudapi.com"
)
// LKEAPProvider 实现腾讯云 LKEAP 的 Provider 接口
// 支持 DeepSeek-R1, DeepSeek-V3 系列模型,具备思维链能力
type LKEAPProvider struct{}
func init() {
Register(&LKEAPProvider{})
}
// Info 返回 LKEAP provider 的元数据
func (p *LKEAPProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderLKEAP,
DisplayName: "腾讯云 LKEAP",
Description: "DeepSeek-R1, DeepSeek-V3, lke-reranker-base 等",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: LKEAPBaseURL,
types.ModelTypeRerank: LKEAPRerankBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeRerank,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 LKEAP provider 配置
func (p *LKEAPProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for LKEAP provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
// IsLKEAPDeepSeekV3Model 检查是否为 DeepSeek V3.x 系列模型
// V3.x 系列支持通过 Thinking 参数控制思维链开关
func IsLKEAPDeepSeekV3Model(modelName string) bool {
return strings.Contains(strings.ToLower(modelName), "deepseek-v3")
}
// IsLKEAPDeepSeekR1Model 检查是否为 DeepSeek R1 系列模型
// R1 系列默认开启思维链
func IsLKEAPDeepSeekR1Model(modelName string) bool {
return strings.Contains(strings.ToLower(modelName), "deepseek-r1")
}
// IsLKEAPThinkingModel 检查是否为支持思维链的 LKEAP 模型
func IsLKEAPThinkingModel(modelName string) bool {
return IsLKEAPDeepSeekR1Model(modelName) || IsLKEAPDeepSeekV3Model(modelName)
}
+48
View File
@@ -0,0 +1,48 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
LongCatBaseURL = "https://api.longcat.chat/openai/v1"
)
// LongCatProvider 实现 LongCat AI 的 Provider 接口
type LongCatProvider struct{}
func init() {
Register(&LongCatProvider{})
}
// Info 返回 LongCat provider 的元数据
func (p *LongCatProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderLongCat,
DisplayName: "LongCat AI",
Description: "LongCat-Flash-Chat, LongCat-Flash-Thinking, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: LongCatBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 LongCat provider 配置
func (p *LongCatProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for LongCat provider")
}
if config.APIKey == "" {
return fmt.Errorf("API key is required for LongCat provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// MimoBaseURL 小米 Mimo API BaseURL
MimoBaseURL = "https://api.xiaomimimo.com/v1"
)
// MimoProvider 实现小米 Mimo 的 Provider 接口
type MimoProvider struct{}
func init() {
Register(&MimoProvider{})
}
// Info 返回小米 Mimo provider 的元数据
func (p *MimoProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderMimo,
DisplayName: "小米 MiMo",
Description: "mimo-v2-flash",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: MimoBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证小米 Mimo provider 配置
func (p *MimoProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Mimo provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// MiniMaxBaseURL MiniMax 国际版 API BaseURL
MiniMaxBaseURL = "https://api.minimax.io/v1"
// MiniMaxCNBaseURL MiniMax 国内版 API BaseURL
MiniMaxCNBaseURL = "https://api.minimaxi.com/v1"
)
// MiniMaxProvider 实现 MiniMax 的 Provider 接口
type MiniMaxProvider struct{}
func init() {
Register(&MiniMaxProvider{})
}
// Info 返回 MiniMax provider 的元数据
func (p *MiniMaxProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderMiniMax,
DisplayName: "MiniMax",
Description: "MiniMax-M3, MiniMax-M2.7, MiniMax-M2.7-highspeed, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: MiniMaxCNBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 MiniMax provider 配置
func (p *MiniMaxProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for MiniMax provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+53
View File
@@ -0,0 +1,53 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// ModelScopeBaseURL ModelScope API BaseURL (OpenAI 兼容模式)
ModelScopeBaseURL = "https://api-inference.modelscope.cn/v1"
)
// ModelScopeProvider 实现 ModelScope (魔搭) 的 Provider 接口
type ModelScopeProvider struct{}
func init() {
Register(&ModelScopeProvider{})
}
// Info 返回 ModelScope provider 的元数据
func (p *ModelScopeProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderModelScope,
DisplayName: "魔搭 ModelScope",
Description: "Qwen/Qwen3-8B, Qwen/Qwen3-Embedding-8B, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: ModelScopeBaseURL,
types.ModelTypeEmbedding: ModelScopeBaseURL,
types.ModelTypeVLLM: ModelScopeBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 ModelScope provider 配置
func (p *ModelScopeProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for ModelScope provider")
}
if config.APIKey == "" {
return fmt.Errorf("API key is required for ModelScope provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+71
View File
@@ -0,0 +1,71 @@
package provider
import (
"fmt"
"strings"
"github.com/Tencent/WeKnora/internal/types"
)
const (
MoonshotBaseURL = "https://api.moonshot.ai/v1"
)
// IsMoonshotFixedTempModel reports whether the given Moonshot/Kimi model
// only accepts temperature=1. The following models reject any temperature
// value other than 1:
// - moonshot-v1 series (moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k)
// - kimi-k2.5 and kimi-k2.6 (no temperature param in API docs)
//
// kimi-k2 / kimi-k2-turbo / kimi-k2-thinking models accept the full [0,1]
// range and are NOT affected.
func IsMoonshotFixedTempModel(modelName string) bool {
name := strings.ToLower(strings.TrimSpace(modelName))
if strings.HasPrefix(name, "moonshot-v1") {
return true
}
// kimi-k2.5, kimi-k2.6 — no temperature parameter supported
if name == "kimi-k2.5" || name == "kimi-k2.6" {
return true
}
return false
}
// MoonshotProvider 实现 Moonshot AI (Kimi) 的 Provider 接口
type MoonshotProvider struct{}
func init() {
Register(&MoonshotProvider{})
}
// Info 返回 Moonshot provider 的元数据
func (p *MoonshotProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderMoonshot,
DisplayName: "月之暗面 Moonshot",
Description: "kimi-k2-turbo-preview, moonshot-v1-8k-vision-preview, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: MoonshotBaseURL,
types.ModelTypeVLLM: MoonshotBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 Moonshot provider 配置
func (p *MoonshotProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for Moonshot provider")
}
if config.APIKey == "" {
return fmt.Errorf("API key is required for Moonshot provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// NovitaOpenAIBaseURL Novita OpenAI-compatible API BaseURL
NovitaOpenAIBaseURL = "https://api.novita.ai/openai/v1"
)
// NovitaProvider 实现 Novita AI 的 Provider 接口
type NovitaProvider struct{}
func init() {
Register(&NovitaProvider{})
}
// Info 返回 Novita provider 的元数据
func (p *NovitaProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderNovita,
DisplayName: "Novita AI",
Description: "moonshotai/kimi-k2.5, zai-org/glm-5, minimax/minimax-m2.7, qwen/qwen3-embedding-0.6b, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: NovitaOpenAIBaseURL,
types.ModelTypeEmbedding: NovitaOpenAIBaseURL,
types.ModelTypeVLLM: NovitaOpenAIBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 Novita provider 配置
func (p *NovitaProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Novita provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// NvidiaChatBaseURL NVIDIA Chat 的默认 BaseURL
NvidiaChatBaseURL = "https://integrate.api.nvidia.com/v1"
// NvidiaRerankBaseURL NVIDIA Rerank 的默认 BaseURL
NvidiaRerankBaseURL = "https://ai.api.nvidia.com/v1/retrieval/nvidia/reranking"
)
// NvidiaProvider 实现NVIDIA AI 的 Provider 接口
type NvidiaProvider struct{}
func init() {
Register(&NvidiaProvider{})
}
// Info 返回NVIDIA provider 的元数据
func (p *NvidiaProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderNvidia,
DisplayName: "NVIDIA",
Description: "deepseek-ai-deepseek-v3_1, nv-embed-v1, rerank-qa-mistral-4b, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: NvidiaChatBaseURL,
types.ModelTypeEmbedding: NvidiaChatBaseURL,
types.ModelTypeRerank: NvidiaRerankBaseURL,
types.ModelTypeVLLM: NvidiaChatBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证NVIDIA provider 配置
func (p *NvidiaProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for NVIDIA")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+86
View File
@@ -0,0 +1,86 @@
package provider
import (
"fmt"
"strings"
"github.com/Tencent/WeKnora/internal/types"
)
const (
OpenAIBaseURL = "https://api.openai.com/v1"
)
// OpenAIProvider 实现 OpenAI 的 Provider 接口
type OpenAIProvider struct{}
func init() {
Register(&OpenAIProvider{})
}
// Info 返回 OpenAI provider 的元数据
func (p *OpenAIProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderOpenAI,
DisplayName: "OpenAI",
Description: "gpt-5.2, gpt-5-mini, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: OpenAIBaseURL,
types.ModelTypeEmbedding: OpenAIBaseURL,
types.ModelTypeRerank: OpenAIBaseURL,
types.ModelTypeVLLM: OpenAIBaseURL,
types.ModelTypeASR: OpenAIBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
types.ModelTypeASR,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 OpenAI provider 配置
func (p *OpenAIProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for OpenAI provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
// IsOpenAIReasoningOrGPT5Model 判断模型是否为 OpenAI / Azure OpenAI 的
// 推理类(o-series)或 GPT-5 系列模型。
//
// 这些模型在 OpenAI Chat Completions API 中:
// - 不再支持 `max_tokens`,必须使用 `max_completion_tokens`;
// - 仅支持默认的 `temperature=1`、`top_p=1`,且不支持 `frequency_penalty` /
// `presence_penalty` 等采样参数(传非默认值会被拒绝)。
//
// 参考:
// - https://platform.openai.com/docs/api-reference/chat
// - https://learn.microsoft.com/azure/ai-services/openai/how-to/reasoning
//
// 仅基于模型名做启发式匹配;对于 Azure OpenAI,因为模型名实际上是 deployment 名,
// 用户若用了自定义部署名我们无法识别,此时仍会按普通模型处理(保持原行为)。
func IsOpenAIReasoningOrGPT5Model(modelName string) bool {
name := strings.ToLower(strings.TrimSpace(modelName))
if name == "" {
return false
}
if strings.HasPrefix(name, "gpt-5") {
return true
}
// o1 / o1-mini / o1-preview / o3 / o3-mini / o4-mini ...
// 必须精确匹配,避免误命中 "openai-..." 之类。
for _, prefix := range []string{"o1", "o3", "o4"} {
if name == prefix || strings.HasPrefix(name, prefix+"-") {
return true
}
}
return false
}
+46
View File
@@ -0,0 +1,46 @@
package provider
import "testing"
// TestIsOpenAIReasoningOrGPT5Model 验证 GPT-5 / o-series 模型识别逻辑。
// 见 issue #1283:这些模型必须使用 max_completion_tokens 替代 max_tokens。
func TestIsOpenAIReasoningOrGPT5Model(t *testing.T) {
cases := []struct {
name string
input string
want bool
}{
{"empty", "", false},
{"gpt-5", "gpt-5", true},
{"gpt-5-mini", "gpt-5-mini", true},
{"gpt-5.2", "gpt-5.2", true},
{"gpt-5.5-pro", "gpt-5.5-pro", true},
{"gpt-5 mixed case", "GPT-5.4-Mini", true},
{"o1", "o1", true},
{"o1-mini", "o1-mini", true},
{"o1-preview", "o1-preview", true},
{"o3", "o3", true},
{"o3-mini", "o3-mini", true},
{"o4-mini", "o4-mini", true},
{"gpt-4", "gpt-4", false},
{"gpt-4o", "gpt-4o", false},
{"gpt-4o-mini", "gpt-4o-mini", false},
{"gpt-3.5-turbo", "gpt-3.5-turbo", false},
{"name starting with 'o1' but not o-series", "olympus-1", false},
{"name starting with 'openai-'", "openai-gpt-4", false},
{"name starting with 'o3' but not o-series", "o3xtra", false},
{"random", "qwen-max", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := IsOpenAIReasoningOrGPT5Model(tc.input)
if got != tc.want {
t.Errorf("IsOpenAIReasoningOrGPT5Model(%q) = %v, want %v", tc.input, got, tc.want)
}
})
}
}
+46
View File
@@ -0,0 +1,46 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
OpenRouterBaseURL = "https://openrouter.ai/api/v1"
)
// OpenRouterProvider 实现 OpenRouter 的 Provider 接口
type OpenRouterProvider struct{}
func init() {
Register(&OpenRouterProvider{})
}
// Info 返回 OpenRouter provider 的元数据
func (p *OpenRouterProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderOpenRouter,
DisplayName: "OpenRouter",
Description: "openai/gpt-5.2-chat, google/gemini-3-flash-preview, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: OpenRouterBaseURL,
types.ModelTypeEmbedding: OpenRouterBaseURL,
types.ModelTypeVLLM: OpenRouterBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 OpenRouter provider 配置
func (p *OpenRouterProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for OpenRouter provider")
}
return nil
}
+313
View File
@@ -0,0 +1,313 @@
// Package provider defines the unified interface and registry for multi-vendor model API adapters.
package provider
import (
"fmt"
"strings"
"sync"
"github.com/Tencent/WeKnora/internal/types"
)
// ProviderName 模型服务商名称
type ProviderName string
const (
// OpenAI
ProviderOpenAI ProviderName = "openai"
// Anthropic Claude
ProviderAnthropic ProviderName = "anthropic"
// 阿里云 DashScope
ProviderAliyun ProviderName = "aliyun"
// 智谱AI (GLM 系列)
ProviderZhipu ProviderName = "zhipu"
// OpenRouter
ProviderOpenRouter ProviderName = "openrouter"
// ProviderLiteLLM is the LiteLLM self-hosted proxy (OpenAI-compatible gateway to 100+ providers).
ProviderLiteLLM ProviderName = "litellm"
// Requesty
ProviderRequesty ProviderName = "requesty"
// 硅基流动
ProviderSiliconFlow ProviderName = "siliconflow"
// Jina AI (Embedding and Rerank)
ProviderJina ProviderName = "jina"
// Generic 兼容OpenAI (自定义部署)
ProviderGeneric ProviderName = "generic"
// DeepSeek
ProviderDeepSeek ProviderName = "deepseek"
// Google Gemini
ProviderGemini ProviderName = "gemini"
// 火山引擎 Ark
ProviderVolcengine ProviderName = "volcengine"
// 腾讯混元
ProviderHunyuan ProviderName = "hunyuan"
// MiniMax
ProviderMiniMax ProviderName = "minimax"
// 小米 Mimo
ProviderMimo ProviderName = "mimo"
// GPUStack (私有化部署)
ProviderGPUStack ProviderName = "gpustack"
// 月之暗面 Moonshot (Kimi)
ProviderMoonshot ProviderName = "moonshot"
// 魔搭 ModelScope
ProviderModelScope ProviderName = "modelscope"
// 百度千帆
ProviderQianfan ProviderName = "qianfan"
// 七牛云
ProviderQiniu ProviderName = "qiniu"
// 美团 LongCat AI
ProviderLongCat ProviderName = "longcat"
// 腾讯云 LKEAP (知识引擎原子能力)
ProviderLKEAP ProviderName = "lkeap"
// NVIDIA
ProviderNvidia ProviderName = "nvidia"
// Novita AI
ProviderNovita ProviderName = "novita"
// Azure OpenAI
ProviderAzureOpenAI ProviderName = "azure_openai"
)
// AllProviders 返回所有注册的提供者名称
func AllProviders() []ProviderName {
return []ProviderName{
ProviderGeneric,
ProviderWeKnoraCloud,
ProviderAliyun,
ProviderZhipu,
ProviderVolcengine,
ProviderHunyuan,
ProviderSiliconFlow,
ProviderDeepSeek,
ProviderMiniMax,
ProviderMoonshot,
ProviderModelScope,
ProviderQianfan,
ProviderQiniu,
ProviderOpenAI,
ProviderAnthropic,
ProviderGemini,
ProviderOpenRouter,
ProviderLiteLLM,
ProviderRequesty,
ProviderJina,
ProviderMimo,
ProviderLongCat,
ProviderLKEAP,
ProviderGPUStack,
ProviderNvidia,
ProviderNovita,
ProviderAzureOpenAI,
}
}
// ProviderInfo 包含提供者的元数据
type ProviderInfo struct {
Name ProviderName // 提供者标识
DisplayName string // 可读名称
Description string // 提供者描述
DefaultURLs map[types.ModelType]string // 按模型类型区分的默认 BaseURL
ModelTypes []types.ModelType // 支持的模型类型
RequiresAuth bool // 是否需要 API key
ExtraFields []ExtraFieldConfig // 额外配置字段
}
// GetDefaultURL 获取指定模型类型的默认 URL
func (p ProviderInfo) GetDefaultURL(modelType types.ModelType) string {
if url, ok := p.DefaultURLs[modelType]; ok {
return url
}
// 回退到 Chat URL
if url, ok := p.DefaultURLs[types.ModelTypeKnowledgeQA]; ok {
return url
}
return ""
}
// ExtraFieldConfig 定义提供者的额外配置字段
type ExtraFieldConfig struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"` // "string", "number", "boolean", "select"
Required bool `json:"required"`
Default string `json:"default"`
Placeholder string `json:"placeholder"`
Options []struct {
Label string `json:"label"`
Value string `json:"value"`
} `json:"options,omitempty"`
}
// Config 表示模型提供者的配置
type Config struct {
Provider ProviderName `json:"provider"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
ModelName string `json:"model_name"`
ModelID string `json:"model_id"`
Extra map[string]any `json:"extra,omitempty"`
}
type Provider interface {
// Info 返回服务商的元数据
Info() ProviderInfo
// ValidateConfig 验证服务商的配置
ValidateConfig(config *Config) error
}
// registry 存储所有注册的提供者
var (
registryMu sync.RWMutex
registry = make(map[ProviderName]Provider)
)
// Register 添加一个提供者到全局注册表
func Register(p Provider) {
registryMu.Lock()
defer registryMu.Unlock()
registry[p.Info().Name] = p
}
// Get 通过名称从注册表中获取提供者
func Get(name ProviderName) (Provider, bool) {
registryMu.RLock()
defer registryMu.RUnlock()
p, ok := registry[name]
return p, ok
}
// GetOrDefault 通过名称从注册表中获取提供者,如果未找到则返回默认提供者
func GetOrDefault(name ProviderName) Provider {
p, ok := Get(name)
if ok {
return p
}
// 如果未找到则返回默认提供者
p, _ = Get(ProviderGeneric)
return p
}
// List 返回所有注册的提供者(按 AllProviders 定义的顺序)
func List() []ProviderInfo {
registryMu.RLock()
defer registryMu.RUnlock()
result := make([]ProviderInfo, 0, len(registry))
for _, name := range AllProviders() {
if p, ok := registry[name]; ok {
result = append(result, p.Info())
}
}
return result
}
// ListByModelType 返回所有支持指定模型类型的提供者(按 AllProviders 定义的顺序)
func ListByModelType(modelType types.ModelType) []ProviderInfo {
registryMu.RLock()
defer registryMu.RUnlock()
result := make([]ProviderInfo, 0)
for _, name := range AllProviders() {
if p, ok := registry[name]; ok {
info := p.Info()
for _, t := range info.ModelTypes {
if t == modelType {
result = append(result, info)
break
}
}
}
}
return result
}
// DetectProvider 通过 BaseURL 检测服务商
func DetectProvider(baseURL string) ProviderName {
switch {
case containsAny(baseURL, "dashscope.aliyuncs.com"):
return ProviderAliyun
case containsAny(baseURL, "open.bigmodel.cn", "zhipu"):
return ProviderZhipu
case containsAny(baseURL, "openrouter.ai"):
return ProviderOpenRouter
// Hostname/path containing "litellm" (including the catalog placeholder
// your_litellm_proxy). Loopback URLs such as localhost:4000 stay generic
// because they are SSRF-blocked unless explicitly whitelisted.
case containsAny(baseURL, "litellm"):
return ProviderLiteLLM
case containsAny(baseURL, "router.requesty.ai", "requesty.ai"):
return ProviderRequesty
case containsAny(baseURL, "siliconflow.cn"):
return ProviderSiliconFlow
case containsAny(baseURL, "api.jina.ai"):
return ProviderJina
case containsAny(baseURL, "openai.azure.com"):
return ProviderAzureOpenAI
case containsAny(baseURL, "api.openai.com"):
return ProviderOpenAI
case containsAny(baseURL, "api.anthropic.com"):
return ProviderAnthropic
case containsAny(baseURL, "api.deepseek.com"):
return ProviderDeepSeek
case containsAny(baseURL, "generativelanguage.googleapis.com"):
return ProviderGemini
case containsAny(baseURL, "volces.com", "volcengine"):
return ProviderVolcengine
case containsAny(baseURL, "hunyuan.cloud.tencent.com"):
return ProviderHunyuan
case containsAny(baseURL, "minimax.io", "minimaxi.com"):
return ProviderMiniMax
case containsAny(baseURL, "xiaomimimo.com"):
return ProviderMimo
case containsAny(baseURL, "gpustack"):
return ProviderGPUStack
case containsAny(baseURL, "modelscope.cn"):
return ProviderModelScope
case containsAny(baseURL, "qiniuapi.com", "qiniu"):
return ProviderQiniu
case containsAny(baseURL, "moonshot.ai"):
return ProviderMoonshot
case containsAny(baseURL, "qianfan.baidubce.com", "baidubce.com"):
return ProviderQianfan
case containsAny(baseURL, "longcat.chat"):
return ProviderLongCat
case containsAny(baseURL, "lkeap.cloud.tencent.com", "api.lkeap", "lkeap.tencentcloudapi.com"):
return ProviderLKEAP
case containsAny(baseURL, "nvidia.com"):
return ProviderNvidia
case containsAny(baseURL, "api.novita.ai", "novita.ai"):
return ProviderNovita
case containsAny(baseURL, "weknora.weixin.qq.com"):
return ProviderWeKnoraCloud
default:
return ProviderGeneric
}
}
func containsAny(s string, substrs ...string) bool {
for _, sub := range substrs {
if strings.Contains(s, sub) {
return true
}
}
return false
}
func NewConfigFromModel(model *types.Model) (*Config, error) {
if model == nil {
return nil, fmt.Errorf("model is nil")
}
providerName := ProviderName(model.Parameters.Provider)
if providerName == "" {
providerName = DetectProvider(model.Parameters.BaseURL)
}
return &Config{
Provider: providerName,
BaseURL: model.Parameters.BaseURL,
APIKey: model.Parameters.APIKey,
ModelName: model.Name,
ModelID: model.ID,
}, nil
}
+381
View File
@@ -0,0 +1,381 @@
package provider
import (
"testing"
"github.com/Tencent/WeKnora/internal/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProviderRegistry(t *testing.T) {
// Test that all default providers are registered
t.Run("default providers registered", func(t *testing.T) {
providers := List()
assert.NotEmpty(t, providers, "should have registered providers")
// Check specific providers exist
for _, name := range []ProviderName{ProviderOpenAI, ProviderAliyun, ProviderZhipu, ProviderGeneric} {
p, ok := Get(name)
assert.True(t, ok, "provider %s should be registered", name)
assert.NotNil(t, p, "provider %s should not be nil", name)
}
})
t.Run("GetOrDefault fallback", func(t *testing.T) {
// Non-existent provider should fall back to generic
p := GetOrDefault("nonexistent")
require.NotNil(t, p)
assert.Equal(t, ProviderGeneric, p.Info().Name)
})
}
func TestDetectProvider(t *testing.T) {
tests := []struct {
url string
expected ProviderName
}{
{"https://api.openai.com/v1", ProviderOpenAI},
{"https://api.anthropic.com/v1", ProviderAnthropic},
{"https://openrouter.ai/api/v1", ProviderOpenRouter},
{"https://litellm.example.com/v1", ProviderLiteLLM},
{LiteLLMBaseURL, ProviderLiteLLM},
{"http://localhost:4000/v1", ProviderGeneric},
{"https://router.requesty.ai/v1", ProviderRequesty},
{"https://dashscope.aliyuncs.com/compatible-mode/v1", ProviderAliyun},
{"https://open.bigmodel.cn/api/paas/v4", ProviderZhipu},
{"https://api.deepseek.com/v1", ProviderDeepSeek},
{"https://generativelanguage.googleapis.com/v1beta/openai", ProviderGemini},
{"https://ark.cn-beijing.volces.com/api/v3", ProviderVolcengine},
{"https://api.hunyuan.cloud.tencent.com/v1", ProviderHunyuan},
{"https://api.minimaxi.com/v1", ProviderMiniMax},
{"https://api.minimax.io/v1", ProviderMiniMax},
{"https://api.xiaomimimo.com/v1", ProviderMimo},
{"https://custom-endpoint.example.com/v1", ProviderGeneric},
{"http://localhost:11434/v1", ProviderGeneric},
{"https://integrate.api.nvidia.com/v1", ProviderNvidia},
{"https://ai.api.nvidia.com/v1/retrieval/nvidia/reranking", ProviderNvidia},
}
for _, tt := range tests {
t.Run(tt.url, func(t *testing.T) {
result := DetectProvider(tt.url)
assert.Equal(t, tt.expected, result)
})
}
}
func TestAnthropicProviderValidation(t *testing.T) {
p := &AnthropicProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "sk-ant-test",
ModelName: "claude-sonnet-4-5",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("missing API key", func(t *testing.T) {
config := &Config{
ModelName: "claude-sonnet-4-5",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API key")
})
t.Run("info", func(t *testing.T) {
info := p.Info()
assert.Equal(t, ProviderAnthropic, info.Name)
assert.Equal(t, AnthropicBaseURL, info.GetDefaultURL(types.ModelTypeKnowledgeQA))
assert.Contains(t, info.ModelTypes, types.ModelTypeKnowledgeQA)
assert.True(t, info.RequiresAuth)
})
}
func TestOpenAIProviderValidation(t *testing.T) {
p := &OpenAIProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "sk-test",
ModelName: "gpt-4",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("missing API key", func(t *testing.T) {
config := &Config{
ModelName: "gpt-4",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API key")
})
t.Run("missing model name", func(t *testing.T) {
config := &Config{
APIKey: "sk-test",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "model name")
})
}
func TestAliyunProviderValidation(t *testing.T) {
p := &AliyunProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "sk-test",
ModelName: "qwen-max",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("info", func(t *testing.T) {
info := p.Info()
assert.Equal(t, ProviderAliyun, info.Name)
assert.Contains(t, info.ModelTypes, types.ModelTypeKnowledgeQA)
assert.Contains(t, info.ModelTypes, types.ModelTypeEmbedding)
assert.Contains(t, info.ModelTypes, types.ModelTypeRerank)
})
}
func TestAliyunModelDetection(t *testing.T) {
t.Run("Qwen3 model detection", func(t *testing.T) {
assert.True(t, IsQwen3Model("qwen3-32b"))
assert.True(t, IsQwen3Model("qwen3-72b"))
assert.False(t, IsQwen3Model("qwen-max"))
assert.False(t, IsQwen3Model("qwen2.5-72b"))
})
t.Run("DeepSeek model detection", func(t *testing.T) {
assert.True(t, IsDeepSeekModel("deepseek-chat"))
assert.True(t, IsDeepSeekModel("deepseek-v3.1"))
assert.True(t, IsDeepSeekModel("DeepSeek-Chat"))
assert.False(t, IsDeepSeekModel("qwen-max"))
})
}
func TestMiniMaxProviderValidation(t *testing.T) {
p := &MiniMaxProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "test-key",
ModelName: "MiniMax-M2.7",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("missing API key", func(t *testing.T) {
config := &Config{
ModelName: "MiniMax-M2.7",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API key")
})
t.Run("missing model name", func(t *testing.T) {
config := &Config{
APIKey: "test-key",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "model name")
})
t.Run("info", func(t *testing.T) {
info := p.Info()
assert.Equal(t, ProviderMiniMax, info.Name)
assert.Equal(t, "MiniMax", info.DisplayName)
assert.Contains(t, info.ModelTypes, types.ModelTypeKnowledgeQA)
assert.True(t, info.RequiresAuth)
assert.Contains(t, info.Description, "M2.7")
})
}
func TestZhipuProviderValidation(t *testing.T) {
p := &ZhipuProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "test-key",
ModelName: "glm-4",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("info", func(t *testing.T) {
info := p.Info()
assert.Equal(t, ProviderZhipu, info.Name)
assert.Equal(t, ZhipuChatBaseURL, info.GetDefaultURL(types.ModelTypeKnowledgeQA))
assert.Equal(t, ZhipuEmbeddingBaseURL, info.GetDefaultURL(types.ModelTypeEmbedding))
})
}
func TestRequestyProviderValidation(t *testing.T) {
p := &RequestyProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "test-key",
ModelName: "openai/gpt-4o-mini",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("missing API key", func(t *testing.T) {
config := &Config{
ModelName: "openai/gpt-4o-mini",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API key")
})
t.Run("info", func(t *testing.T) {
info := p.Info()
assert.Equal(t, ProviderRequesty, info.Name)
assert.Equal(t, "Requesty", info.DisplayName)
assert.Equal(t, RequestyBaseURL, info.GetDefaultURL(types.ModelTypeKnowledgeQA))
assert.Equal(t, RequestyBaseURL, info.GetDefaultURL(types.ModelTypeEmbedding))
assert.Contains(t, info.ModelTypes, types.ModelTypeKnowledgeQA)
assert.True(t, info.RequiresAuth)
})
}
func TestListByModelType(t *testing.T) {
t.Run("chat models", func(t *testing.T) {
providers := ListByModelType(types.ModelTypeKnowledgeQA)
assert.NotEmpty(t, providers)
// Multiple providers support chat
assert.GreaterOrEqual(t, len(providers), 9)
})
t.Run("rerank models", func(t *testing.T) {
providers := ListByModelType(types.ModelTypeRerank)
assert.NotEmpty(t, providers)
// Check that Aliyun supports rerank
foundAliyun := false
foundLKEAP := false
foundVolcengine := false
for _, p := range providers {
if p.Name == ProviderAliyun {
foundAliyun = true
}
if p.Name == ProviderLKEAP {
foundLKEAP = true
assert.Equal(t, LKEAPRerankBaseURL, p.GetDefaultURL(types.ModelTypeRerank))
}
if p.Name == ProviderVolcengine {
foundVolcengine = true
assert.Equal(t, VolcengineRerankBaseURL, p.GetDefaultURL(types.ModelTypeRerank))
}
}
assert.True(t, foundAliyun, "Aliyun should support rerank")
assert.True(t, foundLKEAP, "LKEAP should support rerank")
assert.True(t, foundVolcengine, "Volcengine should support rerank")
})
t.Run("embedding models include openrouter", func(t *testing.T) {
providers := ListByModelType(types.ModelTypeEmbedding)
assert.NotEmpty(t, providers)
found := false
for _, p := range providers {
if p.Name == ProviderOpenRouter {
found = true
assert.Equal(t, OpenRouterBaseURL, p.GetDefaultURL(types.ModelTypeEmbedding))
break
}
}
assert.True(t, found, "OpenRouter should support embedding")
})
t.Run("embedding models include gemini", func(t *testing.T) {
providers := ListByModelType(types.ModelTypeEmbedding)
assert.NotEmpty(t, providers)
found := false
for _, p := range providers {
if p.Name == ProviderGemini {
found = true
assert.Equal(t, GeminiBaseURL, p.GetDefaultURL(types.ModelTypeEmbedding))
break
}
}
assert.True(t, found, "Gemini should support embedding via the native Gemini API")
})
}
func TestLiteLLMProviderValidation(t *testing.T) {
p := &LiteLLMProvider{}
t.Run("valid config", func(t *testing.T) {
config := &Config{
APIKey: "test-key",
ModelName: "gpt-4.1-mini",
}
err := p.ValidateConfig(config)
assert.NoError(t, err)
})
t.Run("missing API key", func(t *testing.T) {
config := &Config{
ModelName: "gpt-4.1-mini",
}
err := p.ValidateConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API key")
})
t.Run("info", func(t *testing.T) {
info := p.Info()
assert.Equal(t, ProviderLiteLLM, info.Name)
assert.Equal(t, "LiteLLM", info.DisplayName)
assert.True(t, info.RequiresAuth)
assert.Equal(t, LiteLLMBaseURL, info.DefaultURLs[types.ModelTypeKnowledgeQA])
assert.Equal(t, LiteLLMBaseURL, info.DefaultURLs[types.ModelTypeEmbedding])
assert.Equal(t, LiteLLMBaseURL, info.DefaultURLs[types.ModelTypeVLLM])
assert.Contains(t, info.ModelTypes, types.ModelTypeKnowledgeQA)
assert.Contains(t, info.ModelTypes, types.ModelTypeEmbedding)
assert.Contains(t, info.ModelTypes, types.ModelTypeVLLM)
})
t.Run("registered and listed", func(t *testing.T) {
got, ok := Get(ProviderLiteLLM)
require.True(t, ok)
require.NotNil(t, got)
foundChat := false
for _, info := range ListByModelType(types.ModelTypeKnowledgeQA) {
if info.Name == ProviderLiteLLM {
foundChat = true
break
}
}
assert.True(t, foundChat, "LiteLLM should appear for chat models")
foundEmbed := false
for _, info := range ListByModelType(types.ModelTypeEmbedding) {
if info.Name == ProviderLiteLLM {
foundEmbed = true
break
}
}
assert.True(t, foundEmbed, "LiteLLM should appear for embedding models")
})
}
+54
View File
@@ -0,0 +1,54 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
QianfanBaseURL = "https://qianfan.baidubce.com/v2"
)
// QianfanProvider 实现百度千帆的 Provider 接口
type QianfanProvider struct{}
func init() {
Register(&QianfanProvider{})
}
// Info 返回百度千帆 provider 的元数据
func (p *QianfanProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderQianfan,
DisplayName: "百度千帆 Baidu Cloud",
Description: "ernie-5.0-thinking-preview, embedding-v1, bce-reranker-base, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: QianfanBaseURL,
types.ModelTypeEmbedding: QianfanBaseURL,
types.ModelTypeRerank: QianfanBaseURL,
types.ModelTypeVLLM: QianfanBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证百度千帆 provider 配置
func (p *QianfanProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for Qianfan provider")
}
if config.APIKey == "" {
return fmt.Errorf("API key is required for Qianfan provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// QiniuBaseURL 七牛云 API BaseURL (OpenAI 兼容模式)
QiniuBaseURL = "https://api.qnaigc.com/v1"
)
// QiniuProvider 实现七牛云的 Provider 接口
type QiniuProvider struct{}
func init() {
Register(&QiniuProvider{})
}
// Info 返回七牛云 provider 的元数据
func (p *QiniuProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderQiniu,
DisplayName: "七牛云 Qiniu",
Description: "deepseek/deepseek-v3.2-251201, z-ai/glm-4.7, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: QiniuBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证七牛云 provider 配置
func (p *QiniuProvider) ValidateConfig(config *Config) error {
if config.BaseURL == "" {
return fmt.Errorf("base URL is required for Qiniu provider")
}
if config.APIKey == "" {
return fmt.Errorf("API key is required for Qiniu provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
RequestyBaseURL = "https://router.requesty.ai/v1"
)
// RequestyProvider 实现 Requesty 的 Provider 接口
type RequestyProvider struct{}
func init() {
Register(&RequestyProvider{})
}
// Info 返回 Requesty provider 的元数据
func (p *RequestyProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderRequesty,
DisplayName: "Requesty",
Description: "openai/gpt-4o-mini, anthropic/claude-sonnet-4-5, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: RequestyBaseURL,
types.ModelTypeEmbedding: RequestyBaseURL,
types.ModelTypeVLLM: RequestyBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证 Requesty provider 配置
func (p *RequestyProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Requesty provider")
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
SiliconFlowBaseURL = "https://api.siliconflow.cn/v1"
)
// SiliconFlowProvider 实现硅基流动的 Provider 接口
type SiliconFlowProvider struct{}
func init() {
Register(&SiliconFlowProvider{})
}
// Info 返回硅基流动 provider 的元数据
func (p *SiliconFlowProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderSiliconFlow,
DisplayName: "硅基流动 SiliconFlow",
Description: "deepseek-ai/DeepSeek-V3.1, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: SiliconFlowBaseURL,
types.ModelTypeEmbedding: SiliconFlowBaseURL,
types.ModelTypeRerank: SiliconFlowBaseURL,
types.ModelTypeVLLM: SiliconFlowBaseURL,
types.ModelTypeASR: SiliconFlowBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
types.ModelTypeASR,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证硅基流动 provider 配置
func (p *SiliconFlowProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for SiliconFlow provider")
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// VolcengineChatBaseURL 火山引擎 Ark Chat API BaseURL (OpenAI 兼容模式)
VolcengineChatBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
// VolcengineEmbeddingBaseURL 火山引擎 Ark Multimodal Embedding API BaseURL
VolcengineEmbeddingBaseURL = "https://ark.cn-beijing.volces.com/api/v3/embeddings/multimodal"
// VolcengineRerankBaseURL 火山引擎知识库托管 Rerank API BaseURL
VolcengineRerankBaseURL = "https://api-knowledgebase.mlp.cn-beijing.volces.com"
)
// VolcengineProvider 实现火山引擎 Ark 的 Provider 接口
type VolcengineProvider struct{}
func init() {
Register(&VolcengineProvider{})
}
// Info 返回火山引擎 provider 的元数据
func (p *VolcengineProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderVolcengine,
DisplayName: "火山引擎 Volcengine",
Description: "doubao-1-5-pro-32k-250115, doubao-embedding-vision-250615, doubao-seed-rerank, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: VolcengineChatBaseURL,
types.ModelTypeEmbedding: VolcengineEmbeddingBaseURL,
types.ModelTypeRerank: VolcengineRerankBaseURL,
types.ModelTypeVLLM: VolcengineChatBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证火山引擎 provider 配置
func (p *VolcengineProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Volcengine Ark provider")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}
+43
View File
@@ -0,0 +1,43 @@
package provider
import "github.com/Tencent/WeKnora/internal/types"
const (
ProviderWeKnoraCloud ProviderName = "weknoracloud"
// WeKnoraCloudBaseURL WeKnoraCloud 服务硬编码 Base URL(统一入口,路径由各实现拼接)
WeKnoraCloudBaseURL = "https://weknora.weixin.qq.com"
)
type WeKnoraCloudProvider struct{}
func init() {
Register(&WeKnoraCloudProvider{})
}
func (p *WeKnoraCloudProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderWeKnoraCloud,
DisplayName: "WeKnoraCloud",
Description: "WeKnora云服务,模型:chat, embedding, rerank, vlm",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: WeKnoraCloudBaseURL,
types.ModelTypeEmbedding: WeKnoraCloudBaseURL,
types.ModelTypeRerank: WeKnoraCloudBaseURL,
types.ModelTypeVLLM: WeKnoraCloudBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
func (p *WeKnoraCloudProvider) ValidateConfig(config *Config) error {
// AppID/AppSecret 通过专用初始化接口写入,此处仅做结构校验。
// 其中 AppSecret 字段当前实际承载上游 API Key。
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package provider
import (
"fmt"
"github.com/Tencent/WeKnora/internal/types"
)
const (
// ZhipuChatBaseURL 智谱 AI Chat 的默认 BaseURL
ZhipuChatBaseURL = "https://open.bigmodel.cn/api/paas/v4"
// ZhipuEmbeddingBaseURL 智谱 AI Embedding 的默认 BaseURL
ZhipuEmbeddingBaseURL = "https://open.bigmodel.cn/api/paas/v4"
// ZhipuRerankBaseURL 智谱 AI Rerank 的默认 BaseURL
ZhipuRerankBaseURL = "https://open.bigmodel.cn/api/paas/v4/rerank"
)
// ZhipuProvider 实现智谱 AI 的 Provider 接口
type ZhipuProvider struct{}
func init() {
Register(&ZhipuProvider{})
}
// Info 返回智谱 AI provider 的元数据
func (p *ZhipuProvider) Info() ProviderInfo {
return ProviderInfo{
Name: ProviderZhipu,
DisplayName: "智谱 BigModel",
Description: "glm-4.7, embedding-3, rerank, etc.",
DefaultURLs: map[types.ModelType]string{
types.ModelTypeKnowledgeQA: ZhipuChatBaseURL,
types.ModelTypeEmbedding: ZhipuEmbeddingBaseURL,
types.ModelTypeRerank: ZhipuRerankBaseURL,
types.ModelTypeVLLM: ZhipuChatBaseURL,
},
ModelTypes: []types.ModelType{
types.ModelTypeKnowledgeQA,
types.ModelTypeEmbedding,
types.ModelTypeRerank,
types.ModelTypeVLLM,
},
RequiresAuth: true,
}
}
// ValidateConfig 验证智谱 AI provider 配置
func (p *ZhipuProvider) ValidateConfig(config *Config) error {
if config.APIKey == "" {
return fmt.Errorf("API key is required for Zhipu AI")
}
if config.ModelName == "" {
return fmt.Errorf("model name is required")
}
return nil
}