dsh-strata-sync 0.10.3 → 0.10.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/README.md +9 -6
- package/dist/client.js +26 -31
- package/dist/index.js +5 -6
- package/dist/summary.mjs +49 -10
- package/docs/README.zh-CN.md +9 -6
- package/docs/README.zh-TW.md +9 -6
- package/docs/VALIDATION.md +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ For example, with automatic output calculation, a 32K context uses a 9,830-token
|
|
|
69
69
|
|
|
70
70
|
## Summary prompt and target length
|
|
71
71
|
|
|
72
|
-
**Dynamic summary target** is
|
|
72
|
+
**Dynamic summary target** is off by default. Enable it to supplement DSH's native summary instructions. When off, DSH uses its native summary and compaction behavior.
|
|
73
73
|
|
|
74
74
|
The editable **Supplemental summary prompt** starts with:
|
|
75
75
|
|
|
@@ -79,17 +79,20 @@ Keep the entire checkpoint, including all required headings, within a target of
|
|
|
79
79
|
|
|
80
80
|
Keep `{target}` in your text—it is replaced with the calculated token target before each supported summary. The template can contain up to 2,048 characters. Use **Restore default prompt** to reset it, then **Save preferences** to apply your choice.
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Two divisors control the target (both default to **2**, or **50%**):
|
|
83
83
|
|
|
84
84
|
| Divisor | Default | Limits the target to |
|
|
85
85
|
|---|---:|---|
|
|
86
|
-
|
|
|
87
|
-
| Summary output (O) | 2 | Actual summary output cap ÷ O |
|
|
86
|
+
| Output budget (O) | 2 | Estimated effective summary output budget ÷ O |
|
|
88
87
|
| Source length (S) | 2 | Estimated content being summarized ÷ S |
|
|
89
88
|
|
|
90
|
-
The target is the
|
|
89
|
+
The target is the smaller result, rounded down. All divisors accept decimals of at least 1. There is no context divisor; a legacy `summaryContextDivisor` value is ignored. Existing output/source divisors and explicit enable choices are preserved. New or unspecified guidance remains **off**.
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
The effective output budget is an **estimate**, not the raw requested `maxTokens` or a server-confirmed cap. It is the smaller of the request's summary cap and estimated remaining context. The latter uses the smaller verified server/client window, subtracts the full prompt (including system text, tool schemas and the supplement), and allows for the supported pi-ai adapter's 4,096-token safety reserve. Text is estimated at 3.5 characters/token with extra framing. The separate source estimate uses DSH's four-character heuristic and excludes the system prompt, tools and summary instruction. Neither estimate is an exact tokenizer count.
|
|
92
|
+
|
|
93
|
+
For example, a 4,096-token effective output budget with enough source content gives a **2,048-token target** in either 32K or 64K context. A 2,000-token source instead gives a **1,000-token target**. Less remaining context can reduce the target further. The UI previews an upper bound from the proposed summary cap, before request-specific costs are known.
|
|
94
|
+
|
|
95
|
+
This is a writing instruction, **not a hard output limit or a guarantee against truncation**. Reasoning may consume the same output budget; the plugin does not change reasoning, `maxTokens`, `fit_max_tokens` or compaction timing. The server's actual tokenizer, later middleware, or a different adapter version may impose a lower limit.
|
|
93
96
|
|
|
94
97
|
Saving summary preferences does not change model budgets. Native prompt content is preserved, and no extra inference or retry loop is added. If the request cannot be verified or safely estimated, or the target is below 256 tokens, the original summary request proceeds unchanged.
|
|
95
98
|
|
package/dist/client.js
CHANGED
|
@@ -115,9 +115,9 @@ window.__ModuleLoader__.load({
|
|
|
115
115
|
"zh-Hant": "啟用後,在每次支援的 Strata 摘要請求前,計算長度目標,並在 DSH 原生摘要提示詞後補充一句。",
|
|
116
116
|
"zh-Hans": "启用后,在每次支持的 Strata 摘要请求前,计算长度目标,并在 DSH 原生摘要提示词后补充一句。"
|
|
117
117
|
},
|
|
118
|
-
"Target = floor(min(
|
|
119
|
-
"zh-Hant": "目標 = floor(min(
|
|
120
|
-
"zh-Hans": "目标 = floor(min(
|
|
118
|
+
"Target = floor(min(estimated effective output budget / O, selected content estimate / S)). Default divisors: 2 and 2 (50% each).": {
|
|
119
|
+
"zh-Hant": "目標 = floor(min(估算有效輸出預算 / O, 待壓縮內容估算 / S))。兩個除數預設均為 2(各取 50%)。",
|
|
120
|
+
"zh-Hans": "目标 = floor(min(估算有效输出预算 / O, 待压缩内容估算 / S))。两个除数默认均为 2(各取 50%)。"
|
|
121
121
|
},
|
|
122
122
|
"When guidance cannot be applied, DSH keeps its native summary behavior.": {
|
|
123
123
|
"zh-Hant": "無法套用時,沿用 DSH 原生摘要。",
|
|
@@ -127,25 +127,21 @@ window.__ModuleLoader__.load({
|
|
|
127
127
|
"zh-Hant": "啟用摘要提示補充",
|
|
128
128
|
"zh-Hans": "启用摘要提示补充"
|
|
129
129
|
},
|
|
130
|
-
"
|
|
131
|
-
"zh-Hant": "
|
|
132
|
-
"zh-Hans": "
|
|
130
|
+
"Off by default. Enable it to supplement DSH's native summary and automatic compaction behavior.": {
|
|
131
|
+
"zh-Hant": "預設關閉。啟用後補充 DSH 原生摘要提示詞。",
|
|
132
|
+
"zh-Hans": "默认关闭。启用后补充 DSH 原生摘要提示词。"
|
|
133
133
|
},
|
|
134
|
-
"
|
|
135
|
-
"zh-Hant": "
|
|
136
|
-
"zh-Hans": "
|
|
137
|
-
},
|
|
138
|
-
"Summary output divisor (output limit ÷ O)": {
|
|
139
|
-
"zh-Hant": "摘要輸出除數(輸出上限 ÷ O)",
|
|
140
|
-
"zh-Hans": "摘要输出除数(输出上限 ÷ O)"
|
|
134
|
+
"Output budget divisor (estimated budget ÷ O)": {
|
|
135
|
+
"zh-Hant": "摘要輸出除數(估算可用預算 ÷ O)",
|
|
136
|
+
"zh-Hans": "摘要输出除数(估算可用预算 ÷ O)"
|
|
141
137
|
},
|
|
142
138
|
"Source length divisor (source estimate ÷ S)": {
|
|
143
139
|
"zh-Hant": "原文長度除數(原文估算 ÷ S)",
|
|
144
140
|
"zh-Hans": "原文长度除数(原文估算 ÷ S)"
|
|
145
141
|
},
|
|
146
|
-
"Defaults:
|
|
147
|
-
"zh-Hant": "預設
|
|
148
|
-
"zh-Hans": "默认
|
|
142
|
+
"Defaults: O = 2, S = 2 (50% each). Smaller divisors allow longer targets; the smaller of the two bounds wins. Decimals are supported.": {
|
|
143
|
+
"zh-Hant": "預設 O = 2、S = 2(各取 50%)。除數越小,目標可越長;最後取兩項限制的較小值。支援小數。",
|
|
144
|
+
"zh-Hans": "默认 O = 2、S = 2(各取 50%)。除数越小,目标可越长;最后取两项限制的较小值。支持小数。"
|
|
149
145
|
},
|
|
150
146
|
"Enter a finite divisor of at least 1; decimals are allowed.": {
|
|
151
147
|
"zh-Hant": "請輸入大於或等於 1 的有限除數,可使用小數。",
|
|
@@ -163,9 +159,9 @@ window.__ModuleLoader__.load({
|
|
|
163
159
|
"zh-Hant": "儲存偏好後,這些控制項會套用至後續摘要請求;其他模型與壓縮設定保持不變。",
|
|
164
160
|
"zh-Hans": "保存偏好后,这些控制项将应用于后续摘要请求;其他模型与压缩设置保持不变。"
|
|
165
161
|
},
|
|
166
|
-
"
|
|
167
|
-
"zh-Hant": "
|
|
168
|
-
"zh-Hans": "
|
|
162
|
+
"The preview uses the proposed summary cap. Actual targets also account for the estimated remaining context and source length; they may be lower.": {
|
|
163
|
+
"zh-Hant": "預覽採用提議的摘要上限。實際目標還會考慮估算的上下文剩餘空間與原文長度,可能更低。",
|
|
164
|
+
"zh-Hans": "预览采用提议的摘要上限。实际目标还会考虑估算的上下文剩余空间与原文长度,可能更低。"
|
|
169
165
|
},
|
|
170
166
|
"The original prompt, output cap and other settings stay unchanged. This is a writing target, not a guaranteed length or an optimal value.": {
|
|
171
167
|
"zh-Hant": "保留原生提示詞、輸出硬上限與其他設定。這是寫作目標,不保證實際長度,也不代表最佳值。",
|
|
@@ -431,20 +427,20 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
|
|
|
431
427
|
};
|
|
432
428
|
const diagnostic = (message) => translations[message] ? t(message) : language === 'en' ? message : t('Server diagnostic: ') + message;
|
|
433
429
|
const [state,setState] = React.useState(null), [preview,setPreview] = React.useState(null), [result,setResult] = React.useState(null);
|
|
434
|
-
const [form,setForm] = React.useState({providerId:'',output:'',manual:false,auto:false,follow:true,summaryGuidance:
|
|
430
|
+
const [form,setForm] = React.useState({providerId:'',output:'',manual:false,auto:false,follow:true,summaryGuidance:false,summaryOutputDivisor:'2',summarySourceDivisor:'2',summaryPrompt:''});
|
|
435
431
|
const [pending,setPending] = React.useState(false), [error,setError] = React.useState(''), [loadError,setLoadError] = React.useState('');
|
|
436
432
|
const [notice,setNotice] = React.useState(''), [conflict,setConflict] = React.useState(false);
|
|
437
433
|
const formRef=React.useRef(form), baseline=React.useRef(null), requestSeq=React.useRef(0), alive=React.useRef(true);
|
|
438
434
|
const numberValue = value => value.trim()===''?NaN:Number(value);
|
|
439
|
-
const values = (f) => ({providerId:f.providerId,outputTokens:f.manual ? (f.output.trim()==='' ? NaN : Number(f.output)) : null,autoSync:f.auto,followLoadedModel:f.follow,summaryGuidance:f.summaryGuidance,
|
|
440
|
-
const preferences = (s) => ({providerId:s.providerId || '',outputTokens:s.outputTokens ?? null,autoSync:!!s.autoSync,followLoadedModel:s.followLoadedModel !== false,summaryGuidance:s.summaryGuidance
|
|
435
|
+
const values = (f) => ({providerId:f.providerId,outputTokens:f.manual ? (f.output.trim()==='' ? NaN : Number(f.output)) : null,autoSync:f.auto,followLoadedModel:f.follow,summaryGuidance:f.summaryGuidance,summaryOutputDivisor:numberValue(f.summaryOutputDivisor),summarySourceDivisor:numberValue(f.summarySourceDivisor),summaryPromptTemplate:f.summaryPrompt});
|
|
436
|
+
const preferences = (s) => ({providerId:s.providerId || '',outputTokens:s.outputTokens ?? null,autoSync:!!s.autoSync,followLoadedModel:s.followLoadedModel !== false,summaryGuidance:s.summaryGuidance===true,summaryOutputDivisor:s.summaryOutputDivisor??2,summarySourceDivisor:s.summarySourceDivisor??2,summaryPromptTemplate:s.summaryPromptTemplate??s.defaultSummaryPromptTemplate??''});
|
|
441
437
|
const equal = (a,b) => Object.keys(a).every(key=>Object.is(a[key],b[key]));
|
|
442
438
|
const acceptStatus = React.useCallback((s,force=false) => {
|
|
443
439
|
if (!alive.current) return;
|
|
444
440
|
const next=preferences(s), old=baseline.current;
|
|
445
441
|
const untouched=!old || equal(values(formRef.current),old.values);
|
|
446
442
|
if (force || untouched) {
|
|
447
|
-
const nextForm={providerId:next.providerId,output:next.outputTokens==null?'':String(next.outputTokens),manual:next.outputTokens!=null,auto:next.autoSync,follow:next.followLoadedModel,summaryGuidance:next.summaryGuidance,
|
|
443
|
+
const nextForm={providerId:next.providerId,output:next.outputTokens==null?'':String(next.outputTokens),manual:next.outputTokens!=null,auto:next.autoSync,follow:next.followLoadedModel,summaryGuidance:next.summaryGuidance,summaryOutputDivisor:String(next.summaryOutputDivisor),summarySourceDivisor:String(next.summarySourceDivisor),summaryPrompt:next.summaryPromptTemplate};
|
|
448
444
|
formRef.current=nextForm; setForm(nextForm); setConflict(false);
|
|
449
445
|
baseline.current={values:next,revision:s.settingsRevision};
|
|
450
446
|
} else if (old.revision!==s.settingsRevision || !equal(old.values,next)) {
|
|
@@ -477,14 +473,14 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
|
|
|
477
473
|
} finally { if(alive.current)setPending(false); }
|
|
478
474
|
}
|
|
479
475
|
const draft=values(form), isDivisor=value=>Number.isFinite(value)&&value>=1&&value<=Number.MAX_SAFE_INTEGER;
|
|
480
|
-
const validDivisor=[draft.
|
|
476
|
+
const validDivisor=[draft.summaryOutputDivisor,draft.summarySourceDivisor].every(isDivisor);
|
|
481
477
|
const validPrompt=typeof draft.summaryPromptTemplate==='string'&&draft.summaryPromptTemplate.length>0&&draft.summaryPromptTemplate.length<=2048&&draft.summaryPromptTemplate.includes('{target}');
|
|
482
478
|
const valid=validDivisor&&validPrompt&&(draft.outputTokens===null || Number.isSafeInteger(draft.outputTokens)&&draft.outputTokens>0);
|
|
483
479
|
const dirty=!!baseline.current&&!equal(draft,baseline.current.values);
|
|
484
480
|
const displayedResult=!result || state?.lastApplied?.at>result.at ? state?.lastApplied : result;
|
|
485
481
|
const activeBudget=preview?.budget;
|
|
486
482
|
const summaryTargetCeiling=form.summaryGuidance&&validDivisor&&Number.isSafeInteger(activeBudget?.contextWindow)&&activeBudget.contextWindow>0&&Number.isSafeInteger(activeBudget?.summaryMaxTokens)&&activeBudget.summaryMaxTokens>0
|
|
487
|
-
? Math.floor(
|
|
483
|
+
? Math.floor(activeBudget.summaryMaxTokens/draft.summaryOutputDivisor) : null;
|
|
488
484
|
const outputSource=(budget)=>t(({server:'Strata Server default',manual:'Manual override',calculated:'Automatic calculation'})[budget?.outputSource]||'Automatic calculation');
|
|
489
485
|
const preserved=(view)=>{
|
|
490
486
|
const status=view?.capabilityStatus;
|
|
@@ -529,7 +525,9 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
|
|
|
529
525
|
h('div',{className:'ss-card','data-testid':'summary-target'},
|
|
530
526
|
h('strong',null,t('Dynamic summary target')),
|
|
531
527
|
h('label',{className:'ss-check'},h('input',{type:'checkbox','data-testid':'summary-guidance-toggle','aria-label':t('Enable summary guidance'),checked:form.summaryGuidance,onChange:e=>edit({summaryGuidance:e.target.checked}),disabled:pending||!state}),t('Enable summary guidance')),
|
|
532
|
-
h('
|
|
528
|
+
h('div',{className:'ss-grid'},
|
|
529
|
+
h('label',null,t('Output budget divisor (estimated budget ÷ O)'),h('input',{type:'number','data-testid':'summary-output-divisor','aria-label':t('Output budget divisor (estimated budget ÷ O)'),'aria-invalid':!isDivisor(draft.summaryOutputDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summaryOutputDivisor,onChange:e=>edit({summaryOutputDivisor:e.target.value}),disabled:pending||!state})),
|
|
530
|
+
h('label',null,t('Source length divisor (source estimate ÷ S)'),h('input',{type:'number','data-testid':'summary-source-divisor','aria-label':t('Source length divisor (source estimate ÷ S)'),'aria-invalid':!isDivisor(draft.summarySourceDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summarySourceDivisor,onChange:e=>edit({summarySourceDivisor:e.target.value}),disabled:pending||!state}))),
|
|
533
531
|
state&&!validDivisor&&h('p',{className:'ss-status ss-error',role:'alert'},t('Enter a finite divisor of at least 1; decimals are allowed.')),
|
|
534
532
|
h('label',{className:'ss-prompt-label'},t('Supplemental summary prompt'),h('textarea',{'data-testid':'summary-prompt','aria-label':t('Supplemental summary prompt'),'aria-invalid':!validPrompt,rows:5,value:form.summaryPrompt,onChange:e=>edit({summaryPrompt:e.target.value}),disabled:pending||!state,spellCheck:false})),
|
|
535
533
|
state&&!validPrompt&&h('p',{className:'ss-status ss-error',role:'alert'},t('Use 1–2,048 characters and include {target}.')),
|
|
@@ -540,11 +538,8 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
|
|
|
540
538
|
? h('p',{'data-testid':'summary-target-ceiling'},t('Maximum target for this preview: '),format(summaryTargetCeiling),' tokens')
|
|
541
539
|
: h('p',{className:'ss-status','data-testid':'summary-target-native'},t('This preview is below the 256-token minimum. The native summary prompt remains unchanged.'))),
|
|
542
540
|
h('details',{className:'ss-details','data-testid':'summary-details'},h('summary',null,t('Calculation and limits')),
|
|
543
|
-
h('
|
|
544
|
-
|
|
545
|
-
h('label',null,t('Source length divisor (source estimate ÷ S)'),h('input',{type:'number','data-testid':'summary-source-divisor','aria-label':t('Source length divisor (source estimate ÷ S)'),'aria-invalid':!isDivisor(draft.summarySourceDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summarySourceDivisor,onChange:e=>edit({summarySourceDivisor:e.target.value}),disabled:pending||!state}))),
|
|
546
|
-
h('p',{className:'ss-muted'},t('Target = floor(min(context / N, summary output limit / O, selected content estimate / S)).')),
|
|
547
|
-
h('p',{className:'ss-muted'},t('When guidance cannot be applied, DSH keeps its native summary behavior.')))),
|
|
541
|
+
h('p',{className:'ss-muted'},t('Target = floor(min(estimated effective output budget / O, selected content estimate / S)). Default divisors: 2 and 2 (50% each).')),
|
|
542
|
+
h('p',{className:'ss-muted'},t('The preview uses the proposed summary cap. Actual targets also account for the estimated remaining context and source length; they may be lower.'),' ',t('When guidance cannot be applied, DSH keeps its native summary behavior.')))),
|
|
548
543
|
h('div',{className:'ss-actions'},
|
|
549
544
|
h('button',{disabled:pending||!state||!form.providerId||!valid||!dirty||conflict,onClick:()=>action(async()=>{
|
|
550
545
|
const changes=Object.fromEntries(Object.entries(values(formRef.current)).filter(([key,value])=>!Object.is(value,baseline.current.values[key])));
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import z from '@deepseek-ai/schemastery';
|
|
5
5
|
import { discoverStrata, planBudget, StrataSyncError } from './core.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { DEFAULT_SUMMARY_TEMPLATE, validSummaryDivisor, validSummaryTemplate, installSummaryGuidance } from './summary.mjs';
|
|
7
7
|
|
|
8
8
|
export const inject = ['configEditor', 'connection', 'agents', 'credentials', 'agentPresets'];
|
|
9
9
|
export const Config = z.object({
|
|
@@ -11,8 +11,7 @@ export const Config = z.object({
|
|
|
11
11
|
autoSync: z.boolean().default(false),
|
|
12
12
|
followLoadedModel: z.boolean().default(true),
|
|
13
13
|
outputTokens: z.number().step(1).min(1),
|
|
14
|
-
summaryGuidance: z.boolean().default(
|
|
15
|
-
summaryContextDivisor: z.number().min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_SUMMARY_DIVISOR),
|
|
14
|
+
summaryGuidance: z.boolean().default(false),
|
|
16
15
|
summaryOutputDivisor: z.number().min(1).max(Number.MAX_SAFE_INTEGER).default(2),
|
|
17
16
|
summarySourceDivisor: z.number().min(1).max(Number.MAX_SAFE_INTEGER).default(2),
|
|
18
17
|
summaryPromptTemplate: z.string().default(DEFAULT_SUMMARY_TEMPLATE),
|
|
@@ -145,7 +144,7 @@ export function apply(ctx, initialConfig) {
|
|
|
145
144
|
function publicSettings(value = settings) {
|
|
146
145
|
return { providerId: value.providerId ?? '', autoSync: value.autoSync ?? false,
|
|
147
146
|
followLoadedModel: value.followLoadedModel ?? true, outputTokens: value.outputTokens ?? null,
|
|
148
|
-
summaryGuidance: value.summaryGuidance ??
|
|
147
|
+
summaryGuidance: value.summaryGuidance ?? false,
|
|
149
148
|
summaryOutputDivisor: value.summaryOutputDivisor ?? 2, summarySourceDivisor: value.summarySourceDivisor ?? 2,
|
|
150
149
|
summaryPromptTemplate: value.summaryPromptTemplate ?? DEFAULT_SUMMARY_TEMPLATE };
|
|
151
150
|
}
|
|
@@ -389,7 +388,7 @@ export function apply(ctx, initialConfig) {
|
|
|
389
388
|
}
|
|
390
389
|
async function configure(request) {
|
|
391
390
|
ensureOpen();
|
|
392
|
-
payload(request, ['providerId', 'autoSync', 'followLoadedModel', 'outputTokens', 'summaryGuidance', '
|
|
391
|
+
payload(request, ['providerId', 'autoSync', 'followLoadedModel', 'outputTokens', 'summaryGuidance', 'summaryOutputDivisor', 'summarySourceDivisor', 'summaryPromptTemplate', 'expectedRevision']);
|
|
393
392
|
refreshSettings();
|
|
394
393
|
if (typeof request.expectedRevision !== 'string' || request.expectedRevision !== hash(publicSettings())) {
|
|
395
394
|
refuse('stale-settings', 'Sync preferences changed. Reload the latest preferences before saving.');
|
|
@@ -405,7 +404,7 @@ export function apply(ctx, initialConfig) {
|
|
|
405
404
|
if (typeof request[key] !== 'boolean') refuse('invalid-request', `${key} must be a boolean.`);
|
|
406
405
|
next[key] = request[key];
|
|
407
406
|
}
|
|
408
|
-
for (const key of ['
|
|
407
|
+
for (const key of ['summaryOutputDivisor', 'summarySourceDivisor']) {
|
|
409
408
|
if (!own(request, key)) continue;
|
|
410
409
|
if (!validSummaryDivisor(request[key])) refuse('invalid-budget', 'Summary divisors must be finite numbers from 1 to Number.MAX_SAFE_INTEGER.');
|
|
411
410
|
next[key] = request[key];
|
package/dist/summary.mjs
CHANGED
|
@@ -12,11 +12,9 @@ const augmentedMessages = globalThis[Symbol.for('dsh-strata-sync/summary-augment
|
|
|
12
12
|
// Eight mandatory headings need enough space for a meaningful checkpoint. Do
|
|
13
13
|
// not round a tiny target UP; leave native behavior intact instead.
|
|
14
14
|
export const MIN_SUMMARY_TARGET = 256;
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (![contextWindow, summaryMaxTokens, sourceTokens].every(positive) || ![divisor, outputDivisor, sourceDivisor].every(validSummaryDivisor)) return null;
|
|
19
|
-
const target = Math.floor(Math.min(contextWindow / divisor, summaryMaxTokens / outputDivisor, sourceTokens / sourceDivisor));
|
|
15
|
+
export function planSummaryTarget(effectiveOutputTokens, sourceTokens, outputDivisor = 2, sourceDivisor = 2) {
|
|
16
|
+
if (![effectiveOutputTokens, sourceTokens].every(positive) || ![outputDivisor, sourceDivisor].every(validSummaryDivisor)) return null;
|
|
17
|
+
const target = Math.floor(Math.min(effectiveOutputTokens / outputDivisor, sourceTokens / sourceDivisor));
|
|
20
18
|
return target >= MIN_SUMMARY_TARGET ? target : null;
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -68,6 +66,47 @@ export function estimateSummarySource(messages) {
|
|
|
68
66
|
return positive(tokens) ? tokens : null;
|
|
69
67
|
}
|
|
70
68
|
|
|
69
|
+
/** Conservative text-only estimate of the output room before provider dispatch.
|
|
70
|
+
* The supported DSH adapter uses pi-ai 1.1.0: 3.5 chars/token and
|
|
71
|
+
* reserves 4096 tokens before clamping maxTokens. Replayed assistant usage is
|
|
72
|
+
* zeroed by DSH. Include system text, tool schemas and framing as well, even
|
|
73
|
+
* where that adapter's heuristic omits them. This is NOT a tokenizer count or
|
|
74
|
+
* the server's final cap; unknown content leaves the native prompt untouched.
|
|
75
|
+
*/
|
|
76
|
+
export function estimateEffectiveSummaryBudget(options, contextWindow, template = DEFAULT_SUMMARY_TEMPLATE) {
|
|
77
|
+
if (!positive(contextWindow) || !positive(options?.maxTokens) || !validSummaryTemplate(template) || !Array.isArray(options.messages)) return null;
|
|
78
|
+
const price = text => Math.ceil(text.length / 3.5) + 4;
|
|
79
|
+
let input = 0;
|
|
80
|
+
try {
|
|
81
|
+
if (options.system !== undefined) {
|
|
82
|
+
if (typeof options.system !== 'string') return null;
|
|
83
|
+
input += price(options.system);
|
|
84
|
+
}
|
|
85
|
+
if (options.tools !== undefined) {
|
|
86
|
+
if (!Array.isArray(options.tools)) return null;
|
|
87
|
+
input += price(JSON.stringify(options.tools));
|
|
88
|
+
}
|
|
89
|
+
for (const message of options.messages) {
|
|
90
|
+
if (!['system', 'user', 'assistant', 'tool'].includes(message?.role) || !Array.isArray(message.content)) return null;
|
|
91
|
+
input += 4;
|
|
92
|
+
if (message.role === 'tool' && !message.content.some(block => block.type === 'text' && block.text?.length)) input += price('(no output)');
|
|
93
|
+
for (const block of message.content) {
|
|
94
|
+
if (['text', 'reasoning'].includes(block?.type) && typeof block.text === 'string') input += price(block.text);
|
|
95
|
+
else if (block?.type === 'tool-call' && typeof block.name === 'string' && typeof block.arguments === 'string') {
|
|
96
|
+
// JSON normalization can expand compact numeric literals (e.g. 1e9).
|
|
97
|
+
const normalized = JSON.stringify(JSON.parse(block.arguments));
|
|
98
|
+
input += price(block.name) + Math.max(price(block.arguments), price(normalized));
|
|
99
|
+
} else return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Reserve the longest possible rendered supplement once, avoiding a
|
|
103
|
+
// self-referential target calculation. The target never exceeds maxTokens.
|
|
104
|
+
input += price('\n- ' + summaryInstruction(options.maxTokens, template));
|
|
105
|
+
} catch { return null; }
|
|
106
|
+
if (!Number.isSafeInteger(input)) return null;
|
|
107
|
+
return Math.max(0, Math.min(options.maxTokens, contextWindow - input - 4096));
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
/** The native waterfall's next() closes over options, so replace only its
|
|
72
111
|
* messages property for the lifetime of this stream; never mutate frozen
|
|
73
112
|
* messages or recursively dispatch a second LLM request.
|
|
@@ -77,13 +116,12 @@ export function installSummaryGuidance(ctx, resolveContext, getSettings = () =>
|
|
|
77
116
|
function preferences() {
|
|
78
117
|
try {
|
|
79
118
|
const value = getSettings();
|
|
80
|
-
const enabled = value.summaryGuidance === undefined ?
|
|
81
|
-
const divisor = value.summaryContextDivisor === undefined ? DEFAULT_SUMMARY_DIVISOR : value.summaryContextDivisor;
|
|
119
|
+
const enabled = value.summaryGuidance === undefined ? false : value.summaryGuidance;
|
|
82
120
|
const outputDivisor = value.summaryOutputDivisor === undefined ? 2 : value.summaryOutputDivisor;
|
|
83
121
|
const sourceDivisor = value.summarySourceDivisor === undefined ? 2 : value.summarySourceDivisor;
|
|
84
122
|
const template = value.summaryPromptTemplate === undefined ? DEFAULT_SUMMARY_TEMPLATE : value.summaryPromptTemplate;
|
|
85
|
-
return enabled === true && [
|
|
86
|
-
? {
|
|
123
|
+
return enabled === true && [outputDivisor, sourceDivisor].every(validSummaryDivisor) && validSummaryTemplate(template)
|
|
124
|
+
? { outputDivisor, sourceDivisor, template } : null;
|
|
87
125
|
} catch { return null; }
|
|
88
126
|
}
|
|
89
127
|
const remove = ctx.on('llm/stream', (options, next) => {
|
|
@@ -103,7 +141,8 @@ export function installSummaryGuidance(ctx, resolveContext, getSettings = () =>
|
|
|
103
141
|
let contextWindow;
|
|
104
142
|
try { contextWindow = await resolveContext(options); }
|
|
105
143
|
catch { /* Metadata/compatibility failure must not break native inference. */ }
|
|
106
|
-
const
|
|
144
|
+
const effectiveOutput = estimateEffectiveSummaryBudget(options, contextWindow, selected.template);
|
|
145
|
+
const target = planSummaryTarget(effectiveOutput, sourceTokens, selected.outputDivisor, selected.sourceDivisor);
|
|
107
146
|
const current = preferences();
|
|
108
147
|
if (!closed && !options.signal?.aborted && current && Object.keys(selected).every(key => selected[key] === current[key]) && target !== null && options.messages === original) {
|
|
109
148
|
const last = { ...native, content: [{ ...native.content[0], text: `${native.content[0].text}\n- ${summaryInstruction(target, selected.template)}` }] };
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -69,7 +69,7 @@ dsh plugin --profile desktop add dsh-strata-sync --registry=https://registry.npm
|
|
|
69
69
|
|
|
70
70
|
## 摘要提示词与目标长度
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
**动态摘要目标**默认关闭。启用后会在 DSH 原生摘要指令后补充要求;关闭时使用 DSH 原生摘要与压缩行为。
|
|
73
73
|
|
|
74
74
|
可编辑的**摘要补充提示词**默认内容为:
|
|
75
75
|
|
|
@@ -79,17 +79,20 @@ Keep the entire checkpoint, including all required headings, within a target of
|
|
|
79
79
|
|
|
80
80
|
请保留 `{target}`,每次支持的摘要请求前,它会替换成计算出的 token 目标。模板最多 2,048 个字符。点击**恢复默认提示词**可重置内容,再点击**保存偏好**应用修改。
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
两个除数控制目标,默认均为 **2(各取 50%)**:
|
|
83
83
|
|
|
84
84
|
| 除数 | 默认值 | 将目标限制为 |
|
|
85
85
|
|---|---:|---|
|
|
86
|
-
|
|
|
87
|
-
| 摘要输出(O) | 2 | 本次摘要实际输出上限 ÷ O |
|
|
86
|
+
| 摘要输出(O) | 2 | 估算有效摘要输出预算 ÷ O |
|
|
88
87
|
| 原文长度(S) | 2 | 待摘要内容的估算长度 ÷ S |
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
目标取两项结果的较小值,再向下取整。除数支持大于或等于 1 的小数。已移除上下文除数;旧设置中的 `summaryContextDivisor` 不再参与计算。已有输出/原文除数及明确的启用选择会保留,新安装或未指定时仍**默认关闭**。
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
有效输出预算是**估算值**,不是直接把请求的 `maxTokens` 当作服务器确认的上限。它取本次摘要输出上限与估算上下文剩余空间中的较小值;后者使用已确认的服务器/客户端窗口较小值,扣除完整提示内容(包含系统提示、工具定义和补充句),再预留支持的 pi-ai 适配器所用的 4,096 tokens 安全空间。文本以每 token 约 3.5 字符并加上格式成本估算。原文长度另采用 DSH 的每 token 约 4 字符估算,不含系统提示、工具和摘要指令。两者都不是精确 tokenizer 计数。
|
|
92
|
+
|
|
93
|
+
例如,有效输出预算为 4,096 tokens 且原文足够长时,32K 与 64K 上下文的目标都是 **2,048 tokens**;原文只有约 2,000 tokens 时,目标为 **1,000 tokens**。上下文剩余空间不足时,目标会进一步降低。界面仅以提议的摘要上限预览目标上界,尚未扣除个别请求的成本。
|
|
94
|
+
|
|
95
|
+
这是写作指令,**不是输出硬上限,也不能保证不截断**。思考可能共用输出预算;插件不会修改思考设置、`maxTokens`、`fit_max_tokens` 或压缩时机。服务器实际 tokenizer、后续中间层或不同适配器版本仍可能产生更低的上限。
|
|
93
96
|
|
|
94
97
|
保存摘要偏好不会改变模型预算,也不会替换原生提示词或增加额外推理、重试循环。无法验证请求、无法安全估算内容,或目标小于 256 tokens 时,会沿用原本的摘要请求。
|
|
95
98
|
|
package/docs/README.zh-TW.md
CHANGED
|
@@ -69,7 +69,7 @@ dsh plugin --profile desktop add dsh-strata-sync --registry=https://registry.npm
|
|
|
69
69
|
|
|
70
70
|
## 摘要提示詞與目標長度
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
**動態摘要目標**預設關閉。啟用後會在 DSH 原生摘要指令後補充要求;關閉時使用 DSH 原生摘要與壓縮行為。
|
|
73
73
|
|
|
74
74
|
可編輯的**摘要補充提示詞**預設內容為:
|
|
75
75
|
|
|
@@ -79,17 +79,20 @@ Keep the entire checkpoint, including all required headings, within a target of
|
|
|
79
79
|
|
|
80
80
|
請保留 `{target}`,每次支援的摘要請求前,它會替換成計算出的 token 目標。範本最多 2,048 個字元。點選**還原預設提示詞**可重設內容,再點選**儲存偏好**套用修改。
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
兩個除數控制目標,預設均為 **2(各取 50%)**:
|
|
83
83
|
|
|
84
84
|
| 除數 | 預設值 | 將目標限制為 |
|
|
85
85
|
|---|---:|---|
|
|
86
|
-
|
|
|
87
|
-
| 摘要輸出(O) | 2 | 本次摘要實際輸出上限 ÷ O |
|
|
86
|
+
| 摘要輸出(O) | 2 | 估算有效摘要輸出預算 ÷ O |
|
|
88
87
|
| 原文長度(S) | 2 | 待摘要內容的估算長度 ÷ S |
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
目標取兩項結果的較小值,再向下取整。除數支援大於或等於 1 的小數。已移除上下文除數;舊設定中的 `summaryContextDivisor` 不再參與計算。既有輸出/原文除數及明確的啟用選擇會保留,新安裝或未指定時仍**預設關閉**。
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
有效輸出預算是**估算值**,不是直接把請求的 `maxTokens` 當作伺服器確認的上限。它取本次摘要輸出上限與估算上下文剩餘空間中的較小值;後者使用已確認的伺服器/客戶端視窗較小值,扣除完整提示內容(包含系統提示、工具定義和補充句),再預留支援的 pi-ai 轉接器所用的 4,096 tokens 安全空間。文字以每 token 約 3.5 字元並加上格式成本估算。原文長度另採 DSH 的每 token 約 4 字元估算,不含系統提示、工具和摘要指令。兩者都不是精確 tokenizer 計數。
|
|
92
|
+
|
|
93
|
+
例如,有效輸出預算為 4,096 tokens 且原文足夠長時,32K 與 64K 上下文的目標都是 **2,048 tokens**;原文只有約 2,000 tokens 時,目標為 **1,000 tokens**。上下文剩餘空間不足時,目標會進一步降低。介面僅以提議的摘要上限預覽目標上界,尚未扣除個別請求的成本。
|
|
94
|
+
|
|
95
|
+
這是寫作指令,**不是輸出硬上限,也不能保證不截斷**。思考可能共用輸出預算;外掛不會修改思考設定、`maxTokens`、`fit_max_tokens` 或壓縮時機。伺服器實際 tokenizer、後續中介層或不同轉接器版本仍可能產生更低的上限。
|
|
93
96
|
|
|
94
97
|
儲存摘要偏好不會改變模型預算,也不會替換原生提示詞或增加額外推論、重試循環。無法驗證請求、無法安全估算內容,或目標小於 256 tokens 時,會沿用原本的摘要請求。
|
|
95
98
|
|
package/docs/VALIDATION.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Validation date: 2026-10-30 (Asia/Taipei).
|
|
4
4
|
|
|
5
|
+
## Two-bound summary target 0.10.4
|
|
6
|
+
|
|
7
|
+
- The target uses only estimated effective output budget / output divisor and selected-source estimate / source divisor. Both divisors default to 2 (50%). The context-divisor control and formula term are removed; legacy stored context divisors are ignored without enabling guidance.
|
|
8
|
+
- Guidance remains off by default in schema, host fallback, request middleware and all three UI locales. Explicit enable choices, output/source divisors and editable templates remain supported. No production settings were edited. Installing the release is left to the user.
|
|
9
|
+
- Effective output room is estimated conservatively against the supported pi-ai 1.1.0 context clamp, including the supplement, system text and tools. It is not a server tokenizer result. Reasoning and later request transformations can still reduce usable answer space.
|
|
10
|
+
- All **155 automated tests pass**. Tests cover the actual pi-ai clamp on multiple context/input sizes, near-full and exhausted context, independent source length, additional prompt costs, ignored legacy divisors, disabled-mode zero discovery, restored native requests and localized controls. The package verifier also passes.
|
|
11
|
+
- No model inference requests were sent. Tests do not prove truncation is impossible; this supplement is a writing target and leaves native hard caps, reasoning and server configuration unchanged.
|
|
12
|
+
|
|
13
|
+
## Earlier opt-in summary validation (before the two-bound update)
|
|
14
|
+
|
|
15
|
+
- Summary guidance now defaults to `false` in the schema, host fallback, request hook and UI. An existing explicit `true` remains enabled; all other budgets and summary parameters are preserved.
|
|
16
|
+
- Added a three-language UI regression for an omitted preference. Host and middleware tests verify that omitted/disabled guidance preserves native requests and makes no metadata calls; explicit opt-in still appends the configured sentence.
|
|
17
|
+
- All 152 automated tests pass, including 39 UI tests and the package verifier. No inference requests were sent.
|
|
18
|
+
|
|
5
19
|
## Concise summary help 0.10.1
|
|
6
20
|
|
|
7
21
|
- Reduces the expanded summary help to the calculation formula and one native-fallback sentence in all three locales. Full explanations remain in the READMEs. No runtime behavior, settings, defaults or prompt text changes.
|