zames_pro 2.75.3 → 2.75.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.75.4] - 2026-10-15
11
+
12
+ ### Added
13
+
14
+ - **qwen:** per-model context window for the status bar and auto-compact
15
+
16
+ ### Changed
17
+
18
+ - **test:** format qwen-context test with prettier
19
+
10
20
  ## [2.75.3] - 2026-10-15
11
21
 
12
22
  ### Fixed
package/dist/config.js CHANGED
@@ -21,6 +21,13 @@ export const DEFAULTS = {
21
21
  qwen: {
22
22
  model: '',
23
23
  enabled: false,
24
+ // Qwen's context window is PER-MODEL, not a site constant: Qwen3-Max is
25
+ // 258K while the 3.7/3.8-Max and Coder builds are 1M. The default assumes
26
+ // the common Qwen3-Max; `/model` re-derives it from the picked id (see
27
+ // qwenContextWindow in src/qwen-ui.ts) and `/config set qwen.contextLimit`
28
+ // overrides it. Kept SEPARATE from ui.contextLimit (DeepSeek's 1M) so one
29
+ // backend's window never becomes the other's denominator.
30
+ contextLimit: 258_048,
24
31
  // Qwen DOES rate-limit a rapid burst: measured live, the 5th back-to-back
25
32
  // send at 0 ms gap hit "too frequent" and needed a 5-min back-off. A 1 s
26
33
  // gap was too small for a real editing session, so the default is now 4 s
@@ -237,6 +244,14 @@ export const CONFIG_SCHEMA = [
237
244
  labelKey: 'cfg.f.qwen_model',
238
245
  groupKey: 'cfg.group.browser',
239
246
  },
247
+ {
248
+ path: 'qwen.contextLimit',
249
+ type: 'number',
250
+ min: 10_000,
251
+ max: 10_000_000,
252
+ labelKey: 'cfg.f.qwen_contextLimit',
253
+ groupKey: 'cfg.group.browser',
254
+ },
240
255
  {
241
256
  path: 'qwen.minSendIntervalMs',
242
257
  type: 'number',
@@ -11,6 +11,10 @@ export const configMessages = {
11
11
  ru: 'Модель Qwen (id)',
12
12
  en: 'Qwen model (id)',
13
13
  },
14
+ 'cfg.f.qwen_contextLimit': {
15
+ ru: 'Qwen: размер контекста, токены (258K у Qwen3-Max, 1M у 3.7/3.8/Coder)',
16
+ en: 'Qwen: context window, tokens (258K on Qwen3-Max, 1M on 3.7/3.8/Coder)',
17
+ },
14
18
  'cfg.f.qwen_minSendIntervalMs': {
15
19
  ru: 'Qwen: пауза между отправками (мс)',
16
20
  en: 'Qwen: pause between sends (ms)',
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ import { applyBrowserLiveConfig, applyStoreLiveConfig } from './config-live.js';
33
33
  import { buildHandoffText, saveProviderHandoff, readProviderHandoff, clearProviderHandoff, } from './provider-handoff.js';
34
34
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
35
35
  import { createCliArgs } from './cli-args.js';
36
+ import { qwenContextWindow } from './qwen-ui.js';
36
37
  import { renderMarkdown, setAnswerWidth } from './markdown.js';
37
38
  import { extractCodeBlocks } from './code-blocks.js';
38
39
  import { contentWidth } from './width.js';
@@ -660,6 +661,42 @@ function providerAnswerTimeoutMs() {
660
661
  function browserProviderUrl() {
661
662
  return providerChatUrlPrefix(config.provider);
662
663
  }
664
+ /**
665
+ * The context window (tokens) for the ACTIVE provider. The two backends
666
+ * advertise different windows and Qwen's is PER-MODEL, so a single global
667
+ * number would either overstate Qwen's (258K models) or understate the 1M
668
+ * builds — and the number is the denominator of the auto-compact threshold,
669
+ * so an error directly delays (or prematurely fires) compaction.
670
+ *
671
+ * `config.qwen.contextLimit` / `ui.contextLimit` are the operator-tunable
672
+ * values; `/model` rewrites the Qwen one from the picked model id.
673
+ */
674
+ function effectiveContextLimit() {
675
+ if (config.provider === 'qwen') {
676
+ return config.qwen.contextLimit > 0
677
+ ? config.qwen.contextLimit
678
+ : config.ui.contextLimit;
679
+ }
680
+ return config.ui.contextLimit;
681
+ }
682
+ /**
683
+ * Re-derive `qwen.contextLimit` from a picked model id and persist it. Called
684
+ * by `/model` on an explicit pick (never on startup), so a manual
685
+ * `/config set qwen.contextLimit` survives until the operator switches model
686
+ * again. An UNKNOWN model id leaves the limit alone: guessing would either
687
+ * fire auto-compact too early or too late. The live editor is repainted by
688
+ * the caller (it owns the `editor` reference); this only touches config.
689
+ */
690
+ function applyModelContextLimit(model) {
691
+ const window = qwenContextWindow(model);
692
+ if (window === null)
693
+ return;
694
+ config.qwen.contextLimit = window;
695
+ try {
696
+ writeConfigValue('home', 'qwen.contextLimit', String(window));
697
+ }
698
+ catch { }
699
+ }
663
700
  /**
664
701
  * Build the browser for a provider. Called at startup AND on a live
665
702
  * `/provider` switch, so both paths construct the class the same way. The
@@ -756,8 +793,9 @@ async function printRestoredHistory(browser, ui, chatId = null) {
756
793
  out(theme.dim(t('chats.history_tokens', { v: String(restoredTokens) })));
757
794
  // Warn when the restored chat is near the context limit, and suggest
758
795
  // /compact. The threshold mirrors the auto-compact one so the operator
759
- // sees the same signal whether or not auto-compact is enabled.
760
- const limit = config.ui.contextLimit;
796
+ // sees the same signal whether or not auto-compact is enabled. The limit
797
+ // is the ACTIVE provider's window (Qwen's is per-model).
798
+ const limit = effectiveContextLimit();
761
799
  if (limit > 0) {
762
800
  const pct = (restoredTokens / limit) * 100;
763
801
  if (pct >= 80) {
@@ -1292,8 +1330,10 @@ async function main() {
1292
1330
  // Task-list summary ("tasks: 2/5") before the context counter. Pulled on
1293
1331
  // every render, like the queue badge.
1294
1332
  ed.onTasksQuery = () => todosSummary();
1295
- // Use the configured context window for the fill percentage/color.
1296
- ed.setContextLimit(config.ui.contextLimit);
1333
+ // Use the configured context window for the fill percentage/color. The
1334
+ // window is PROVIDER-dependent: Qwen's is per-model (258K/1M, see
1335
+ // qwenContextWindow), DeepSeek's is the ui.contextLimit.
1336
+ ed.setContextLimit(effectiveContextLimit());
1297
1337
  // Toggle icons (🧠 deep thinking, 🌐 web search) before the context
1298
1338
  // counter. The editor pulls the live state on every render.
1299
1339
  ed.onToggleQuery = () => browser.getToggleStatesSync();
@@ -1380,7 +1420,7 @@ async function main() {
1380
1420
  chatId: currentChatId,
1381
1421
  queueLength: pendingQueue.length,
1382
1422
  tokens: browser.getLastTokenUsage(),
1383
- contextLimit: config.ui.contextLimit,
1423
+ contextLimit: effectiveContextLimit(),
1384
1424
  tasks: todosSummary(),
1385
1425
  locale: currentLocale,
1386
1426
  });
@@ -1690,10 +1730,13 @@ async function main() {
1690
1730
  }
1691
1731
  // The context window size feeds the status-bar percentage and its color.
1692
1732
  // Apply it to the live editor immediately (the value is a plain number,
1693
- // no restart needed).
1694
- if (path === 'ui.contextLimit' && typeof value === 'number') {
1733
+ // no restart needed). A change to EITHER provider's limit matters: the
1734
+ // Qwen one is what a /model pick rewrites, and the active provider is
1735
+ // what decides which value the editor shows.
1736
+ if ((path === 'ui.contextLimit' || path === 'qwen.contextLimit') &&
1737
+ typeof value === 'number') {
1695
1738
  if (editor) {
1696
- editor.setContextLimit(value);
1739
+ editor.setContextLimit(effectiveContextLimit());
1697
1740
  editor.refreshStatus();
1698
1741
  }
1699
1742
  }
@@ -2556,6 +2599,16 @@ async function main() {
2556
2599
  config.qwen.model = picked;
2557
2600
  if (typeof qb.model === 'string')
2558
2601
  qb.model = picked;
2602
+ // Derive the context window from the picked model: Qwen windows
2603
+ // are per-model (258K vs 1M), and this number is the denominator
2604
+ // of the auto-compact threshold. The operator's manual
2605
+ // `/config set qwen.contextLimit` still wins afterwards (this
2606
+ // only runs on an explicit pick).
2607
+ applyModelContextLimit(picked);
2608
+ if (editor) {
2609
+ editor.setContextLimit(effectiveContextLimit());
2610
+ editor.refreshStatus();
2611
+ }
2559
2612
  try {
2560
2613
  writeConfigValue('home', 'qwen.model', picked);
2561
2614
  }
@@ -2571,6 +2624,11 @@ async function main() {
2571
2624
  config.qwen.model = arg;
2572
2625
  if (typeof qb.model === 'string')
2573
2626
  qb.model = arg;
2627
+ applyModelContextLimit(arg);
2628
+ if (editor) {
2629
+ editor.setContextLimit(effectiveContextLimit());
2630
+ editor.refreshStatus();
2631
+ }
2574
2632
  try {
2575
2633
  writeConfigValue('home', 'qwen.model', arg);
2576
2634
  }
@@ -3257,7 +3315,7 @@ async function main() {
3257
3315
  saveLastChat(chatId, currentWorkdir);
3258
3316
  }
3259
3317
  },
3260
- contextLimit: config.ui.contextLimit,
3318
+ contextLimit: effectiveContextLimit(),
3261
3319
  getTokenUsage: () => browser.getLastTokenUsage(),
3262
3320
  goal: sessionGoal,
3263
3321
  });
@@ -3372,7 +3430,7 @@ async function main() {
3372
3430
  mcpTools: mcpStatus.toolCount,
3373
3431
  transcriptOk: !!transcript.file,
3374
3432
  authSaved: authMarkerExists(),
3375
- contextLimit: config.ui.contextLimit,
3433
+ contextLimit: effectiveContextLimit(),
3376
3434
  hasOrigin: gitOk
3377
3435
  ? (await runGitArgs(['remote'], currentWorkdir, 5000)).includes('origin')
3378
3436
  : undefined,
@@ -3890,7 +3948,7 @@ async function main() {
3890
3948
  }
3891
3949
  : null,
3892
3950
  autoCompactPct: config.browser.autoCompactPct,
3893
- contextLimit: config.ui.contextLimit,
3951
+ contextLimit: effectiveContextLimit(),
3894
3952
  getTokenUsage: () => browser.getLastTokenUsage(),
3895
3953
  goal: sessionGoal,
3896
3954
  // Completion-goal mode (Claude Code parity): the goal is a CONDITION,
package/dist/qwen-ui.js CHANGED
@@ -76,6 +76,47 @@ export const NEW_CHAT_SELECTOR = '.new-chat, button[aria-label="New Chat" i]';
76
76
  // model itself is chosen via the config (sent with each completion request)
77
77
  // unless a per-session override is set.
78
78
  export const MODEL_TRIGGER_SELECTOR = '.wms-trigger';
79
+ // ---------- context windows ----------
80
+ // Qwen's per-model context window (tokens). The window is a property of the
81
+ // MODEL, not the site: Qwen3-Max is 258K while the 3.7/3.8-Max and Coder
82
+ // builds are 1M. zames uses the number ONLY as the denominator for the
83
+ // status-bar fill percentage and the auto-compact threshold, so a wrong value
84
+ // either fires compaction far too late (window overstated) or too early.
85
+ //
86
+ // Matching is done on a NORMALIZED id: lowercased, non-alphanumerics dropped,
87
+ // so `qwen3-max`, `Qwen3-Max` and `qwen3_max` all hit the same row. The list is
88
+ // ordered MOST SPECIFIC first: a `qwen3-coder-plus` id must not stop at a
89
+ // generic `qwen3` rule. Values verified against the public model cards
90
+ // (contextwindows.dev / haimaker.ai) in October 2026.
91
+ const CONTEXT_WINDOWS = [
92
+ // The 1M builds MUST be checked before the 258K `qwen3-max` family, or
93
+ // `qwen3.7-max` would match the shorter `max` rule first.
94
+ [/qwen3\.?7.?max/, 1_000_000],
95
+ [/qwen3\.?8.?max/, 1_000_000],
96
+ [/qwen3coder/, 1_000_000],
97
+ [/qwenlong/, 10_000_000],
98
+ [/qwen3max/, 258_048],
99
+ // Generic fallback: the OTHER qwen3 builds (Plus/Turbo/Flash) advertised 1M
100
+ // on their model cards, so an unknown qwen3 id gets 1M rather than the min.
101
+ [/qwen3/, 1_000_000],
102
+ ];
103
+ /** Lowercase and drop every non-alphanumeric so `qwen3.7-max` -> `qwen37max`. */
104
+ export function normalizeModelId(model) {
105
+ return String(model || '')
106
+ .toLowerCase()
107
+ .replace(/[^a-z0-9]/g, '');
108
+ }
109
+ /** The context window (tokens) for a Qwen model id, or null when unknown. */
110
+ export function qwenContextWindow(model) {
111
+ const id = normalizeModelId(model);
112
+ if (!id)
113
+ return null;
114
+ for (const [re, window] of CONTEXT_WINDOWS) {
115
+ if (re.test(id))
116
+ return window;
117
+ }
118
+ return null;
119
+ }
79
120
  // ---------- age gate / cookies ----------
80
121
  // A first-time visitor must confirm their birth year before chatting. The
81
122
  // dialog is an ant-modal; the Continue button is the black footer button.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.3",
3
+ "version": "2.75.4",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {