diff --git a/README.md b/README.md
index f2bd53c9..2a47b94e 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://go.dev/) [](https://www.postgresql.org/) [](https://www.docker.com/) [](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) [](https://opentelemetry.io/) [](https://www.anthropic.com/) [](https://openai.com/) [](LICENSE)
-**GoClaw** is a multi-agent AI gateway that connects LLMs to your tools, channels, and data — deployed as a single Go binary with zero runtime dependencies. It orchestrates agent teams, inter-agent delegation, and quality-gated workflows across 11+ LLM providers with full multi-tenant isolation.
+**GoClaw** is a multi-agent AI gateway that connects LLMs to your tools, channels, and data — deployed as a single Go binary with zero runtime dependencies. It orchestrates agent teams, inter-agent delegation, and quality-gated workflows across 13+ LLM providers with full multi-tenant isolation.
A Go port of [OpenClaw](https://github.com/openclaw/openclaw) with enhanced security, multi-tenant PostgreSQL, and production-grade observability.
@@ -16,7 +16,7 @@ A Go port of [OpenClaw](https://github.com/openclaw/openclaw) with enhanced secu
- **Multi-Tenant PostgreSQL** — Per-user workspaces, per-user context files, encrypted API keys (AES-256-GCM), isolated sessions — the only Claw project with DB-native multi-tenancy
- **Single Binary** — ~25 MB static Go binary, no Node.js runtime, <1s startup, runs on a $5 VPS
- **Production Security** — 5-layer defense: rate limiting, prompt injection detection, SSRF protection, shell deny patterns, AES-256-GCM encryption
-- **11+ LLM Providers** — Anthropic (native HTTP+SSE with prompt caching), OpenAI, OpenRouter, Groq, DeepSeek, Gemini, Mistral, xAI, MiniMax, Cohere, Perplexity
+- **13+ LLM Providers** — Anthropic (native HTTP+SSE with prompt caching), OpenAI, OpenRouter, Groq, DeepSeek, Gemini, Mistral, xAI, MiniMax, Cohere, Perplexity, DashScope (Qwen), Bailian Coding
- **5 Messaging Channels** — Telegram, Discord, Zalo, Feishu/Lark, WhatsApp with `/stop` and `/stopall` commands
## Claw Ecosystem
@@ -51,7 +51,7 @@ A Go port of [OpenClaw](https://github.com/openclaw/openclaw) with enhanced secu
| Messaging channels | 37+ | 15+ | 10+ | 5+ |
| Companion apps | macOS, iOS, Android | Python SDK | — | Web dashboard |
| Live Canvas / Voice | ✅ (A2UI + TTS/STT) | — | Voice transcription | TTS (4 providers) |
-| LLM providers | 10+ | 8 native + 29 compat | 13+ | **11+** |
+| LLM providers | 10+ | 8 native + 29 compat | 13+ | **13+** |
| Per-user workspaces | ✅ (file-based) | — | — | ✅ (managed mode only) |
| Encrypted secrets | — (env vars only) | ✅ ChaCha20-Poly1305 | — (plaintext JSON) | ✅ AES-256-GCM in DB |
@@ -78,7 +78,7 @@ graph TB
SCHED --> ROUTER["Agent Router"]
ROUTER --> LOOP["Agent Loop
think → act → observe"]
LOOP --> TOOLS["Tool Registry
fs · exec · web · memory · delegate · team · mcp · custom"]
- LOOP --> LLM["LLM Providers
Anthropic (native + prompt caching) · OpenAI-compat (10+)"]
+ LOOP --> LLM["LLM Providers
Anthropic (native + prompt caching) · OpenAI-compat (12+)"]
end
subgraph Storage
@@ -294,7 +294,7 @@ Quality gates validate agent output before it reaches users. Configured in agent
## Features
### LLM Providers
-- **11+ providers** — OpenRouter, Anthropic, OpenAI, Groq, DeepSeek, Gemini, Mistral, xAI, MiniMax, Cohere, Perplexity, and any OpenAI-compatible endpoint
+- **13+ providers** — OpenRouter, Anthropic, OpenAI, Groq, DeepSeek, Gemini, Mistral, xAI, MiniMax, Cohere, Perplexity, DashScope (Qwen), Bailian Coding, and any OpenAI-compatible endpoint
- **Anthropic native** — Direct HTTP+SSE integration with prompt caching (`cache_control`) for ~90% cost reduction on repeated prefixes
- **OpenAI-compatible** — Automatic prompt caching for OpenAI, MiniMax, OpenRouter (cache metrics tracked in traces)
@@ -557,6 +557,8 @@ When `GOCLAW_*_API_KEY` environment variables are set, the gateway automatically
| `GOCLAW_MINIMAX_API_KEY` | MiniMax |
| `GOCLAW_COHERE_API_KEY` | Cohere |
| `GOCLAW_PERPLEXITY_API_KEY` | Perplexity |
+| `GOCLAW_DASHSCOPE_API_KEY` | DashScope (Qwen) |
+| `GOCLAW_BAILIAN_API_KEY` | Bailian Coding |
diff --git a/cmd/gateway_providers.go b/cmd/gateway_providers.go
index 9e2a69a1..e0396d76 100644
--- a/cmd/gateway_providers.go
+++ b/cmd/gateway_providers.go
@@ -66,6 +66,20 @@ func registerProviders(registry *providers.Registry, cfg *config.Config) {
registry.Register(providers.NewOpenAIProvider("perplexity", cfg.Providers.Perplexity.APIKey, "https://api.perplexity.ai", "sonar-pro"))
slog.Info("registered provider", "name", "perplexity")
}
+
+ if cfg.Providers.DashScope.APIKey != "" {
+ registry.Register(providers.NewDashScopeProvider(cfg.Providers.DashScope.APIKey, cfg.Providers.DashScope.APIBase, "qwen3-max"))
+ slog.Info("registered provider", "name", "dashscope")
+ }
+
+ if cfg.Providers.Bailian.APIKey != "" {
+ base := cfg.Providers.Bailian.APIBase
+ if base == "" {
+ base = "https://coding-intl.dashscope.aliyuncs.com/v1"
+ }
+ registry.Register(providers.NewOpenAIProvider("bailian", cfg.Providers.Bailian.APIKey, base, "qwen3.5-plus"))
+ slog.Info("registered provider", "name", "bailian")
+ }
}
// registerProvidersFromDB loads providers from Postgres and registers them.
@@ -84,6 +98,14 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
if p.ProviderType == store.ProviderAnthropicNative {
registry.Register(providers.NewAnthropicProvider(p.APIKey,
providers.WithAnthropicBaseURL(p.APIBase)))
+ } else if p.ProviderType == store.ProviderDashScope {
+ registry.Register(providers.NewDashScopeProvider(p.APIKey, p.APIBase, ""))
+ } else if p.ProviderType == store.ProviderBailian {
+ base := p.APIBase
+ if base == "" {
+ base = "https://coding-intl.dashscope.aliyuncs.com/v1"
+ }
+ registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "qwen3.5-plus"))
} else {
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "")
if p.ProviderType == store.ProviderMiniMax {
diff --git a/internal/agent/loop.go b/internal/agent/loop.go
index 5d9e811d..6b574bb8 100644
--- a/internal/agent/loop.go
+++ b/internal/agent/loop.go
@@ -553,6 +553,14 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
if req.Stream {
resp, err = l.provider.ChatStream(ctx, chatReq, func(chunk providers.StreamChunk) {
+ if chunk.Thinking != "" {
+ l.emit(AgentEvent{
+ Type: protocol.ChatEventThinking,
+ AgentID: l.id,
+ RunID: req.RunID,
+ Payload: map[string]string{"content": chunk.Thinking},
+ })
+ }
if chunk.Content != "" {
l.emit(AgentEvent{
Type: protocol.ChatEventChunk,
diff --git a/internal/config/config_channels.go b/internal/config/config_channels.go
index 49a5a001..225e2124 100644
--- a/internal/config/config_channels.go
+++ b/internal/config/config_channels.go
@@ -110,6 +110,8 @@ type ProvidersConfig struct {
MiniMax ProviderConfig `json:"minimax"`
Cohere ProviderConfig `json:"cohere"`
Perplexity ProviderConfig `json:"perplexity"`
+ DashScope ProviderConfig `json:"dashscope"`
+ Bailian ProviderConfig `json:"bailian"`
}
type ProviderConfig struct {
@@ -130,7 +132,9 @@ func (c *Config) HasAnyProvider() bool {
p.XAI.APIKey != "" ||
p.MiniMax.APIKey != "" ||
p.Cohere.APIKey != "" ||
- p.Perplexity.APIKey != ""
+ p.Perplexity.APIKey != "" ||
+ p.DashScope.APIKey != "" ||
+ p.Bailian.APIKey != ""
}
// GatewayConfig controls the gateway server.
diff --git a/internal/config/config_load.go b/internal/config/config_load.go
index 09eaa8cc..b2a2d0e7 100644
--- a/internal/config/config_load.go
+++ b/internal/config/config_load.go
@@ -105,6 +105,8 @@ func (c *Config) applyEnvOverrides() {
envStr("GOCLAW_MINIMAX_API_KEY", &c.Providers.MiniMax.APIKey)
envStr("GOCLAW_COHERE_API_KEY", &c.Providers.Cohere.APIKey)
envStr("GOCLAW_PERPLEXITY_API_KEY", &c.Providers.Perplexity.APIKey)
+ envStr("GOCLAW_DASHSCOPE_API_KEY", &c.Providers.DashScope.APIKey)
+ envStr("GOCLAW_BAILIAN_API_KEY", &c.Providers.Bailian.APIKey)
envStr("GOCLAW_GATEWAY_TOKEN", &c.Gateway.Token)
envStr("GOCLAW_TELEGRAM_TOKEN", &c.Channels.Telegram.Token)
envStr("GOCLAW_DISCORD_TOKEN", &c.Channels.Discord.Token)
diff --git a/internal/config/config_secrets.go b/internal/config/config_secrets.go
index 6a8a4cb4..8dd74ae9 100644
--- a/internal/config/config_secrets.go
+++ b/internal/config/config_secrets.go
@@ -32,6 +32,8 @@ func (c *Config) MaskedCopy() *Config {
maskNonEmpty(&cp.Providers.MiniMax.APIKey)
maskNonEmpty(&cp.Providers.Cohere.APIKey)
maskNonEmpty(&cp.Providers.Perplexity.APIKey)
+ maskNonEmpty(&cp.Providers.DashScope.APIKey)
+ maskNonEmpty(&cp.Providers.Bailian.APIKey)
// Mask gateway token
maskNonEmpty(&cp.Gateway.Token)
@@ -77,6 +79,8 @@ func (c *Config) StripSecrets() {
c.Providers.MiniMax.APIKey = ""
c.Providers.Cohere.APIKey = ""
c.Providers.Perplexity.APIKey = ""
+ c.Providers.DashScope.APIKey = ""
+ c.Providers.Bailian.APIKey = ""
// Gateway token
c.Gateway.Token = ""
@@ -127,6 +131,8 @@ func (c *Config) StripMaskedSecrets() {
stripIfMasked(&c.Providers.MiniMax.APIKey)
stripIfMasked(&c.Providers.Cohere.APIKey)
stripIfMasked(&c.Providers.Perplexity.APIKey)
+ stripIfMasked(&c.Providers.DashScope.APIKey)
+ stripIfMasked(&c.Providers.Bailian.APIKey)
// Gateway token
stripIfMasked(&c.Gateway.Token)
diff --git a/internal/http/provider_models.go b/internal/http/provider_models.go
index 2fc0a1d7..b5c69f9f 100644
--- a/internal/http/provider_models.go
+++ b/internal/http/provider_models.go
@@ -51,6 +51,8 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
models, err = fetchAnthropicModels(ctx, p.APIKey)
case "gemini_native":
models, err = fetchGeminiModels(ctx, p.APIKey)
+ case "bailian":
+ models = bailianModels()
default:
// All other types use OpenAI-compatible /models endpoint
apiBase := strings.TrimRight(p.APIBase, "/")
@@ -145,6 +147,22 @@ func fetchGeminiModels(ctx context.Context, apiKey string) ([]ModelInfo, error)
return models, nil
}
+// bailianModels returns a hardcoded list of models available on the
+// Bailian Coding platform (coding-intl.dashscope.aliyuncs.com).
+// The platform does not expose a /v1/models endpoint.
+func bailianModels() []ModelInfo {
+ return []ModelInfo{
+ {ID: "qwen3.5-plus", Name: "Qwen 3.5 Plus"},
+ {ID: "kimi-k2.5", Name: "Kimi K2.5"},
+ {ID: "GLM-5", Name: "GLM-5"},
+ {ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
+ {ID: "qwen3-max-2026-01-23", Name: "Qwen 3 Max (2026-01-23)"},
+ {ID: "qwen3-coder-next", Name: "Qwen 3 Coder Next"},
+ {ID: "qwen3-coder-plus", Name: "Qwen 3 Coder Plus"},
+ {ID: "glm-4.7", Name: "GLM 4.7"},
+ }
+}
+
// fetchOpenAIModels calls an OpenAI-compatible /models endpoint.
func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string) ([]ModelInfo, error) {
req, err := http.NewRequestWithContext(ctx, "GET", apiBase+"/models", nil)
@@ -180,3 +198,4 @@ func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string) ([]ModelInfo
}
return models, nil
}
+
diff --git a/internal/http/providers.go b/internal/http/providers.go
index f316640e..803e175a 100644
--- a/internal/http/providers.go
+++ b/internal/http/providers.go
@@ -67,6 +67,14 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) {
if p.ProviderType == store.ProviderAnthropicNative {
h.providerReg.Register(providers.NewAnthropicProvider(p.APIKey,
providers.WithAnthropicBaseURL(p.APIBase)))
+ } else if p.ProviderType == store.ProviderDashScope {
+ h.providerReg.Register(providers.NewDashScopeProvider(p.APIKey, p.APIBase, ""))
+ } else if p.ProviderType == store.ProviderBailian {
+ base := p.APIBase
+ if base == "" {
+ base = "https://coding-intl.dashscope.aliyuncs.com/v1"
+ }
+ h.providerReg.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "qwen3.5-plus"))
} else {
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "")
if p.ProviderType == store.ProviderMiniMax {
diff --git a/internal/providers/dashscope.go b/internal/providers/dashscope.go
new file mode 100644
index 00000000..b08df9b0
--- /dev/null
+++ b/internal/providers/dashscope.go
@@ -0,0 +1,56 @@
+package providers
+
+import (
+ "context"
+ "log/slog"
+)
+
+const (
+ dashscopeDefaultBase = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
+ dashscopeDefaultModel = "qwen3-max"
+)
+
+// DashScopeProvider wraps OpenAIProvider to handle DashScope-specific behaviors.
+// Critical: DashScope does NOT support tools + streaming simultaneously.
+// When tools are present, ChatStream falls back to non-streaming Chat().
+type DashScopeProvider struct {
+ *OpenAIProvider
+}
+
+func NewDashScopeProvider(apiKey, apiBase, defaultModel string) *DashScopeProvider {
+ if apiBase == "" {
+ apiBase = dashscopeDefaultBase
+ }
+ if defaultModel == "" {
+ defaultModel = dashscopeDefaultModel
+ }
+ return &DashScopeProvider{
+ OpenAIProvider: NewOpenAIProvider("dashscope", apiKey, apiBase, defaultModel),
+ }
+}
+
+func (p *DashScopeProvider) Name() string { return "dashscope" }
+
+// ChatStream handles DashScope's limitation: tools + streaming cannot coexist.
+// When tools are present, falls back to non-streaming Chat() and synthesizes
+// chunk callbacks for the caller.
+func (p *DashScopeProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk)) (*ChatResponse, error) {
+ if len(req.Tools) > 0 {
+ slog.Debug("dashscope: tools present, falling back to non-streaming Chat")
+ resp, err := p.Chat(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+ if onChunk != nil {
+ if resp.Thinking != "" {
+ onChunk(StreamChunk{Thinking: resp.Thinking})
+ }
+ if resp.Content != "" {
+ onChunk(StreamChunk{Content: resp.Content})
+ }
+ onChunk(StreamChunk{Done: true})
+ }
+ return resp, nil
+ }
+ return p.OpenAIProvider.ChatStream(ctx, req, onChunk)
+}
diff --git a/internal/providers/openai.go b/internal/providers/openai.go
index 22b8f8b6..d7805538 100644
--- a/internal/providers/openai.go
+++ b/internal/providers/openai.go
@@ -122,6 +122,12 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun
}
delta := chunk.Choices[0].Delta
+ if delta.ReasoningContent != "" {
+ result.Thinking += delta.ReasoningContent
+ if onChunk != nil {
+ onChunk(StreamChunk{Thinking: delta.ReasoningContent})
+ }
+ }
if delta.Content != "" {
result.Content += delta.Content
if onChunk != nil {
@@ -326,6 +332,7 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse {
if len(resp.Choices) > 0 {
msg := resp.Choices[0].Message
result.Content = msg.Content
+ result.Thinking = msg.ReasoningContent
result.FinishReason = resp.Choices[0].FinishReason
for _, tc := range msg.ToolCalls {
diff --git a/internal/providers/openai_types.go b/internal/providers/openai_types.go
index 0458b2bb..4de0aaec 100644
--- a/internal/providers/openai_types.go
+++ b/internal/providers/openai_types.go
@@ -13,9 +13,10 @@ type openAIChoice struct {
}
type openAIMessage struct {
- Role string `json:"role"`
- Content string `json:"content"`
- ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
+ Role string `json:"role"`
+ Content string `json:"content"`
+ ReasoningContent string `json:"reasoning_content,omitempty"`
+ ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
}
type openAIToolCall struct {
@@ -54,8 +55,9 @@ type openAIStreamChoice struct {
}
type openAIStreamDelta struct {
- Content string `json:"content,omitempty"`
- ToolCalls []openAIStreamToolCall `json:"tool_calls,omitempty"`
+ Content string `json:"content,omitempty"`
+ ReasoningContent string `json:"reasoning_content,omitempty"`
+ ToolCalls []openAIStreamToolCall `json:"tool_calls,omitempty"`
}
type openAIStreamToolCall struct {
diff --git a/internal/providers/types.go b/internal/providers/types.go
index 4434be77..6d52dc91 100644
--- a/internal/providers/types.go
+++ b/internal/providers/types.go
@@ -30,6 +30,7 @@ type ChatRequest struct {
// ChatResponse is the result from an LLM call.
type ChatResponse struct {
Content string `json:"content"`
+ Thinking string `json:"thinking,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FinishReason string `json:"finish_reason"` // "stop", "tool_calls", "length"
Usage *Usage `json:"usage,omitempty"`
diff --git a/internal/store/provider_store.go b/internal/store/provider_store.go
index d2e44658..28fe9f0a 100644
--- a/internal/store/provider_store.go
+++ b/internal/store/provider_store.go
@@ -19,6 +19,8 @@ const (
ProviderMiniMax = "minimax_native"
ProviderCohere = "cohere"
ProviderPerplexity = "perplexity"
+ ProviderDashScope = "dashscope"
+ ProviderBailian = "bailian"
)
// ValidProviderTypes lists all accepted provider_type values.
@@ -34,6 +36,8 @@ var ValidProviderTypes = map[string]bool{
ProviderMiniMax: true,
ProviderCohere: true,
ProviderPerplexity: true,
+ ProviderDashScope: true,
+ ProviderBailian: true,
}
// LLMProviderData represents an LLM provider configuration.
diff --git a/internal/tools/read_image.go b/internal/tools/read_image.go
index c1d777c6..3126410f 100644
--- a/internal/tools/read_image.go
+++ b/internal/tools/read_image.go
@@ -27,12 +27,13 @@ func MediaImagesFromCtx(ctx context.Context) []providers.ImageContent {
// --- ReadImageTool ---
// visionProviderPriority is the order in which providers are tried for vision.
-var visionProviderPriority = []string{"openrouter", "gemini", "anthropic"}
+var visionProviderPriority = []string{"openrouter", "gemini", "anthropic", "dashscope"}
// visionModelOverrides maps provider names to preferred vision models.
// Providers not listed here use their default model.
var visionModelOverrides = map[string]string{
"openrouter": "google/gemini-2.5-flash-image",
+ "dashscope": "qwen3-vl",
}
// ReadImageTool uses a vision-capable provider to describe images attached to the current message.
diff --git a/ui/web/src/pages/providers/provider-form-dialog.tsx b/ui/web/src/pages/providers/provider-form-dialog.tsx
index 20e139ed..5439848b 100644
--- a/ui/web/src/pages/providers/provider-form-dialog.tsx
+++ b/ui/web/src/pages/providers/provider-form-dialog.tsx
@@ -40,6 +40,8 @@ const PROVIDER_TYPES = [
{ value: "minimax_native", label: "MiniMax (Native)", apiBase: "https://api.minimax.io/v1", placeholder: "" },
{ value: "cohere", label: "Cohere", apiBase: "https://api.cohere.ai/compatibility/v1", placeholder: "" },
{ value: "perplexity", label: "Perplexity", apiBase: "https://api.perplexity.ai", placeholder: "" },
+ { value: "dashscope", label: "DashScope (Qwen)", apiBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", placeholder: "" },
+ { value: "bailian", label: "Bailian Coding", apiBase: "https://coding-intl.dashscope.aliyuncs.com/v1", placeholder: "" },
];
export function ProviderFormDialog({ open, onOpenChange, provider, onSubmit }: ProviderFormDialogProps) {
@@ -139,7 +141,7 @@ export function ProviderFormDialog({ open, onOpenChange, provider, onSubmit }: P
setProviderType(v);
if (!isEdit) {
const preset = PROVIDER_TYPES.find((t) => t.value === v);
- if (preset?.apiBase) setApiBase(preset.apiBase);
+ setApiBase(preset?.apiBase || "");
}
}}
>