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
15 changes: 15 additions & 0 deletions cmd/gateway_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ func registerProviders(registry *providers.Registry, cfg *config.Config, modelRe
slog.Info("registered provider", "name", "zai-coding")
}

if cfg.Providers.Qiniu.APIKey != "" {
base := cfg.Providers.Qiniu.APIBase
if base == "" {
base = store.QiniuDefaultAPIBase
}
registry.Register(providers.NewOpenAIProvider("qiniu", cfg.Providers.Qiniu.APIKey, base, store.QiniuDefaultModel))
slog.Info("registered provider", "name", "qiniu")
}

// Local / self-hosted Ollama — gated on Host, no API key required.
// Ollama's OpenAI-compat endpoint accepts any non-empty Bearer value.
if cfg.Providers.Ollama.Host != "" {
Expand Down Expand Up @@ -367,6 +376,12 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
base = "https://api.z.ai/api/coding/paas/v4"
}
registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "glm-5"))
case store.ProviderQiniu:
base := p.APIBase
if base == "" {
base = store.QiniuDefaultAPIBase
}
registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.QiniuDefaultModel))
case store.ProviderOllamaCloud:
base := p.APIBase
if base == "" {
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config_channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ type ProvidersConfig struct {
Bailian ProviderConfig `json:"bailian"`
Zai ProviderConfig `json:"zai"`
ZaiCoding ProviderConfig `json:"zai_coding"`
Qiniu ProviderConfig `json:"qiniu"` // Qiniu AI Gateway (OpenAI-compatible endpoint)
Ollama OllamaConfig `json:"ollama"` // local Ollama instance (no API key needed)
OllamaCloud ProviderConfig `json:"ollama_cloud"` // Ollama Cloud (API key required)
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
Expand Down Expand Up @@ -284,6 +285,8 @@ func (p *ProvidersConfig) APIBaseForType(providerType string) string {
return p.Zai.APIBase
case "zai_coding":
return p.ZaiCoding.APIBase
case "qiniu":
return p.Qiniu.APIBase
case "ollama_cloud":
return p.OllamaCloud.APIBase
case "novita":
Expand Down Expand Up @@ -315,6 +318,7 @@ func (c *Config) HasAnyProvider() bool {
p.Bailian.APIKey != "" ||
p.Zai.APIKey != "" ||
p.ZaiCoding.APIKey != "" ||
p.Qiniu.APIKey != "" ||
p.Ollama.Host != "" ||
p.OllamaCloud.APIKey != "" ||
p.ClaudeCLI.CLIPath != "" ||
Expand Down
3 changes: 3 additions & 0 deletions internal/config/config_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ func (c *Config) applyEnvOverrides() {
envStr("GOCLAW_BAILIAN_API_KEY", &c.Providers.Bailian.APIKey)
envStr("GOCLAW_ZAI_API_KEY", &c.Providers.Zai.APIKey)
envStr("GOCLAW_ZAI_CODING_API_KEY", &c.Providers.ZaiCoding.APIKey)
envStr("GOCLAW_QINIU_API_KEY", &c.Providers.Qiniu.APIKey)
envStr("QINIU_API_KEY", &c.Providers.Qiniu.APIKey)
envStr("GOCLAW_QINIU_API_BASE", &c.Providers.Qiniu.APIBase)
envStr("GOCLAW_OLLAMA_HOST", &c.Providers.Ollama.Host)
envStr("GOCLAW_OLLAMA_CLOUD_API_KEY", &c.Providers.OllamaCloud.APIKey)
envStr("GOCLAW_OLLAMA_CLOUD_API_BASE", &c.Providers.OllamaCloud.APIBase)
Expand Down
3 changes: 3 additions & 0 deletions internal/config/config_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (c *Config) MaskedCopy() *Config {
maskNonEmpty(&cp.Providers.Bailian.APIKey)
maskNonEmpty(&cp.Providers.Zai.APIKey)
maskNonEmpty(&cp.Providers.ZaiCoding.APIKey)
maskNonEmpty(&cp.Providers.Qiniu.APIKey)
maskNonEmpty(&cp.Providers.OllamaCloud.APIKey)

// Mask gateway token
Expand Down Expand Up @@ -83,6 +84,7 @@ func (c *Config) StripSecrets() {
c.Providers.Bailian.APIKey = ""
c.Providers.Zai.APIKey = ""
c.Providers.ZaiCoding.APIKey = ""
c.Providers.Qiniu.APIKey = ""
c.Providers.OllamaCloud.APIKey = ""

// Gateway token
Expand Down Expand Up @@ -135,6 +137,7 @@ func (c *Config) StripMaskedSecrets() {
stripIfMasked(&c.Providers.Bailian.APIKey)
stripIfMasked(&c.Providers.Zai.APIKey)
stripIfMasked(&c.Providers.ZaiCoding.APIKey)
stripIfMasked(&c.Providers.Qiniu.APIKey)
stripIfMasked(&c.Providers.OllamaCloud.APIKey)

// Gateway token
Expand Down
9 changes: 9 additions & 0 deletions internal/http/provider_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
return
}

if p.ProviderType == store.ProviderQiniu && p.APIKey == "" {
respond(withReasoningCapabilities(qiniuModels()))
return
}

// Ollama: use native /api/tags for richer metadata (parameter size, quantization, family).
// ProviderOllama has no API key; ProviderOllamaCloud requires one but both use the same endpoint.
if p.ProviderType == store.ProviderOllama || p.ProviderType == store.ProviderOllamaCloud {
Expand Down Expand Up @@ -118,6 +123,10 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht

if err != nil {
slog.Warn("providers.models", "provider", p.Name, "error", err)
if p.ProviderType == store.ProviderQiniu {
respond(withReasoningCapabilities(qiniuModels()))
return
}
// Return empty list instead of error — provider may not support /models
respond([]ModelInfo{})
return
Expand Down
7 changes: 7 additions & 0 deletions internal/http/provider_models_catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ func dashScopeModels() []ModelInfo {
}
}

// qiniuModels returns a fallback list for Qiniu when /models cannot be fetched.
func qiniuModels() []ModelInfo {
return []ModelInfo{
{ID: "deepseek-v3", Name: "DeepSeek V3"},
}
}

// claudeCLIModels returns the model aliases accepted by the Claude CLI.
func claudeCLIModels() []ModelInfo {
return []ModelInfo{
Expand Down
38 changes: 38 additions & 0 deletions internal/http/provider_models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,44 @@ func TestProvidersHandlerListProviderModelsOpenAICompatAnnotatesKnownModels(t *t
}
}

func TestProvidersHandlerListProviderModelsQiniuFallbackModel(t *testing.T) {
token := setupProvidersAdminToken(t)
providerStore := newMockProviderStore()
provider := &store.LLMProviderData{
BaseModel: store.BaseModel{ID: uuid.New()},
Name: "qiniu",
ProviderType: store.ProviderQiniu,
Enabled: true,
}
if err := providerStore.CreateProvider(t.Context(), provider); err != nil {
t.Fatalf("CreateProvider() error = %v", err)
}

handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
mux := http.NewServeMux()
handler.RegisterRoutes(mux)

req := httptest.NewRequest(http.MethodGet, "/v1/providers/"+provider.ID.String()+"/models", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Fatalf("status code = %d, want %d, body=%s", w.Code, http.StatusOK, w.Body.String())
}

var result ProviderModelsResponse
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Fatalf("Decode() error = %v", err)
}
if len(result.Models) != 1 {
t.Fatalf("models len = %d, want 1", len(result.Models))
}
if result.Models[0].ID != store.QiniuDefaultModel {
t.Fatalf("models[0].id = %q, want %q", result.Models[0].ID, store.QiniuDefaultModel)
}
}

// TestProvidersHandlerListProviderModelsOllamaRichMetadata verifies that the
// handler fetches /api/tags from Ollama and maps rich details (family,
// parameter_size, quantization_level) into the display name.
Expand Down
6 changes: 6 additions & 0 deletions internal/http/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) {
base = store.NovitaDefaultAPIBase
}
h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.NovitaDefaultModel))
case store.ProviderQiniu:
base := apiBase
if base == "" {
base = store.QiniuDefaultAPIBase
}
h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.QiniuDefaultModel))
default:
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, apiBase, "")
if p.ProviderType == store.ProviderMiniMax {
Expand Down
6 changes: 6 additions & 0 deletions internal/store/provider_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
ProviderOllamaCloud = "ollama_cloud" // Ollama Cloud (Bearer token required)
ProviderACP = "acp" // ACP (Agent Client Protocol) agent subprocess
ProviderNovita = "novita" // Novita AI (OpenAI-compatible endpoint)
ProviderQiniu = "qiniu" // Qiniu AI Gateway (OpenAI-compatible endpoint)
ProviderBytePlus = "byteplus" // BytePlus ModelArk (Seed 2.0 models)
ProviderBytePlusCoding = "byteplus_coding" // BytePlus ModelArk Coding Plan

Expand All @@ -42,6 +43,10 @@ const (
BytePlusDefaultAPIBase = "https://ark.ap-southeast.bytepluses.com/api/v3"
BytePlusCodingDefaultAPIBase = "https://ark.ap-southeast.bytepluses.com/api/coding/v3"
BytePlusDefaultModel = "seed-2-0-lite-260228"

// Qiniu AI Gateway defaults.
QiniuDefaultAPIBase = "https://api.qnaigc.com/v1"
QiniuDefaultModel = "deepseek-v3"
)

// ValidProviderTypes lists all accepted provider_type values.
Expand All @@ -68,6 +73,7 @@ var ValidProviderTypes = map[string]bool{
ProviderOllamaCloud: true,
ProviderACP: true,
ProviderNovita: true,
ProviderQiniu: true,
ProviderBytePlus: true,
ProviderBytePlusCoding: true,
}
Expand Down
1 change: 1 addition & 0 deletions ui/desktop/frontend/src/constants/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [
{ value: 'yescale', label: 'YesScale', apiBase: 'https://api.yescale.one/v1', needsKey: true },
{ value: 'zai', label: 'Z.ai API', apiBase: 'https://api.z.ai/api/paas/v4', needsKey: true },
{ value: 'zai_coding', label: 'Z.ai Coding Plan', apiBase: 'https://api.z.ai/api/coding/paas/v4', needsKey: true },
{ value: 'qiniu', label: 'Qiniu', apiBase: 'https://api.qnaigc.com/v1', needsKey: true },
{ value: 'byteplus', label: 'BytePlus ModelArk', apiBase: 'https://ark.ap-southeast.bytepluses.com/api/v3', needsKey: true },
{ value: 'byteplus_coding', label: 'BytePlus Coding Plan', apiBase: 'https://ark.ap-southeast.bytepluses.com/api/coding/v3', needsKey: true },
{ value: 'ollama', label: 'Ollama (Local)', apiBase: 'http://localhost:11434/v1', needsKey: false },
Expand Down
1 change: 1 addition & 0 deletions ui/web/src/constants/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [
{ value: "yescale", label: "YesScale", apiBase: "https://api.yescale.one/v1", placeholder: "" },
{ value: "zai", label: "Z.ai API", apiBase: "https://api.z.ai/api/paas/v4", placeholder: "" },
{ value: "zai_coding", label: "Z.ai Coding Plan", apiBase: "https://api.z.ai/api/coding/paas/v4", placeholder: "" },
{ value: "qiniu", label: "Qiniu", apiBase: "https://api.qnaigc.com/v1", placeholder: "" },
{ value: "byteplus", label: "BytePlus ModelArk", apiBase: "https://ark.ap-southeast.bytepluses.com/api/v3", placeholder: "" },
{ value: "byteplus_coding", label: "BytePlus Coding Plan", apiBase: "https://ark.ap-southeast.bytepluses.com/api/coding/v3", placeholder: "" },
{ value: "ollama", label: "Ollama (Local)", apiBase: "http://localhost:11434/v1", placeholder: "" },
Expand Down