qwen-claude 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.3
2
+ Name: qwen-claude
3
+ Version: 0.1.0
4
+ Summary: Qwen + Claude Code Router bootstrapper
5
+ Author: Saad-Kamran-2006
6
+ Author-email: Saad-Kamran-2006 <saadkamran6ft@gmail.com>
7
+ License: MIT
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
File without changes
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "qwen-claude"
3
+ version = "0.1.0"
4
+ description = "Qwen + Claude Code Router bootstrapper"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [
8
+ { name = "Saad-Kamran-2006", email = "saadkamran6ft@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = []
12
+
13
+ [project.scripts]
14
+ qc = "qwen_claude.cli:run"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.9.26,<0.10.0"]
18
+ build-backend = "uv_build"
File without changes
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ import site
3
+ import sys
4
+ import json
5
+ import os
6
+ import time
7
+ import shutil
8
+ import subprocess
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ # ------------------- CONFIG (EDIT IF NEEDED) -------------------
14
+ QWEN_OAUTH = Path(os.environ["USERPROFILE"]) / ".qwen" / "oauth_creds.json"
15
+ CCR_CONFIG = Path(os.environ["USERPROFILE"]) / ".claude-code-router" / "config.json"
16
+ SCHEMA_FILE = Path(__file__).parent / "schema.json"
17
+
18
+ REFRESH_BUFFER_SECONDS = 120
19
+ RESTART_CCR_ON_CHANGE = True
20
+ RUN_CCR_CODE = True
21
+ # ---------------------------------------------------------------
22
+
23
+
24
+ def load_json(path: Path) -> dict:
25
+ with path.open("r", encoding="utf-8") as f:
26
+ return json.load(f)
27
+
28
+
29
+ def save_json_atomic(path: Path, data: dict) -> None:
30
+ tmp = path.with_suffix(path.suffix + ".tmp")
31
+ with tmp.open("w", encoding="utf-8") as f:
32
+ json.dump(data, f, indent=2)
33
+ tmp.replace(path)
34
+
35
+
36
+ def is_expiring_soon(oauth: dict, buffer_seconds: int) -> bool:
37
+ exp_ms = int(oauth.get("expiry_date", 0))
38
+ if exp_ms <= 0:
39
+ return True
40
+ exp_s = exp_ms / 1000.0
41
+ return time.time() >= (exp_s - buffer_seconds)
42
+
43
+
44
+ def which(cmd: str) -> Optional[str]:
45
+ return shutil.which(cmd)
46
+
47
+
48
+ # ------------------- DEPENDENCY & SCHEMA CHECKS -------------------
49
+ def verify_required_tools() -> None:
50
+ requirements = [
51
+ {
52
+ "name": "qwen",
53
+ "binary": "qwen",
54
+ "install": "npm install -g @qwen-code/qwen-code@latest",
55
+ },
56
+ {
57
+ "name": "claude-code-router",
58
+ "binary": "ccr",
59
+ "install": "npm install -g @musistudio/claude-code-router",
60
+ },
61
+ {
62
+ "name": "claude",
63
+ "binary": "claude",
64
+ "install": "npm install -g @anthropic-ai/claude-code",
65
+ },
66
+ ]
67
+ for req in requirements:
68
+ path = which(req["binary"])
69
+ if not path:
70
+ print(
71
+ f"[ERR] Required package '{req['name']}' is not installed.\n Install: {req['install']}"
72
+ )
73
+ raise SystemExit(1)
74
+ print(f"[OK] Found {req['name']} → {path}")
75
+
76
+
77
+ def schema_validation() -> None:
78
+ """
79
+ Checks for api_base_url within the 'qwen' provider in config.json.
80
+ """
81
+ target_url = "https://portal.qwen.ai/v1/chat/completions"
82
+
83
+ if not SCHEMA_FILE.exists():
84
+ print(f"[ERR] Schema file missing at {SCHEMA_FILE}.")
85
+ return
86
+
87
+ if not CCR_CONFIG.exists():
88
+ print(f"[INFO] {CCR_CONFIG.name} missing. Initializing from schema...")
89
+ save_json_atomic(CCR_CONFIG, load_json(SCHEMA_FILE))
90
+ return
91
+
92
+ try:
93
+ config_data = load_json(CCR_CONFIG)
94
+ except Exception:
95
+ config_data = {}
96
+
97
+ # Find the qwen provider block
98
+ providers = config_data.get("Providers", [])
99
+ qwen_provider = next(
100
+ (p for p in providers if isinstance(p, dict) and p.get("name") == "qwen"), None
101
+ )
102
+
103
+ needs_reset = False
104
+ if not qwen_provider:
105
+ print("[INFO] 'qwen' provider block missing in config.")
106
+ needs_reset = True
107
+ elif "api_base_url" not in qwen_provider:
108
+ print("[INFO] 'api_base_url' missing in qwen provider config.")
109
+ needs_reset = True
110
+ elif qwen_provider.get("api_base_url") != target_url:
111
+ print(f"[INFO] URL mismatch. Found: {qwen_provider.get('api_base_url')}")
112
+ needs_reset = True
113
+
114
+ if needs_reset:
115
+ print(f"[INFO] Rewriting {CCR_CONFIG.name} using {SCHEMA_FILE.name}...")
116
+ save_json_atomic(CCR_CONFIG, load_json(SCHEMA_FILE))
117
+ else:
118
+ print("[OK] Schema validation passed.")
119
+
120
+
121
+ # -------------------------------------------------------------
122
+
123
+
124
+ def run_windows_cmd(exe_path: str, args: list[str], quiet: bool = True) -> int:
125
+ exe_lower = exe_path.lower()
126
+ stdout = subprocess.DEVNULL if quiet else None
127
+ stderr = subprocess.DEVNULL if quiet else None
128
+
129
+ if exe_lower.endswith(".ps1"):
130
+ cmd = [
131
+ "powershell",
132
+ "-NoProfile",
133
+ "-ExecutionPolicy",
134
+ "Bypass",
135
+ "-File",
136
+ exe_path,
137
+ *args,
138
+ ]
139
+ return subprocess.run(cmd, check=False, stdout=stdout, stderr=stderr).returncode
140
+ if exe_lower.endswith(".cmd") or exe_lower.endswith(".bat"):
141
+ cmdline = subprocess.list2cmdline([exe_path, *args])
142
+ return subprocess.run(
143
+ ["cmd.exe", "/d", "/s", "/c", cmdline],
144
+ check=False,
145
+ stdout=stdout,
146
+ stderr=stderr,
147
+ ).returncode
148
+ return subprocess.run(
149
+ [exe_path, *args], check=False, stdout=stdout, stderr=stderr
150
+ ).returncode
151
+
152
+
153
+ def force_qwen_refresh(qwen_path: str) -> None:
154
+ run_windows_cmd(qwen_path, ["--version"], quiet=True)
155
+
156
+
157
+ def update_ccr_api_key(ccr_config: Path, new_token: str) -> bool:
158
+ cfg = load_json(ccr_config)
159
+ providers = cfg.get("Providers", [])
160
+ changed = False
161
+ for p in providers:
162
+ if isinstance(p, dict) and p.get("name") == "qwen":
163
+ if p.get("api_key") != new_token:
164
+ p["api_key"] = new_token
165
+ changed = True
166
+
167
+ if changed:
168
+ save_json_atomic(ccr_config, cfg)
169
+ return changed
170
+
171
+
172
+ def print_install_info():
173
+ print("[INFO] qwen-claude installed at:")
174
+ for p in site.getsitepackages():
175
+ if "qwen_claude" in p:
176
+ print(" ", p)
177
+
178
+ print("[INFO] Executable location:")
179
+ print(" ", Path(sys.executable).parent)
180
+
181
+
182
+ def main() -> int:
183
+ print_install_info()
184
+ verify_required_tools()
185
+ schema_validation()
186
+
187
+ qwen_path = which("qwen")
188
+ ccr_path = which("ccr")
189
+
190
+ # Auth logic
191
+ if not QWEN_OAUTH.exists():
192
+ print("[INFO] Launching Qwen for authentication...")
193
+ proc = subprocess.Popen([qwen_path])
194
+ while not QWEN_OAUTH.exists():
195
+ if proc.poll() is not None:
196
+ return 6
197
+ time.sleep(1)
198
+ proc.terminate()
199
+
200
+ oauth = load_json(QWEN_OAUTH)
201
+ if is_expiring_soon(oauth, REFRESH_BUFFER_SECONDS):
202
+ force_qwen_refresh(qwen_path)
203
+ oauth = load_json(QWEN_OAUTH)
204
+
205
+ access_token = oauth.get("access_token")
206
+ changed = update_ccr_api_key(CCR_CONFIG, access_token)
207
+
208
+ # RESTART LOGIC FIX: Only restart once
209
+ ccr_needs_restart = changed and RESTART_CCR_ON_CHANGE
210
+
211
+ if ccr_needs_restart:
212
+ print("[OK] Token updated. Restarting CCR...")
213
+ run_windows_cmd(ccr_path, ["restart"], quiet=False)
214
+ else:
215
+ print("[OK] CCR config is up to date.")
216
+
217
+ if RUN_CCR_CODE and ccr_path:
218
+ # If we didn't just restart it above, we might need to restart/start it here
219
+ # or just run 'code'. Since you had a restart here before, I'll keep it
220
+ # but ensure it doesn't double-trigger if changed was true.
221
+ if not ccr_needs_restart:
222
+ print("[INFO] Starting CCR...")
223
+ run_windows_cmd(ccr_path, ["restart"], quiet=False)
224
+
225
+ print("[INFO] Launching Claude Code...")
226
+ run_windows_cmd(ccr_path, ["code"], quiet=False)
227
+
228
+ return 0
229
+
230
+
231
+ def run():
232
+ raise SystemExit(main())
233
+
234
+
235
+ if __name__ == "__main__":
236
+ raise SystemExit(main())
@@ -0,0 +1,39 @@
1
+ {
2
+ "LOG": true,
3
+ "LOG_LEVEL": "info",
4
+ "CLAUDE_PATH": "",
5
+ "HOST": "127.0.0.1",
6
+ "PORT": 3456,
7
+ "APIKEY": "",
8
+ "API_TIMEOUT_MS": "600000",
9
+ "PROXY_URL": "",
10
+ "transformers": [],
11
+ "Providers": [
12
+ {
13
+ "name": "qwen",
14
+ "api_base_url": "https://portal.qwen.ai/v1/chat/completions",
15
+ "api_key": "PASTE_YOUR_QWEN_ACCESS_TOKEN_HERE",
16
+ "models": ["qwen3-coder-plus", "qwen3-coder-plus", "qwen3-coder-plus"]
17
+ }
18
+ ],
19
+ "StatusLine": {
20
+ "enabled": false,
21
+ "currentStyle": "default",
22
+ "default": {
23
+ "modules": []
24
+ },
25
+ "powerline": {
26
+ "modules": []
27
+ }
28
+ },
29
+ "Router": {
30
+ "default": "qwen,qwen3-coder-plus",
31
+ "background": "qwen,qwen3-coder-plus",
32
+ "think": "qwen,qwen3-coder-plus",
33
+ "longContext": "qwen,qwen3-coder-plus",
34
+ "longContextThreshold": 60000,
35
+ "webSearch": "qwen,qwen3-coder-plus",
36
+ "image": ""
37
+ },
38
+ "CUSTOM_ROUTER_PATH": ""
39
+ }