Skip to content

feat: Add Qwen CLI provider support and merge upstream#1750

Open
dome wants to merge 8 commits intosipeed:mainfrom
domeclaw:main
Open

feat: Add Qwen CLI provider support and merge upstream#1750
dome wants to merge 8 commits intosipeed:mainfrom
domeclaw:main

Conversation

@dome
Copy link

@dome dome commented Mar 18, 2026

📝 Description

🗣️ Type of Change

  • 🐞 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 📖 Documentation update
  • ⚡ Code refactoring (no functional changes, no api changes)

🤖 AI Code Generation

  • 🤖 Fully AI-generated (100% AI, 0% Human)
  • 🛠️ Mostly AI-generated (AI draft, Human verified/modified)
  • 👨‍💻 Mostly Human-written (Human lead, AI assisted or none)

🔗 Related Issue

📚 Technical Context (Skip for Docs)

  • Reference URL:
  • Reasoning:

🧪 Test Environment

  • Hardware:
  • OS:
  • Model/Provider:
  • Channels:

📸 Evidence (Optional)

Click to view Logs/Screenshots

☑️ Checklist

  • My code/docs follow the style of this project.
  • I have performed a self-review of my own changes.
  • I have updated the documentation accordingly.

dome added 2 commits March 18, 2026 16:48
Add support for Qwen Code CLI (qwen) as a new LLM provider.
This enables users to use the qwen CLI tool as a provider,
similar to the existing claude-cli and codex-cli providers.

Changes:
- Add QwenCliProvider implementation with JSON event parsing
- Add comprehensive unit tests (32 test cases)
- Register qwen-cli protocol in factory_provider.go
- Add providerTypeQwenCLI in factory.go
- Add qwen-code to default model list in defaults.go
Copilot AI review requested due to automatic review settings March 18, 2026 14:43
@CLAassistant
Copy link

CLAassistant commented Mar 18, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new local qwen CLI–backed provider to the PicoClaw providers layer, enabling qwen-cli/... models to be selected via config and used like other CLI providers.

Changes:

  • Introduces QwenCliProvider that runs the qwen CLI as a subprocess and parses JSON event output into LLMResponse.
  • Extends provider factories to recognize qwen-cli protocol / qwen-code provider selection.
  • Adds a default qwen-code model entry and comprehensive unit tests for the new provider.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/providers/qwen_cli_provider.go Implements the Qwen CLI provider, prompt building, and JSON event parsing.
pkg/providers/qwen_cli_provider_test.go Adds unit tests for CLI execution behavior, prompt building, JSON parsing, and config factory integration.
pkg/providers/factory.go Allows selecting Qwen CLI via agents.defaults.provider values.
pkg/providers/factory_provider.go Adds qwen-cli protocol handling in CreateProviderFromConfig.
pkg/config/defaults.go Adds a default qwen-code model entry using qwen-cli/qwen-code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
}
case "error":
lastError = event.Error.Message
Comment on lines +74 to +75
if ctx.Err() == context.Canceled {
return nil, ctx.Err()
Comment on lines +333 to +348
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

start := time.Now()
_, err := p.Chat(ctx, []Message{
{Role: "user", Content: "Hello"},
}, nil, "", nil)
elapsed := time.Since(start)

if err == nil {
t.Fatal("Chat() expected error on context cancellation")
}
// Should fail well before the full 2s sleep completes
if elapsed > 3*time.Second {
t.Errorf("Chat() took %v, expected to fail faster via context cancellation", elapsed)
}
Comment on lines +186 to +193
case "qwen-cli", "qwen-code", "qwencode":
workspace := cfg.WorkspacePath()
if workspace == "" {
workspace = "."
}
sel.providerType = providerTypeQwenCLI
sel.workspace = workspace
return sel, nil
Copilot AI review requested due to automatic review settings March 21, 2026 17:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new local Qwen CLI-backed LLM provider to PicoClaw, integrates it into provider factories, and ships a default model_list entry so it can be selected via config.

Changes:

  • Introduce QwenCliProvider that shells out to qwen and parses its JSON event output.
  • Extend provider factory logic to recognize qwen-cli protocol / provider selection paths.
  • Add qwen-code default model entry (qwen-cli/qwen-code) in config defaults, plus comprehensive unit tests for the new provider.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/providers/qwen_cli_provider.go New CLI-based provider implementation and JSON event parsing.
pkg/providers/qwen_cli_provider_test.go Unit/integration-style tests for prompt building, JSON parsing, and factory integration.
pkg/providers/factory.go Adds provider selection routing for qwen-cli / aliases in legacy selection logic.
pkg/providers/factory_provider.go Adds qwen-cli protocol support to CreateProviderFromConfig.
pkg/config/defaults.go Adds a default qwen-code model entry using the new qwen-cli provider.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +212 to +215
usage = &UsageInfo{
PromptTokens: event.Usage.InputTokens,
CompletionTokens: event.Usage.OutputTokens,
TotalTokens: event.Usage.TotalTokens,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In usage parsing, TotalTokens is set directly from event.Usage.TotalTokens, but qwenUsage marks total_tokens as optional (omitempty). If total_tokens is omitted (or zero), this reports TotalTokens as 0 even when input/output tokens are present. Consider computing TotalTokens as InputTokens+OutputTokens when TotalTokens is 0 (and optionally only populating UsageInfo when tokens are non-zero) to match how other providers report usage.

Suggested change
usage = &UsageInfo{
PromptTokens: event.Usage.InputTokens,
CompletionTokens: event.Usage.OutputTokens,
TotalTokens: event.Usage.TotalTokens,
promptTokens := event.Usage.InputTokens
completionTokens := event.Usage.OutputTokens
totalTokens := event.Usage.TotalTokens
if totalTokens == 0 && (promptTokens > 0 || completionTokens > 0) {
totalTokens = promptTokens + completionTokens
}
usage = &UsageInfo{
PromptTokens: promptTokens,
CompletionTokens: completionTokens,
TotalTokens: totalTokens,

Copilot uses AI. Check for mistakes.
}
}
case "error":
lastError = event.Error.Message
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case "error": lastError = event.Error.Message can panic when the CLI emits an "error" event without an error object (i.e., event.Error is nil). Add a nil check (and optionally fall back to event.Result/Type) before dereferencing event.Error.

Suggested change
lastError = event.Error.Message
if event.Error != nil && event.Error.Message != "" {
lastError = event.Error.Message
} else if event.Result != "" {
lastError = event.Result
} else {
lastError = "error event with no message"
}

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +193
case "qwen-cli", "qwen-code", "qwencode":
workspace := cfg.WorkspacePath()
if workspace == "" {
workspace = "."
}
sel.providerType = providerTypeQwenCLI
sel.workspace = workspace
return sel, nil
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new explicit provider branch was added for qwen-cli/qwen-code, but resolveProviderSelection has table-driven tests for other providers and currently has no coverage for this new branch. Add a test case asserting providerTypeQwenCLI is selected and that the workspace defaulting behavior is correct.

Copilot uses AI. Check for mistakes.
Comment on lines +344 to +348
}
// Should fail well before the full 2s sleep completes
if elapsed > 3*time.Second {
t.Errorf("Chat() took %v, expected to fail faster via context cancellation", elapsed)
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context-cancellation test checks elapsed > 3*time.Second, but the mock sleeps for 2s. This assertion will still pass even if cancellation is broken (the command could run the full 2s and still be <=3s). Tighten the bound (e.g., assert elapsed is well below the sleep duration) and/or assert the error is context.DeadlineExceeded.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 23, 2026 09:43
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 3 to 5
import (
"github.com/sipeed/picoclaw/pkg/auth"
)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file references fmt/strings/config and other identifiers (e.g., fmt.Errorf, strings.ToLower, config.Config) but the import block only imports pkg/auth. As-is, pkg/providers won’t compile; add the required imports (and remove unused ones) or split logic into separate files with the correct imports.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +84
sel.apiBase = cfg.Providers.Anthropic.APIBase
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
sel.providerType = providerTypeClaudeAuth
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultAnthropicAPIBase is referenced here but isn’t defined anywhere in pkg/providers (search only finds these call sites). This causes a compile error; either define the constant (likely the default Anthropic base URL) or inline the value / reuse an existing helper.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +80
if err != nil {
if ctx.Err() == context.Canceled {
return nil, ctx.Err()
}
if stderrStr := stderr.String(); stderrStr != "" {
return nil, fmt.Errorf("qwen cli error: %s", stderrStr)
}
return nil, fmt.Errorf("qwen cli error: %w", err)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When exec.CommandContext exits due to a context deadline, ctx.Err() will be context.DeadlineExceeded, but this code only returns ctx.Err() for context.Canceled. Consider returning ctx.Err() whenever it’s non-nil so callers can reliably detect timeouts vs. CLI failures.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 25, 2026 14:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +40
func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
model := cfg.Agents.Defaults.GetModelName()
providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
lowerModel := strings.ToLower(model)

if providerName == "" && model == "" {
return providerSelection{}, fmt.Errorf("no model configured: agents.defaults.model is empty")
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveProviderSelection uses config.Config, fmt, and strings, but factory.go only imports pkg/auth. As-is, this file won’t compile due to missing imports. Add the required imports (and remove any unused ones) so the new provider selection logic builds.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +62
// Supported protocols: openai, litellm, novita, anthropic, anthropic-messages,
// antigravity, claude-cli, codex-cli, qwen-cli, github-copilot
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated comment listing “Supported protocols” is now misleading: CreateProviderFromConfig supports many more protocols (e.g. azure/azure-openai, bedrock, openrouter, groq, etc.) than the short list shown here. Please either expand the comment to match the switch cases, or revert to wording that points readers to the switch as the authoritative list.

Suggested change
// Supported protocols: openai, litellm, novita, anthropic, anthropic-messages,
// antigravity, claude-cli, codex-cli, qwen-cli, github-copilot
// Supported protocols are determined by the switch on `protocol` below;
// see that switch for the authoritative list.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +92
case "anthropic", "claude":
if cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != "" {
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
sel.apiBase = cfg.Providers.Anthropic.APIBase
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
sel.providerType = providerTypeClaudeAuth
return sel, nil
}
sel.apiKey = cfg.Providers.Anthropic.APIKey
sel.apiBase = cfg.Providers.Anthropic.APIBase
sel.proxy = cfg.Providers.Anthropic.Proxy
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultAnthropicAPIBase is referenced here but does not appear to be defined anywhere else in the repo (search shows only these references). This will fail to compile. Either define the constant in providers (consistent with other defaults), or inline the default URL string as done in CreateProviderFromConfig ("https://api.anthropic.com/v1").

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants