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 +7 -0
- package/package.json +1 -1
- package/src/core/config.ts +2 -0
- package/src/core/model-alias.ts +47 -7
- package/src/routes/chat/validation.ts +1 -1
- package/src/services/fingerprint.ts +45 -5
- package/src/services/playwright.ts +15 -0
- package/src/services/qwen-chat-pool.ts +4 -4
- package/src/services/qwen-headers.ts +28 -6
- package/src/sync/omp.ts +27 -21
- package/src/sync/opencode.ts +34 -30
- package/src/tui/app.ts +1 -1
- package/src/tui/proxy-client.ts +50 -49
- package/src/tui/views/chat-view.ts +1 -0
- package/src/update-cli.ts +21 -6
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.
|
|
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": {
|
package/src/core/config.ts
CHANGED
|
@@ -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(
|
package/src/core/model-alias.ts
CHANGED
|
@@ -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
|
-
*
|
|
69
|
-
*
|
|
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(
|
|
107
|
+
export function mapClientModelToQwen(
|
|
108
|
+
model: string,
|
|
109
|
+
enableAliases = true,
|
|
110
|
+
): string {
|
|
75
111
|
if (!model) return model;
|
|
76
|
-
|
|
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
|
-
|
|
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,
|
|
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 = `${
|
|
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(
|
|
192
|
-
{ brand: "Chromium", version: String(
|
|
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
|
-
|
|
156
|
+
_model: string,
|
|
157
157
|
chatId: string,
|
|
158
158
|
) {
|
|
159
|
-
return `${accountId || "global"}:${
|
|
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,
|
|
187
|
-
return
|
|
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":
|
|
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 ||
|
|
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":
|
|
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(
|
|
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
|
-
|
|
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}`;
|
package/src/sync/opencode.ts
CHANGED
|
@@ -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(
|
|
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 :
|
|
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 = "
|
|
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");
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -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
|
|
282
|
+
let liveModelsPromise: Promise<string[]> | null = null;
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
295
|
-
isFetchingLiveModels = true;
|
|
311
|
+
liveModelsPromise = (async () => {
|
|
296
312
|
const controller = new AbortController();
|
|
297
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
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}
|
|
38
|
+
return { cmd: "bun", args: ["add", "-g", `${packageName}${versionTag}`] };
|
|
34
39
|
case "pnpm":
|
|
35
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
cmd: "pnpm",
|
|
42
|
+
args: targetVersion
|
|
43
|
+
? ["add", "-g", `${packageName}@${targetVersion}`]
|
|
44
|
+
: ["update", "-g", packageName],
|
|
45
|
+
};
|
|
36
46
|
case "yarn":
|
|
37
|
-
return {
|
|
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}
|
|
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, {
|