qwenproxy-cli 1.0.5 → 1.0.6

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.
package/bin/qwenproxy.js CHANGED
@@ -134,15 +134,22 @@ if (isBrowserCommand) {
134
134
  const patchrightEntry = require.resolve("patchright");
135
135
  cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
136
136
  } catch {}
137
+ const installEnv = {
138
+ ...process.env,
139
+ PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
140
+ process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
141
+ };
137
142
 
138
143
  if (cliPath && fs.existsSync(cliPath)) {
139
144
  spawnSync(process.execPath, [cliPath, "install", "chromium"], {
140
145
  stdio: "inherit",
146
+ env: installEnv,
141
147
  });
142
148
  } else {
143
149
  const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
144
150
  spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
145
151
  stdio: "inherit",
152
+ env: installEnv,
146
153
  });
147
154
  }
148
155
  console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -154,6 +154,7 @@ const envSchema = z
154
154
  QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
155
155
  QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
156
156
  QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
157
+ QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
157
158
  QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
158
159
  QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
159
160
  CONTEXT_METER_ENABLED: z.string().default("true"),
@@ -358,6 +359,7 @@ export const config = {
358
359
  /** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
359
360
  /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
360
361
  browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
362
+ mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
361
363
  chatMode: env.QWEN_CHAT_MODE,
362
364
  maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
363
365
  maxPersonalizationBytes: Math.max(
@@ -63,15 +63,55 @@ export function stripThinkingSuffix(model: string): {
63
63
  return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
64
64
  }
65
65
 
66
+ /**
67
+ * Mapeia modelos populares de terceiros (OpenAI / Anthropic) para o tier Qwen equivalente:
68
+ * - mini / haiku / 3.5 -> qwen3.7-plus
69
+ * - gpt-4* / o1* / o3* / opus / sonnet / claude-* -> qwen3.8-max
70
+ * Modelos que já começam com "qwen" passam intactos.
71
+ */
72
+ export function mapKnownModelAlias(model: string): string {
73
+ if (!model) return model;
74
+ const lower = model.toLowerCase();
75
+ if (lower.startsWith("qwen")) return model;
76
+
77
+ // Lightweight / mini models
78
+ if (
79
+ lower.includes("mini") ||
80
+ lower.includes("haiku") ||
81
+ lower.includes("3.5-turbo")
82
+ ) {
83
+ return "qwen3.7-plus";
84
+ }
85
+
86
+ // Flagship reasoning / chat models
87
+ if (
88
+ lower.startsWith("gpt-") ||
89
+ lower.startsWith("chatgpt-") ||
90
+ lower.startsWith("o1") ||
91
+ lower.startsWith("o3") ||
92
+ lower.includes("sonnet") ||
93
+ lower.includes("opus") ||
94
+ lower.startsWith("claude")
95
+ ) {
96
+ return "qwen3.8-max";
97
+ }
98
+
99
+ return model;
100
+ }
101
+
66
102
  /**
67
103
  * Mapeia o id de modelo para o Qwen upstream.
68
- * Ids `qwen*` passam direto (após remover o sufixo de raciocínio); ids de
69
- * outros provedores (gpt-*, grok-*, etc.) também passam “as-is” o Codex/Custom
70
- * provider envia o id Qwen correto, e qualquer id desconhecido deve chegar ao
71
- * upstream para que este responda um erro claro de modelo, em vez de ser
72
- * silenciosamente roteado para um tier qualquer.
104
+ * Quando enableAliases é verdadeiro (padrão via QWEN_MAP_OPENAI_MODELS),
105
+ * converte gpt-* / o1* / claude-* para os tiers correspondentes do Qwen.
73
106
  */
74
- export function mapClientModelToQwen(model: string): string {
107
+ export function mapClientModelToQwen(
108
+ model: string,
109
+ enableAliases = true,
110
+ ): string {
75
111
  if (!model) return model;
76
- return stripThinkingSuffix(model.trim()).baseModel;
112
+ const base = stripThinkingSuffix(model.trim()).baseModel;
113
+ if (enableAliases) {
114
+ return mapKnownModelAlias(base);
115
+ }
116
+ return base;
77
117
  }
@@ -70,7 +70,7 @@ export async function parseRequestBody(c: Context): Promise<ParsedRequest> {
70
70
 
71
71
  // Thinking suffixes → base model + reasoning mode
72
72
  const { baseModel, enableThinking, reasoningMode } = stripThinkingSuffix(body.model);
73
- const modelId = mapClientModelToQwen(baseModel);
73
+ const modelId = mapClientModelToQwen(baseModel, config.qwen.mapOpenAiModels);
74
74
 
75
75
  // OpenAI `reasoning_effort` (none|minimal|low|medium|high|xhigh|max).
76
76
  // Precedence: an explicit model suffix wins — effort only acts on unsuffixed
@@ -23,9 +23,48 @@ function pick<T>(rng: () => number, values: readonly T[]): T {
23
23
  function randInt(rng: () => number, min: number, max: number): number {
24
24
  return Math.floor(rng() * (max - min + 1)) + min;
25
25
  }
26
+ let dynamicChromeMajor = 151;
26
27
 
27
- const CHROME_MAJOR = 149;
28
+ export function updateChromeMajor(major: number): void {
29
+ if (typeof major === "number" && major >= 100) {
30
+ dynamicChromeMajor = major;
31
+ }
32
+ }
33
+
34
+ export function getChromeMajor(): number {
35
+ return dynamicChromeMajor;
36
+ }
37
+
38
+ export function getSystemLocale(): string {
39
+ return (
40
+ process.env.QWEN_LOCALE ||
41
+ (typeof Intl !== "undefined" && Intl.DateTimeFormat
42
+ ? Intl.DateTimeFormat().resolvedOptions().locale
43
+ : "") ||
44
+ "pt-BR"
45
+ );
46
+ }
47
+
48
+ function getLanguageProfiles(locale = getSystemLocale()): readonly (readonly string[])[] {
49
+ const primary = locale.split("-")[0];
50
+ if (primary === "en") {
51
+ return [
52
+ ["en-US", "en"],
53
+ ["en-US", "en", "es"],
54
+ ["en", "en-US"],
55
+ ["en-US", "en;q=0.9"],
56
+ ] as const;
57
+ }
58
+ return [
59
+ [locale, primary, "en-US", "en"],
60
+ [locale, primary, "en-US", "en", "es"],
61
+ [locale, "en-US", "en", primary],
62
+ [locale, primary, "en"],
63
+ [locale, `${primary};q=0.9`, "en-US;q=0.8", "en;q=0.7"],
64
+ ] as const;
65
+ }
28
66
 
67
+ const CHROME_MAJOR = 151;
29
68
  const VIEWPORTS = [
30
69
  { width: 1366, height: 768 },
31
70
  { width: 1440, height: 900 },
@@ -176,20 +215,21 @@ export function getFingerprintProfile(accountId: string): FingerprintProfile {
176
215
  const rng = mulberry32(seed);
177
216
  const viewport = pick(rng, VIEWPORTS);
178
217
  const webgl = pick(rng, WEBGL_PROFILES);
179
- const languages = [...pick(rng, LANGUAGE_PROFILES)];
218
+ const languages = [...pick(rng, getLanguageProfiles())];
180
219
  const hardwareConcurrency = pick(rng, HARDWARE_CONCURRENCIES);
181
220
  const deviceMemory = pick(rng, DEVICE_MEMORIES);
182
221
  const platformInfo = pick(rng, PLATFORM_VERSIONS);
183
222
  const notABrand = pick(rng, NOT_A_BRAND_VARIANTS);
184
223
 
224
+ const major = dynamicChromeMajor;
185
225
  const build = randInt(rng, 7300, 7600);
186
226
  const patch = randInt(rng, 0, 160);
187
- const chromeVersion = `${CHROME_MAJOR}.0.${build}.${patch}`;
227
+ const chromeVersion = `${major}.0.${build}.${patch}`;
188
228
 
189
229
  const brands = [
190
230
  { brand: notABrand.brand, version: notABrand.version },
191
- { brand: "Google Chrome", version: String(CHROME_MAJOR) },
192
- { brand: "Chromium", version: String(CHROME_MAJOR) },
231
+ { brand: "Google Chrome", version: String(major) },
232
+ { brand: "Chromium", version: String(major) },
193
233
  ];
194
234
  const fullVersionList = [
195
235
  { brand: notABrand.brand, version: chromeVersion },
@@ -13,6 +13,11 @@ import { createRequire } from "module";
13
13
  const requireLocal = createRequire(import.meta.url);
14
14
 
15
15
  function autoInstallPlaywrightChromium(): void {
16
+ const installEnv = {
17
+ ...process.env,
18
+ PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
19
+ process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
20
+ };
16
21
  try {
17
22
  const patchrightEntry = requireLocal.resolve("patchright");
18
23
  const cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
@@ -20,6 +25,7 @@ function autoInstallPlaywrightChromium(): void {
20
25
  console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando automaticamente...");
21
26
  spawnSync(process.execPath, [cliPath, "install", "chromium"], {
22
27
  stdio: "inherit",
28
+ env: installEnv,
23
29
  });
24
30
  return;
25
31
  }
@@ -29,6 +35,7 @@ function autoInstallPlaywrightChromium(): void {
29
35
  const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
30
36
  spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
31
37
  stdio: "inherit",
38
+ env: installEnv,
32
39
  });
33
40
  }
34
41
  import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
@@ -47,6 +54,7 @@ import { getAccountsByPriority } from "../core/account-priority.ts";
47
54
  import {
48
55
  clearFingerprintCache,
49
56
  getFingerprintProfile,
57
+ updateChromeMajor,
50
58
  type FingerprintProfile,
51
59
  } from "./fingerprint.ts";
52
60
  import { subtlePageActivity } from "./human-behavior.ts";
@@ -309,6 +317,13 @@ export async function getOrLaunchSharedBrowser(
309
317
  }
310
318
  }
311
319
 
320
+ try {
321
+ const v = browser.version();
322
+ const major = parseInt(v.split(".")[0], 10);
323
+ if (major >= 100) {
324
+ updateChromeMajor(major);
325
+ }
326
+ } catch {}
312
327
  browser.on("disconnected", () => {
313
328
  console.warn("[Playwright] Shared browser disconnected");
314
329
  sharedBrowser = null;
@@ -153,10 +153,10 @@ const WARM_POOL_LOW_WATER = 3;
153
153
 
154
154
  function warmChatKey(
155
155
  accountId: string | undefined,
156
- model: string,
156
+ _model: string,
157
157
  chatId: string,
158
158
  ) {
159
- return `${accountId || "global"}:${model}:${chatId}`;
159
+ return `${accountId || "global"}:${chatId}`;
160
160
  }
161
161
 
162
162
  function markWarmChatInFlight(
@@ -183,8 +183,8 @@ function isWarmChatInFlight(
183
183
  return inFlightWarmChats.has(warmChatKey(accountId, model, chatId));
184
184
  }
185
185
 
186
- function chatPoolKey(accountId: string | undefined, model: string): string {
187
- return `${accountId || "global"}:${model}`;
186
+ function chatPoolKey(accountId: string | undefined, _model?: string): string {
187
+ return accountId || "global";
188
188
  }
189
189
 
190
190
  function isQwenChatPoolEnabled(): boolean {
@@ -1,6 +1,24 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
2
  import { qwenUrl, qwenOrigin } from "./qwen-url.ts";
3
3
  import { config } from "../core/config.js";
4
+ import { getChromeMajor, getSystemLocale } from "./fingerprint.ts";
5
+
6
+ export function getSystemAcceptLanguage(customLocale?: string): string {
7
+ const locale = customLocale || getSystemLocale();
8
+ const primary = locale.split("-")[0];
9
+ if (locale.toLowerCase() === "en-us" || locale.toLowerCase() === "en") {
10
+ return "en-US,en;q=0.9";
11
+ }
12
+ if (locale.toLowerCase().startsWith("pt")) {
13
+ return "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7";
14
+ }
15
+ return `${locale},${primary};q=0.9,en-US;q=0.8,en;q=0.7`;
16
+ }
17
+
18
+ export function getDefaultQwenUserAgent(major = getChromeMajor()): string {
19
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`;
20
+ }
21
+
4
22
  let dynamicWebVersion: string | null = null;
5
23
 
6
24
  export function updateQwenWebVersion(version?: string | null): void {
@@ -14,13 +32,12 @@ export function getQwenWebVersion(): string {
14
32
  }
15
33
 
16
34
  export const QWEN_WEB_VERSION = config.qwen.webVersion;
17
- export const DEFAULT_QWEN_USER_AGENT =
18
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
35
+ export const DEFAULT_QWEN_USER_AGENT = getDefaultQwenUserAgent(151);
19
36
  const QWEN_TIMEZONE_HEADER = new Date().toString().split(" (")[0];
20
-
21
37
  export interface BuildQwenHeadersOptions {
22
38
  cookie: string;
23
39
  userAgent?: string;
40
+ acceptLanguage?: string;
24
41
  bxUa?: string;
25
42
  bxUmidtoken?: string;
26
43
  bxV?: string;
@@ -40,7 +57,10 @@ export function buildQwenRequestHeaders(
40
57
  const headers: Record<string, string> = {
41
58
  ...(opts.extra ?? {}),
42
59
  Accept: "application/json",
43
- "Accept-Language": "pt-BR,pt;q=0.9",
60
+ "Accept-Language":
61
+ opts.extra?.["Accept-Language"] ||
62
+ opts.acceptLanguage ||
63
+ getSystemAcceptLanguage(),
44
64
  "Accept-Encoding": "gzip, deflate, br, zstd",
45
65
  "Content-Type": "application/json",
46
66
  Cookie: opts.cookie,
@@ -54,7 +74,7 @@ export function buildQwenRequestHeaders(
54
74
  "Sec-Fetch-Mode": "cors",
55
75
  "Sec-Fetch-Site": "same-origin",
56
76
  Connection: "keep-alive",
57
- "User-Agent": opts.userAgent || DEFAULT_QWEN_USER_AGENT,
77
+ "User-Agent": opts.userAgent || getDefaultQwenUserAgent(),
58
78
  "X-Request-Id": uuidv4(),
59
79
  "bx-v": opts.bxV || "2.5.37",
60
80
  source: "web",
@@ -62,7 +82,9 @@ export function buildQwenRequestHeaders(
62
82
  timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
63
83
  // Use the real browser client-hints when captured (anti-hardcoded); fall
64
84
  // back to the static fingerprint otherwise.
65
- "sec-ch-ua": opts.secChUa || '"Google Chrome";v="150", "Chromium";v="150", "Not.A/Brand";v="99"',
85
+ "sec-ch-ua":
86
+ opts.secChUa ||
87
+ `"Google Chrome";v="${getChromeMajor()}", "Chromium";v="${getChromeMajor()}", "Not.A/Brand";v="99"`,
66
88
  "sec-ch-ua-mobile": opts.secChUaMobile || "?0",
67
89
  "sec-ch-ua-platform": opts.secChUaPlatform || '"Windows"',
68
90
  };
package/src/sync/omp.ts CHANGED
@@ -3,7 +3,30 @@ import path from "node:path";
3
3
  import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
4
  import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
5
 
6
- function buildOmpProviderYaml(baseUrl: string, apiKey: string): string {
6
+ function buildOmpProviderYaml(
7
+ baseUrl: string,
8
+ apiKey: string,
9
+ primaryModel: string = "qwen3.8-max",
10
+ ): string {
11
+ const modelList = [primaryModel];
12
+ if (primaryModel !== "qwen3.7-plus") {
13
+ modelList.push("qwen3.7-plus");
14
+ }
15
+
16
+ const formattedModels = modelList
17
+ .map(
18
+ (m) => ` - id: ${m}
19
+ name: ${m === "qwen3.8-max" ? "Qwen3.8-Max" : m === "qwen3.7-plus" ? "Qwen3.7-Plus" : m}
20
+ input: [text, image]
21
+ contextWindow: 1000000
22
+ maxTokens: 131072
23
+ reasoning: true
24
+ thinking:
25
+ mode: effort
26
+ efforts: [low, medium, high]`,
27
+ )
28
+ .join("\n");
29
+
7
30
  return ` qwenproxy:
8
31
  baseUrl: ${baseUrl}
9
32
  api: openai-completions
@@ -13,29 +36,12 @@ function buildOmpProviderYaml(baseUrl: string, apiKey: string): string {
13
36
  supportsReasoningEffort: true
14
37
  maxTokensField: max_completion_tokens
15
38
  models:
16
- - id: qwen3.8-max
17
- name: Qwen3.8-Max
18
- input: [text, image]
19
- contextWindow: 1000000
20
- maxTokens: 131072
21
- reasoning: true
22
- thinking:
23
- mode: effort
24
- efforts: [low, medium, high]
25
- - id: qwen3.7-plus
26
- name: Qwen3.7-Plus
27
- input: [text, image]
28
- contextWindow: 1000000
29
- maxTokens: 131072
30
- reasoning: true
31
- thinking:
32
- mode: effort
33
- efforts: [low, medium, high]
39
+ ${formattedModels}
34
40
  `;
35
41
  }
36
42
 
37
43
  export function syncOmp(options: SyncOptions): ClientSyncResult {
38
- const { filePath, apiKey, baseUrl } = options;
44
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
39
45
  try {
40
46
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
47
 
@@ -47,7 +53,7 @@ export function syncOmp(options: SyncOptions): ClientSyncResult {
47
53
  content = fs.readFileSync(filePath, "utf-8");
48
54
  }
49
55
 
50
- const providerBlock = buildOmpProviderYaml(baseUrl, apiKey);
56
+ const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model);
51
57
 
52
58
  if (!content.trim()) {
53
59
  content = `providers:\n${providerBlock}`;
@@ -3,7 +3,36 @@ import path from "node:path";
3
3
  import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
4
  import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
5
 
6
- function buildOpenCodeProviderObject(baseUrl: string, apiKey: string): Record<string, any> {
6
+ function buildOpenCodeProviderObject(
7
+ baseUrl: string,
8
+ apiKey: string,
9
+ primaryModel: string = "qwen3.8-max",
10
+ ): Record<string, any> {
11
+ const modelsObj: Record<string, any> = {};
12
+ const modelList = [primaryModel];
13
+ if (primaryModel !== "qwen3.7-plus") {
14
+ modelList.push("qwen3.7-plus");
15
+ }
16
+
17
+ for (const m of modelList) {
18
+ modelsObj[m] = {
19
+ name:
20
+ m === "qwen3.8-max"
21
+ ? "Qwen 3.8 Max"
22
+ : m === "qwen3.7-plus"
23
+ ? "Qwen 3.7 Plus"
24
+ : m,
25
+ limit: { context: 1048576, output: 65536 },
26
+ modalities: { input: ["text", "image"], output: ["text"] },
27
+ reasoning: true,
28
+ variants: {
29
+ low: { effort: "low" },
30
+ medium: { effort: "medium" },
31
+ high: { effort: "high" },
32
+ },
33
+ };
34
+ }
35
+
7
36
  return {
8
37
  npm: "@ai-sdk/openai-compatible",
9
38
  name: "QwenProxy",
@@ -11,30 +40,7 @@ function buildOpenCodeProviderObject(baseUrl: string, apiKey: string): Record<st
11
40
  baseURL: baseUrl,
12
41
  apiKey: apiKey,
13
42
  },
14
- models: {
15
- "qwen3.8-max": {
16
- name: "Qwen 3.8 Max",
17
- limit: { context: 1048576, output: 65536 },
18
- modalities: { input: ["text", "image"], output: ["text"] },
19
- reasoning: true,
20
- variants: {
21
- low: { effort: "low" },
22
- medium: { effort: "medium" },
23
- high: { effort: "high" },
24
- },
25
- },
26
- "qwen3.7-plus": {
27
- name: "Qwen 3.7 Plus",
28
- limit: { context: 1048576, output: 65536 },
29
- modalities: { input: ["text", "image"], output: ["text"] },
30
- reasoning: true,
31
- variants: {
32
- low: { effort: "low" },
33
- medium: { effort: "medium" },
34
- high: { effort: "high" },
35
- },
36
- },
37
- },
43
+ models: modelsObj,
38
44
  };
39
45
  }
40
46
  function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
@@ -121,10 +127,8 @@ function findKeyObjectSpan(content: string, key: string): { start: number; end:
121
127
  }
122
128
 
123
129
  export function syncOpenCode(options: SyncOptions): ClientSyncResult {
124
- const { filePath, apiKey, baseUrl } = options;
130
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
125
131
  try {
126
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
127
-
128
132
  let backupPath: string | undefined;
129
133
  let content = "";
130
134
 
@@ -133,10 +137,10 @@ export function syncOpenCode(options: SyncOptions): ClientSyncResult {
133
137
  content = fs.readFileSync(filePath, "utf-8");
134
138
  }
135
139
 
136
- const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey);
140
+ const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model);
137
141
  const providerJson = JSON.stringify(providerObj, null, 6)
138
142
  .split("\n")
139
- .map((line, idx) => (idx === 0 ? line : " " + line))
143
+ .map((line, idx) => (idx === 0 ? line : ` ${line}`))
140
144
  .join("\n");
141
145
 
142
146
  const qwenEntry = ` "qwenproxy": ${providerJson}`;
package/src/tui/app.ts CHANGED
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import path from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
- let cachedAppVersion = "v1.0.5";
14
+ let cachedAppVersion = "";
15
15
  try {
16
16
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
17
17
  const pkgPath = path.resolve(currentDir, "../../package.json");
@@ -279,66 +279,67 @@ export async function streamChatCompletions(
279
279
  * Fetches all live models dynamically from the running proxy /v1/models catalog.
280
280
  */
281
281
  let cachedLiveModels: string[] | null = null;
282
- let isFetchingLiveModels = false;
282
+ let liveModelsPromise: Promise<string[]> | null = null;
283
283
 
284
- export async function fetchLiveModels(): Promise<string[]> {
285
- if (cachedLiveModels && cachedLiveModels.length > 0) {
284
+ const DEFAULT_FALLBACK_MODELS = [
285
+ "qwen3.8-max",
286
+ "qwen3.7-plus",
287
+ "qwen3.7-max",
288
+ "z-image-turbo",
289
+ "qwen-image-3.0-pro",
290
+ "qwen-image-3.0",
291
+ "wan2.7-image-pro",
292
+ "wan2.7-image",
293
+ "wan3.0-video",
294
+ "wan2.7-t2v",
295
+ ];
296
+
297
+ export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
298
+ if (!forceRefresh && cachedLiveModels && cachedLiveModels.length > 0) {
286
299
  return cachedLiveModels;
287
300
  }
288
301
 
302
+ if (liveModelsPromise) {
303
+ return liveModelsPromise;
304
+ }
305
+
289
306
  const port = config.server?.port || 7936;
290
307
  const configuredHost = config.server?.host;
291
308
  const host = configuredHost && configuredHost !== "0.0.0.0" ? configuredHost : "127.0.0.1";
292
309
  const apiKey = config.apiKey || "sk-qwenproxy-local";
293
310
 
294
- if (!isFetchingLiveModels) {
295
- isFetchingLiveModels = true;
311
+ liveModelsPromise = (async () => {
296
312
  const controller = new AbortController();
297
- const timeout = setTimeout(() => controller.abort(), 2500);
298
- fetch(`http://${host}:${port}/v1/models`, {
299
- headers: { Authorization: `Bearer ${apiKey}` },
300
- signal: controller.signal,
301
- })
302
- .then(async (resp) => {
303
- clearTimeout(timeout);
304
- if (resp.ok) {
305
- const json = (await resp.json()) as any;
306
- if (Array.isArray(json?.data)) {
307
- const models = json.data
308
- .map((m: any) => m.id)
309
- .filter((id: any): id is string => typeof id === "string" && id.trim().length > 0)
310
- .filter(
311
- (id: string) =>
312
- !id.endsWith("-fast") &&
313
- !id.endsWith("-thinking") &&
314
- !id.endsWith("-no-thinking"),
315
- );
316
- if (models.length > 0) {
317
- cachedLiveModels = Array.from(new Set(models));
318
- }
313
+ const timeout = setTimeout(() => controller.abort(), 3000);
314
+ try {
315
+ const resp = await fetch(`http://${host}:${port}/v1/models`, {
316
+ headers: { Authorization: `Bearer ${apiKey}` },
317
+ signal: controller.signal,
318
+ });
319
+ if (resp.ok) {
320
+ const json = (await resp.json()) as any;
321
+ if (Array.isArray(json?.data)) {
322
+ const models = json.data
323
+ .map((m: any) => m.id)
324
+ .filter((id: any): id is string => typeof id === "string" && id.trim().length > 0)
325
+ .filter(
326
+ (id: string) =>
327
+ !id.endsWith("-fast") &&
328
+ !id.endsWith("-thinking") &&
329
+ !id.endsWith("-no-thinking"),
330
+ );
331
+ if (models.length > 0) {
332
+ cachedLiveModels = Array.from(new Set(models));
333
+ return cachedLiveModels;
319
334
  }
320
335
  }
321
- })
322
- .catch(() => {
323
- clearTimeout(timeout);
324
- })
325
- .finally(() => {
326
- isFetchingLiveModels = false;
327
- });
328
- }
336
+ }
337
+ } catch {} finally {
338
+ clearTimeout(timeout);
339
+ liveModelsPromise = null;
340
+ }
341
+ return cachedLiveModels || DEFAULT_FALLBACK_MODELS;
342
+ })();
329
343
 
330
- return (
331
- cachedLiveModels || [
332
- "qwen3.8-max",
333
- "qwen3.7-plus",
334
- "qwen3.7-max",
335
- "z-image-turbo",
336
- "qwen-image-3.0-pro",
337
- "qwen-image-3.0",
338
- "wan2.7-image-pro",
339
- "wan2.7-image",
340
- "wan3.0-video",
341
- "wan2.7-t2v",
342
- ]
343
- );
344
+ return liveModelsPromise;
344
345
  }
@@ -341,6 +341,7 @@ export class ChatView implements TuiView {
341
341
  (key.ctrl && key.name === "o") ||
342
342
  (key.meta && key.name === "m")
343
343
  ) {
344
+ void this.refreshModels();
344
345
  this.isModelModalOpen = true;
345
346
  this.modalSelectedIndex = this.selectedModelIndex;
346
347
  this.onNeedsRender?.();
package/src/update-cli.ts CHANGED
@@ -27,17 +27,32 @@ export function detectPackageManager(): PackageManager {
27
27
 
28
28
  return "npm";
29
29
  }
30
- export function getUpdateArgs(pm: PackageManager, packageName: string): { cmd: string; args: string[] } {
30
+ export function getUpdateArgs(
31
+ pm: PackageManager,
32
+ packageName: string,
33
+ targetVersion?: string,
34
+ ): { cmd: string; args: string[] } {
35
+ const versionTag = targetVersion ? `@${targetVersion}` : "@latest";
31
36
  switch (pm) {
32
37
  case "bun":
33
- return { cmd: "bun", args: ["add", "-g", `${packageName}@latest`] };
38
+ return { cmd: "bun", args: ["add", "-g", `${packageName}${versionTag}`] };
34
39
  case "pnpm":
35
- return { cmd: "pnpm", args: ["update", "-g", packageName] };
40
+ return {
41
+ cmd: "pnpm",
42
+ args: targetVersion
43
+ ? ["add", "-g", `${packageName}@${targetVersion}`]
44
+ : ["update", "-g", packageName],
45
+ };
36
46
  case "yarn":
37
- return { cmd: "yarn", args: ["global", "upgrade", packageName] };
47
+ return {
48
+ cmd: "yarn",
49
+ args: targetVersion
50
+ ? ["global", "add", `${packageName}@${targetVersion}`]
51
+ : ["global", "upgrade", packageName],
52
+ };
38
53
  case "npm":
39
54
  default:
40
- return { cmd: "npm", args: ["install", "-g", `${packageName}@latest`] };
55
+ return { cmd: "npm", args: ["install", "-g", `${packageName}${versionTag}`] };
41
56
  }
42
57
  }
43
58
 
@@ -93,7 +108,7 @@ export async function runUpdateCommand(): Promise<void> {
93
108
  }
94
109
 
95
110
  console.log(`\n🚀 Nova versão disponível: v${currentVersion} ➔ v${latestVersion}`);
96
- const { cmd, args } = getUpdateArgs(pm, packageName);
111
+ const { cmd, args } = getUpdateArgs(pm, packageName, latestVersion);
97
112
  console.log(`⏳ Atualizando globalmente via ${pm} (${cmd} ${args.join(" ")})...`);
98
113
  const fullUpdateCmd = `${cmd} ${args.join(" ")}`;
99
114
  const updateProc = spawnSync(fullUpdateCmd, {