Compare commits
2 Commits
main
...
hoyyeva/qwen
| Author | SHA1 | Date | |
|---|---|---|---|
| ddf1078053 | |||
| a95b22c672 |
@@ -1535,6 +1535,11 @@ func TestIntegration_InstallHint(t *testing.T) {
|
||||
input: "unknown",
|
||||
wantEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "qwen uses official install page",
|
||||
input: "qwen",
|
||||
wantURL: "https://qwen.ai/qwencode",
|
||||
},
|
||||
{
|
||||
name: "empty name has no hint",
|
||||
input: "",
|
||||
|
||||
@@ -280,6 +280,7 @@ Supported integrations:
|
||||
openclaw OpenClaw (aliases: clawdbot, moltbot)
|
||||
pi Pi
|
||||
pool Pool
|
||||
qwen Qwen Code
|
||||
vscode VS Code (aliases: code)
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -473,6 +473,81 @@ func TestLaunchIntegration_ManagedSingleIntegrationConfigOnlySkipsFinalRun(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_QwenConfiguresSingleModel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withInteractiveSession(t, true)
|
||||
withLauncherHooks(t)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"gemma4"}]}`)
|
||||
case "/api/show":
|
||||
fmt.Fprint(w, `{"model_info":{"general.context_length":131072}}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
binDir := filepath.Join(tmpDir, "bin")
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create bin dir: %v", err)
|
||||
}
|
||||
writeFakeBinary(t, binDir, "qwen")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
DefaultSingleSelector = func(title string, items []ModelItem, current string) (string, error) {
|
||||
return "gemma4", nil
|
||||
}
|
||||
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{
|
||||
Name: "qwen",
|
||||
ConfigureOnly: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(tmpDir, ".qwen", "settings.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read qwen config: %v", err)
|
||||
}
|
||||
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("failed to parse qwen config: %v", err)
|
||||
}
|
||||
|
||||
modelCfg := cfg["model"].(map[string]any)
|
||||
if modelCfg["name"] != "gemma4" {
|
||||
t.Fatalf("expected model.name gemma4, got %v", modelCfg["name"])
|
||||
}
|
||||
|
||||
modelProviders := cfg["modelProviders"].(map[string]any)
|
||||
openai := modelProviders["openai"].([]any)
|
||||
if len(openai) != 1 {
|
||||
t.Fatalf("expected one provider, got %d", len(openai))
|
||||
}
|
||||
|
||||
saved, err := config.LoadIntegration("qwen")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload qwen integration config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"gemma4"}); diff != "" {
|
||||
t.Fatalf("saved models mismatch: %s", diff)
|
||||
}
|
||||
if !saved.Onboarded {
|
||||
t.Fatal("expected qwen integration to be marked onboarded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_ManagedSingleIntegrationSkipsRewriteWhenSavedMatches(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
@@ -1619,7 +1694,11 @@ func TestLaunchIntegration_EditorForceConfigure(t *testing.T) {
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
editor := &launcherEditorRunner{paths: []string{"/tmp/settings.json"}}
|
||||
settingsPath := filepath.Join(t.TempDir(), "settings.json")
|
||||
if err := os.WriteFile(settingsPath, []byte("{}"), 0o644); err != nil {
|
||||
t.Fatalf("failed to seed editor settings: %v", err)
|
||||
}
|
||||
editor := &launcherEditorRunner{paths: []string{settingsPath}}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
var multiCalled bool
|
||||
@@ -2264,7 +2343,11 @@ func TestLaunchIntegration_ConfiguredEditorLaunchSkipsReconfigure(t *testing.T)
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
editor := &launcherEditorRunner{paths: []string{"/tmp/settings.json"}}
|
||||
settingsPath := filepath.Join(t.TempDir(), "settings.json")
|
||||
if err := os.WriteFile(settingsPath, []byte("{}"), 0o644); err != nil {
|
||||
t.Fatalf("failed to seed editor settings: %v", err)
|
||||
}
|
||||
editor := &launcherEditorRunner{paths: []string{settingsPath}}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2", "qwen3:8b"}); err != nil {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ollama/ollama/cmd/config"
|
||||
"github.com/ollama/ollama/cmd/internal/fileutil"
|
||||
"github.com/ollama/ollama/envconfig"
|
||||
)
|
||||
|
||||
const qwenOllamaEnvKey = "OLLAMA_API_KEY"
|
||||
|
||||
type Qwen struct{}
|
||||
|
||||
func (q *Qwen) String() string { return "Qwen Code" }
|
||||
|
||||
func (q *Qwen) findPath() (string, error) {
|
||||
if p, err := exec.LookPath("qwen"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var candidates []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
candidates = []string{
|
||||
"/opt/homebrew/bin/qwen",
|
||||
"/usr/local/bin/qwen",
|
||||
filepath.Join(home, ".local", "bin", "qwen"),
|
||||
filepath.Join(home, "Library", "Application Support", "qwen", "bin", "qwen"),
|
||||
}
|
||||
case "windows":
|
||||
candidates = []string{
|
||||
filepath.Join(home, "AppData", "Local", "Programs", "qwen", "qwen.exe"),
|
||||
filepath.Join(home, "AppData", "Roaming", "qwen", "bin", "qwen.exe"),
|
||||
}
|
||||
default:
|
||||
candidates = []string{
|
||||
filepath.Join(home, ".local", "bin", "qwen"),
|
||||
filepath.Join(home, ".cargo", "bin", "qwen"),
|
||||
"/usr/local/bin/qwen",
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("qwen binary not found (checked PATH, ~/.local/bin, ~/.cargo/bin, /usr/local/bin)")
|
||||
}
|
||||
|
||||
func (q *Qwen) Run(model string, args []string) error {
|
||||
qwenPath, err := q.findPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("qwen is not installed: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(qwenPath, qwenLaunchArgs(model, args)...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = qwenLaunchEnv(model)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (q *Qwen) Paths() []string {
|
||||
path, err := q.configPath()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return []string{path}
|
||||
}
|
||||
|
||||
func (q *Qwen) Configure(model string) error {
|
||||
if model == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath, err := q.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := q.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg["env"] = map[string]any{
|
||||
qwenOllamaEnvKey: "ollama",
|
||||
}
|
||||
cfg["modelProviders"] = map[string]any{
|
||||
"openai": []map[string]any{qwenProvider(model)},
|
||||
}
|
||||
cfg["security"] = map[string]any{
|
||||
"auth": map[string]any{
|
||||
"selectedType": "openai",
|
||||
"baseUrl": qwenBaseURL(),
|
||||
},
|
||||
}
|
||||
cfg["model"] = map[string]any{
|
||||
"name": model,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fileutil.WriteWithBackup(configPath, data)
|
||||
}
|
||||
|
||||
func (q *Qwen) CurrentModel() string {
|
||||
cfg, err := q.readConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if modelCfg, ok := cfg["model"].(map[string]any); ok {
|
||||
if name, ok := modelCfg["name"].(string); ok {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
}
|
||||
|
||||
modelProviders, ok := cfg["modelProviders"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
providers, ok := modelProviders["openai"].([]any)
|
||||
if !ok || len(providers) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
provider, ok := providers[0].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
name, _ := provider["id"].(string)
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
func (q *Qwen) Onboard() error {
|
||||
return config.MarkIntegrationOnboarded("qwen")
|
||||
}
|
||||
|
||||
func (q *Qwen) RequiresInteractiveOnboarding() bool { return false }
|
||||
|
||||
func (q *Qwen) configPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine config path")
|
||||
}
|
||||
return filepath.Join(home, ".qwen", "settings.json"), nil
|
||||
}
|
||||
|
||||
func (q *Qwen) readConfig() (map[string]any, error) {
|
||||
configPath, err := q.configPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := map[string]any{}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse qwen config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func qwenBaseURL() string {
|
||||
return strings.TrimRight(envconfig.Host().String(), "/") + "/v1"
|
||||
}
|
||||
|
||||
func qwenProvider(model string) map[string]any {
|
||||
return map[string]any{
|
||||
"id": model,
|
||||
"name": fmt.Sprintf("%s (Ollama)", model),
|
||||
"baseUrl": qwenBaseURL(),
|
||||
"envKey": qwenOllamaEnvKey,
|
||||
}
|
||||
}
|
||||
|
||||
func qwenLaunchArgs(model string, args []string) []string {
|
||||
launchArgs := append([]string{}, args...)
|
||||
if !qwenHasFlag(launchArgs, "--auth-type") {
|
||||
launchArgs = append([]string{"--auth-type", "openai"}, launchArgs...)
|
||||
}
|
||||
if model != "" && !qwenHasFlag(launchArgs, "--model", "-m") {
|
||||
launchArgs = append([]string{"--model", model}, launchArgs...)
|
||||
}
|
||||
return launchArgs
|
||||
}
|
||||
|
||||
func qwenLaunchEnv(model string) []string {
|
||||
env := os.Environ()
|
||||
env = qwenUpsertEnv(env, "OPENAI_API_KEY", "ollama")
|
||||
env = qwenUpsertEnv(env, "OPENAI_BASE_URL", qwenBaseURL())
|
||||
if model != "" {
|
||||
env = qwenUpsertEnv(env, "OPENAI_MODEL", model)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func qwenUpsertEnv(env []string, key, value string) []string {
|
||||
prefix := key + "="
|
||||
filtered := env[:0]
|
||||
for _, entry := range env {
|
||||
if strings.HasPrefix(entry, prefix) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
return append(filtered, prefix+value)
|
||||
}
|
||||
|
||||
func qwenHasFlag(args []string, names ...string) bool {
|
||||
for _, arg := range args {
|
||||
for _, name := range names {
|
||||
if arg == name || strings.HasPrefix(arg, name+"=") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/ollama/ollama/cmd/config"
|
||||
)
|
||||
|
||||
func setQwenTestHome(t *testing.T, home string) {
|
||||
t.Helper()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
}
|
||||
|
||||
func TestQwenConfigure(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setQwenTestHome(t, tmpDir)
|
||||
|
||||
q := &Qwen{}
|
||||
if err := q.Configure("gemma4"); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(tmpDir, ".qwen", "settings.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("failed to parse config: %v", err)
|
||||
}
|
||||
|
||||
envCfg := cfg["env"].(map[string]any)
|
||||
if envCfg[qwenOllamaEnvKey] != "ollama" {
|
||||
t.Fatalf("expected env[%q] to be ollama, got %v", qwenOllamaEnvKey, envCfg[qwenOllamaEnvKey])
|
||||
}
|
||||
|
||||
modelCfg := cfg["model"].(map[string]any)
|
||||
if modelCfg["name"] != "gemma4" {
|
||||
t.Fatalf("expected model.name gemma4, got %v", modelCfg["name"])
|
||||
}
|
||||
|
||||
security := cfg["security"].(map[string]any)
|
||||
auth := security["auth"].(map[string]any)
|
||||
if auth["selectedType"] != "openai" {
|
||||
t.Fatalf("expected auth.selectedType openai, got %v", auth["selectedType"])
|
||||
}
|
||||
if auth["baseUrl"] != qwenBaseURL() {
|
||||
t.Fatalf("expected auth.baseUrl %q, got %v", qwenBaseURL(), auth["baseUrl"])
|
||||
}
|
||||
|
||||
modelProviders := cfg["modelProviders"].(map[string]any)
|
||||
openai := modelProviders["openai"].([]any)
|
||||
if len(openai) != 1 {
|
||||
t.Fatalf("expected one openai provider, got %d", len(openai))
|
||||
}
|
||||
|
||||
provider := openai[0].(map[string]any)
|
||||
if provider["id"] != "gemma4" {
|
||||
t.Fatalf("expected provider id gemma4, got %v", provider["id"])
|
||||
}
|
||||
if provider["name"] != "gemma4 (Ollama)" {
|
||||
t.Fatalf("expected provider name %q, got %v", "gemma4 (Ollama)", provider["name"])
|
||||
}
|
||||
if provider["baseUrl"] != qwenBaseURL() {
|
||||
t.Fatalf("expected provider baseUrl %q, got %v", qwenBaseURL(), provider["baseUrl"])
|
||||
}
|
||||
if provider["envKey"] != qwenOllamaEnvKey {
|
||||
t.Fatalf("expected provider envKey %q, got %v", qwenOllamaEnvKey, provider["envKey"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenCurrentModel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setQwenTestHome(t, tmpDir)
|
||||
|
||||
q := &Qwen{}
|
||||
if got := q.CurrentModel(); got != "" {
|
||||
t.Fatalf("expected empty model without config, got %q", got)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".qwen")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, "settings.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"model":{"name":"llama3.2"}}`), 0o644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
if got := q.CurrentModel(); got != "llama3.2" {
|
||||
t.Fatalf("expected current model llama3.2, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenCurrentModelFallsBackToProvider(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setQwenTestHome(t, tmpDir)
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".qwen")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, "settings.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"modelProviders":{"openai":[{"id":"mistral"}]}}`), 0o644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
if got := (&Qwen{}).CurrentModel(); got != "mistral" {
|
||||
t.Fatalf("expected provider fallback mistral, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenOnboard(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setQwenTestHome(t, tmpDir)
|
||||
|
||||
if err := (&Qwen{}).Onboard(); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
saved, err := config.LoadIntegration("qwen")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load integration config: %v", err)
|
||||
}
|
||||
if !saved.Onboarded {
|
||||
t.Fatal("expected qwen integration to be marked onboarded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenIntegration(t *testing.T) {
|
||||
q := &Qwen{}
|
||||
|
||||
t.Run("String", func(t *testing.T) {
|
||||
if got := q.String(); got != "Qwen Code" {
|
||||
t.Fatalf("String() = %q, want %q", got, "Qwen Code")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("implements Runner", func(t *testing.T) {
|
||||
var _ Runner = q
|
||||
})
|
||||
|
||||
t.Run("implements ManagedSingleModel", func(t *testing.T) {
|
||||
var _ ManagedSingleModel = q
|
||||
})
|
||||
|
||||
t.Run("implements ManagedInteractiveOnboarding", func(t *testing.T) {
|
||||
var _ ManagedInteractiveOnboarding = q
|
||||
})
|
||||
}
|
||||
|
||||
func TestQwenFindPath(t *testing.T) {
|
||||
q := &Qwen{}
|
||||
path, err := q.findPath()
|
||||
if err != nil {
|
||||
t.Skipf("qwen binary not found, skipping: %v", err)
|
||||
}
|
||||
if path == "" {
|
||||
t.Fatal("expected non-empty path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenPaths(t *testing.T) {
|
||||
testDir := filepath.Join(t.TempDir(), "qwen-paths-test")
|
||||
setQwenTestHome(t, testDir)
|
||||
|
||||
q := &Qwen{}
|
||||
os.MkdirAll(filepath.Join(testDir, ".qwen"), 0o755)
|
||||
os.WriteFile(filepath.Join(testDir, ".qwen", "settings.json"), []byte("{}"), 0o644)
|
||||
|
||||
paths := q.Paths()
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("expected 1 path, got %v", paths)
|
||||
}
|
||||
|
||||
want, err := filepath.EvalSymlinks(filepath.Join(testDir, ".qwen", "settings.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve expected path: %v", err)
|
||||
}
|
||||
got, err := filepath.EvalSymlinks(paths[0])
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve returned path: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("expected user config path %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenLaunchArgs(t *testing.T) {
|
||||
got := qwenLaunchArgs("llama3.2", nil)
|
||||
want := []string{"--model", "llama3.2", "--auth-type", "openai"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got = qwenLaunchArgs("llama3.2", []string{"--auth-type", "openai"})
|
||||
want = []string{"--model", "llama3.2", "--auth-type", "openai"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got = qwenLaunchArgs("llama3.2", []string{"-m", "gemma4"})
|
||||
want = []string{"--auth-type", "openai", "-m", "gemma4"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenLaunchEnv(t *testing.T) {
|
||||
t.Setenv("OPENAI_API_KEY", "")
|
||||
t.Setenv("OPENAI_BASE_URL", "")
|
||||
t.Setenv("OPENAI_MODEL", "")
|
||||
|
||||
env := qwenLaunchEnv("llama3.2")
|
||||
if !slices.Contains(env, "OPENAI_API_KEY=ollama") {
|
||||
t.Fatalf("expected OPENAI_API_KEY override, got %v", env)
|
||||
}
|
||||
if !slices.Contains(env, "OPENAI_BASE_URL="+qwenBaseURL()) {
|
||||
t.Fatalf("expected OPENAI_BASE_URL override, got %v", env)
|
||||
}
|
||||
if !slices.Contains(env, "OPENAI_MODEL=llama3.2") {
|
||||
t.Fatalf("expected OPENAI_MODEL override, got %v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenLaunchEnvOverridesExistingOpenAIEnv(t *testing.T) {
|
||||
t.Setenv("OPENAI_API_KEY", "real-key")
|
||||
t.Setenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
t.Setenv("OPENAI_MODEL", "gpt-4.1")
|
||||
|
||||
env := qwenLaunchEnv("llama3.2")
|
||||
if !slices.Contains(env, "OPENAI_API_KEY=ollama") {
|
||||
t.Fatalf("expected OPENAI_API_KEY override, got %v", env)
|
||||
}
|
||||
if !slices.Contains(env, "OPENAI_BASE_URL="+qwenBaseURL()) {
|
||||
t.Fatalf("expected OPENAI_BASE_URL override, got %v", env)
|
||||
}
|
||||
if !slices.Contains(env, "OPENAI_MODEL=llama3.2") {
|
||||
t.Fatalf("expected OPENAI_MODEL override, got %v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwenRunDoesNotRewriteConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setQwenTestHome(t, tmpDir)
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
qwenBinDir := filepath.Join(tmpDir, "bin")
|
||||
if err := os.MkdirAll(qwenBinDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create bin dir: %v", err)
|
||||
}
|
||||
|
||||
qwenBin := filepath.Join(qwenBinDir, "qwen")
|
||||
qwenScript := "#!/bin/sh\nexit 0\n"
|
||||
if runtime.GOOS == "windows" {
|
||||
qwenBin = filepath.Join(qwenBinDir, "qwen.bat")
|
||||
qwenScript = "@echo off\r\nexit /b 0\r\n"
|
||||
}
|
||||
if err := os.WriteFile(qwenBin, []byte(qwenScript), 0o755); err != nil {
|
||||
t.Fatalf("failed to write fake qwen binary: %v", err)
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := os.Chmod(qwenBin, 0o755); err != nil {
|
||||
t.Fatalf("failed to chmod fake qwen binary: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Setenv("PATH", qwenBinDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".qwen")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
initialConfig := []byte(`{"model":{"name":"qwen3:32b"}}`)
|
||||
configPath := filepath.Join(configDir, "settings.json")
|
||||
if err := os.WriteFile(configPath, initialConfig, 0o644); err != nil {
|
||||
t.Fatalf("failed to write initial config: %v", err)
|
||||
}
|
||||
|
||||
if err := (&Qwen{}).Run("qwen3:32b", nil); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config after run: %v", err)
|
||||
}
|
||||
if string(data) != string(initialConfig) {
|
||||
t.Fatalf("expected run not to rewrite config, got %s", string(data))
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -33,7 +33,7 @@ type IntegrationInfo struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
var launcherIntegrationOrder = []string{"claude-desktop", "claude", "openclaw", "hermes", "opencode", "codex", "copilot", "droid", "pi", "pool"}
|
||||
var launcherIntegrationOrder = []string{"claude-desktop", "claude", "openclaw", "hermes", "opencode", "codex", "copilot", "droid", "pi", "pool", "qwen"}
|
||||
|
||||
var integrationSpecs = []*IntegrationSpec{
|
||||
{
|
||||
@@ -217,6 +217,18 @@ var integrationSpecs = []*IntegrationSpec{
|
||||
URL: "https://code.visualstudio.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "qwen",
|
||||
Runner: &Qwen{},
|
||||
Description: "Qwen's AI coding agent with tool use",
|
||||
Install: IntegrationInstallSpec{
|
||||
CheckInstalled: func() bool {
|
||||
_, err := (&Qwen{}).findPath()
|
||||
return err == nil
|
||||
},
|
||||
URL: "https://qwen.ai/qwencode",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var integrationSpecsByName map[string]*IntegrationSpec
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"/integrations/droid",
|
||||
"/integrations/goose",
|
||||
"/integrations/pi",
|
||||
"/integrations/qwen",
|
||||
"/integrations/pool"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ Coding assistants that can read, modify, and execute code in your projects.
|
||||
- [Droid](/integrations/droid)
|
||||
- [Goose](/integrations/goose)
|
||||
- [Pi](/integrations/pi)
|
||||
- [Qwen Code](/integrations/qwen)
|
||||
- [Pool](/integrations/pool)
|
||||
|
||||
## Assistants
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Qwen Code
|
||||
---
|
||||
|
||||
Qwen Code is Qwen's terminal-based coding agent for code editing, code review, and tool-assisted development workflows.
|
||||
|
||||
## Install
|
||||
|
||||
Install [Qwen Code](https://qwen.ai/qwencode).
|
||||
|
||||
You can also use one of these install methods:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```shell macOS / Linux (script)
|
||||
bash -c "$(curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh)"
|
||||
```
|
||||
|
||||
```powershell Windows (PowerShell)
|
||||
powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"
|
||||
```
|
||||
|
||||
```shell npm (all platforms)
|
||||
npm install -g @qwen-code/qwen-code@latest
|
||||
```
|
||||
|
||||
```shell macOS / Linux (Homebrew)
|
||||
brew install qwen-code
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Usage with Ollama
|
||||
|
||||
### Quick setup
|
||||
|
||||
```bash
|
||||
ollama launch qwen
|
||||
```
|
||||
|
||||
To configure Qwen without launching it:
|
||||
|
||||
```bash
|
||||
ollama launch qwen --config
|
||||
```
|
||||
|
||||
|
||||
Run directly with a model:
|
||||
```bash
|
||||
ollama launch qwen --model kimi-k2.5:cloud
|
||||
```
|
||||
|
||||
`ollama launch qwen` updates `~/.qwen/settings.json` so Qwen Code uses Ollama's OpenAI-compatible endpoint.
|
||||
|
||||
It sets:
|
||||
|
||||
- `security.auth.selectedType` to `openai`
|
||||
- `env.OLLAMA_API_KEY` to `ollama`
|
||||
- `modelProviders.openai` to the selected Ollama model
|
||||
- `model.name` to the selected Ollama model
|
||||
Reference in New Issue
Block a user