Skip to content
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -820,17 +820,18 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
> [!NOTE]
> Groq provides free voice transcription via Whisper. If configured, Telegram voice messages will be automatically transcribed.

| Provider | Purpose | Get API Key |
| -------------------------- | --------------------------------------- | -------------------------------------------------------------------- |
| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (Zhipu direct) | [bigmodel.cn](https://bigmodel.cn) |
| `openrouter(To be tested)` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(To be tested)` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(To be tested)` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(To be tested)` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
| `cerebras` | LLM (Cerebras direct) | [cerebras.ai](https://cerebras.ai) |
| Provider | Purpose | Get API Key |
| --------------------------- | --------------------------------------- | -------------------------------------------------------------------- |
| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (Zhipu direct) | [bigmodel.cn](https://bigmodel.cn) |
| `openrouter(To be tested)` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(To be tested)` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(To be tested)` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(To be tested)` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (Qwen API Key) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `qwen-oauth` | LLM (Qwen OAuth QR login) | `picoclaw auth login --provider qwen` |
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
| `cerebras` | LLM (Cerebras direct) | [cerebras.ai](https://cerebras.ai) |

### Model Configuration (model_list)

Expand All @@ -854,7 +855,8 @@ This design also enables **multi-agent support** with flexible provider selectio
| **Google Gemini** | `gemini/` | `https://generativelanguage.googleapis.com/v1beta` | OpenAI | [Get Key](https://aistudio.google.com/api-keys) |
| **Groq** | `groq/` | `https://api.groq.com/openai/v1` | OpenAI | [Get Key](https://console.groq.com) |
| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [Get Key](https://platform.moonshot.cn) |
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [Get Key](https://dashscope.console.aliyun.com) |
| **Qwen** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [Get Key](https://dashscope.console.aliyun.com) |
| **Qwen (OAuth)** | `qwen-oauth/` | `https://portal.qwen.ai/v1` | OpenAI | `picoclaw auth login --provider qwen` |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [Get Key](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Get Key](https://openrouter.ai/keys) |
Expand Down Expand Up @@ -938,6 +940,18 @@ This design also enables **multi-agent support** with flexible provider selectio

> Run `picoclaw auth login --provider anthropic` to paste your API token.

**Qwen (OAuth - QR login)**

```json
{
"model_name": "qwen-coder",
"model": "qwen-oauth/coder-model",
"auth_method": "oauth"
}
```

> Run `picoclaw auth login --provider qwen` to scan QR code and login. Also supports aliases: `qwen-oauth`, `qwenoauth`, or `qwen-portal`.

**Ollama (local)**

```json
Expand Down
36 changes: 25 additions & 11 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,17 +420,18 @@ Agent 读取 HEARTBEAT.md
> [!NOTE]
> Groq 通过 Whisper 提供免费的语音转录。如果配置了 Groq,Telegram 语音消息将被自动转录为文字。

| 提供商 | 用途 | 获取 API Key |
| -------------------- | ---------------------------- | -------------------------------------------------------------------- |
| `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) |
| `openrouter(待测试)` | LLM (推荐,可访问所有模型) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(待测试)` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(待测试)` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(待测试)` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (通义千问) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) |
| `cerebras` | LLM (Cerebras 直连) | [cerebras.ai](https://cerebras.ai) |
| 提供商 | 用途 | 获取 API Key |
| --------------------- | ---------------------------- | -------------------------------------------------------------------- |
| `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) |
| `openrouter(待测试)` | LLM (推荐,可访问所有模型) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(待测试)` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(待测试)` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(待测试)` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (通义千问 API Key) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `qwen-oauth` | LLM (通义千问 OAuth 扫码) | `picoclaw auth login --provider qwen` |
| `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) |
| `cerebras` | LLM (Cerebras 直连) | [cerebras.ai](https://cerebras.ai) |

### 模型配置 (model_list)

Expand All @@ -455,6 +456,7 @@ Agent 读取 HEARTBEAT.md
| **Groq** | `groq/` | `https://api.groq.com/openai/v1` | OpenAI | [获取密钥](https://console.groq.com) |
| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [获取密钥](https://platform.moonshot.cn) |
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [获取密钥](https://dashscope.console.aliyun.com) |
| **通义千问 (OAuth)**| `qwen-oauth/` | `https://portal.qwen.ai/v1` | OpenAI | `picoclaw auth login --provider qwen` |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取密钥](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
Expand Down Expand Up @@ -538,6 +540,18 @@ Agent 读取 HEARTBEAT.md

> 运行 `picoclaw auth login --provider anthropic` 来设置 OAuth 凭证。

**通义千问 (Qwen OAuth - 扫码登录)**

```json
{
"model_name": "qwen-coder",
"model": "qwen-oauth/coder-model",
"auth_method": "oauth"
}
```

> 运行 `picoclaw auth login --provider qwen` 进行扫码登录。也支持简写:`qwen-oauth`、`qwenoauth` 或 `qwen-portal`。

**Ollama (本地)**

```json
Expand Down
71 changes: 70 additions & 1 deletion cmd/picoclaw/internal/auth/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/sipeed/picoclaw/pkg/providers"
)

const supportedProvidersMsg = "supported providers: openai, anthropic, google-antigravity"
const supportedProvidersMsg = "supported providers: openai, anthropic, google-antigravity, qwen"

func authLoginCmd(provider string, useDeviceCode bool) error {
switch provider {
Expand All @@ -25,6 +25,8 @@ func authLoginCmd(provider string, useDeviceCode bool) error {
return authLoginPasteToken(provider)
case "google-antigravity", "antigravity":
return authLoginGoogleAntigravity()
case "qwen":
return authLoginQwen()
default:
return fmt.Errorf("unsupported provider: %s (%s)", provider, supportedProvidersMsg)
}
Expand Down Expand Up @@ -282,6 +284,10 @@ func authLogoutCmd(provider string) error {
if isAntigravityModel(appCfg.ModelList[i].Model) {
appCfg.ModelList[i].AuthMethod = ""
}
case "qwen":
if isQwenModel(appCfg.ModelList[i].Model) {
appCfg.ModelList[i].AuthMethod = ""
}
}
}
// Clear AuthMethod in Providers (legacy)
Expand All @@ -292,6 +298,8 @@ func authLogoutCmd(provider string) error {
appCfg.Providers.Anthropic.AuthMethod = ""
case "google-antigravity", "antigravity":
appCfg.Providers.Antigravity.AuthMethod = ""
case "qwen":
appCfg.Providers.Qwen.AuthMethod = ""
}
config.SaveConfig(internal.GetConfigPath(), appCfg)
}
Expand All @@ -315,6 +323,7 @@ func authLogoutCmd(provider string) error {
appCfg.Providers.OpenAI.AuthMethod = ""
appCfg.Providers.Anthropic.AuthMethod = ""
appCfg.Providers.Antigravity.AuthMethod = ""
appCfg.Providers.Qwen.AuthMethod = ""
config.SaveConfig(internal.GetConfigPath(), appCfg)
}

Expand Down Expand Up @@ -435,3 +444,63 @@ func isAnthropicModel(model string) bool {
return model == "anthropic" ||
strings.HasPrefix(model, "anthropic/")
}

// isQwenModel checks if a model string belongs to the qwen provider
func isQwenModel(model string) bool {
return model == "qwen" ||
model == "qwen-oauth" ||
strings.HasPrefix(model, "qwen/") ||
strings.HasPrefix(model, "qwen-oauth/")
}

// authLoginQwen performs the Qwen Portal OAuth device-code (QR scan) login flow.
func authLoginQwen() error {
cred, err := auth.LoginQwenQRCode()
if err != nil {
return fmt.Errorf("login failed: %w", err)
}

if err = auth.SetCredential("qwen", cred); err != nil {
return fmt.Errorf("failed to save credentials: %w", err)
}

appCfg, cfgErr := internal.LoadConfig()
if cfgErr == nil {
// Update or add qwen entry in ModelList.
found := false
for i := range appCfg.ModelList {
if isQwenModel(appCfg.ModelList[i].Model) {
appCfg.ModelList[i].Model = "qwen-portal/coder-model"
appCfg.ModelList[i].APIBase = "https://portal.qwen.ai/v1"
appCfg.ModelList[i].APIKey = "qwen-oauth"
appCfg.ModelList[i].AuthMethod = "oauth"
found = true
break
}
}
if !found {
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
ModelName: "qwen-coder",
Model: "qwen-portal/coder-model",
APIBase: "https://portal.qwen.ai/v1",
APIKey: "qwen-oauth",
AuthMethod: "oauth",
})
}

// Update default model.
appCfg.Agents.Defaults.ModelName = "qwen-coder"

if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
fmt.Printf("Warning: could not update config: %v\n", err)
}
}

fmt.Println("\n✓ Qwen OAuth login successful!")
fmt.Println("Default model set to: qwen-coder (coder-model)")
fmt.Println("Available models: qwen-coder, qwen-vision")
//nolint:gosmopolitan // intentional Chinese character for user-facing message
fmt.Println("Try it: picoclaw agent -m \"你好\" --model qwen-coder")

return nil
}
2 changes: 1 addition & 1 deletion cmd/picoclaw/internal/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func newLoginCommand() *cobra.Command {
},
}

cmd.Flags().StringVarP(&provider, "provider", "p", "", "Provider to login with (openai, anthropic)")
cmd.Flags().StringVarP(&provider, "provider", "p", "", "Provider to login with (openai, anthropic, qwen)")
cmd.Flags().BoolVar(&useDeviceCode, "device-code", false, "Use device code flow (for headless environments)")
_ = cmd.MarkFlagRequired("provider")

Expand Down
8 changes: 7 additions & 1 deletion cmd/picoclaw/internal/auth/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ func newLogoutCommand() *cobra.Command {
},
}

cmd.Flags().StringVarP(&provider, "provider", "p", "", "Provider to logout from (openai, anthropic); empty = all")
cmd.Flags().StringVarP(
&provider,
"provider",
"p",
"",
"Provider to logout from (openai, anthropic, qwen); empty = all",
)

return cmd
}
11 changes: 11 additions & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@
"model": "openai/gpt-5.2",
"api_key": "sk-key2",
"api_base": "https://api2.example.com/v1"
},
{
"model_name": "qwen-api",
"model": "qwen/qwen-plus",
"api_key": "sk-your-qwen-api-key",
"api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1"
},
{
"model_name": "qwen-oauth",
"model": "qwen-oauth/coder-model",
"auth_method": "oauth"
}
],
"channels": {
Expand Down
Loading