@raolin2025/claude-code-node 2.8.15 → 2.8.16
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 +19 -0
- package/package.json +1 -1
- package/src/__tests__/small-model.test.js +37 -0
- package/src/core/cli.js +5 -0
- package/src/core/query-engine.js +33 -1
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ cc-node --resume session-1747000000000-abc123
|
|
|
69
69
|
| `--no-stream` | | 禁用流式响应 | `false` |
|
|
70
70
|
| `--max-messages` | | 消息条数上限,超过则折叠早期历史为摘要(解决本地小模型"条数过多变傻") | `0`(关闭) |
|
|
71
71
|
| `--small-model` | | 小模型适配模式(强制工具调用 + 敷衍重试 + 意图引导 + 工具精简) | `false` |
|
|
72
|
+
| `--max-output-tokens` | | 覆盖单次响应输出上限(默认根据上下文窗口动态计算) | 窗口×1/16 |
|
|
72
73
|
| `--stdio` | | **JSON-RPC 服务器模式**(供桥接层/外部客户端接入,见下) | |
|
|
73
74
|
| `--help` | `-h` | 显示帮助 | |
|
|
74
75
|
|
|
@@ -177,6 +178,24 @@ cc-node 会自动感知当前所用模型的**上下文窗口长度**,并在
|
|
|
177
178
|
|
|
178
179
|
> 切换模型(`/model`)后会自动重新探测窗口。`/budget` 也会显示当前窗口与 80% 触发阈值。
|
|
179
180
|
|
|
181
|
+
### 📤 单次输出上限(max_tokens)动态计算
|
|
182
|
+
|
|
183
|
+
cc-node 的**单次响应输出上限**(`max_tokens`)不再写死 4096,而是**根据上下文窗口大小动态计算**:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
max_tokens = max(4096, 窗口大小 × 1/16),且不超过窗口的一半
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| 窗口 | 动态输出上限 |
|
|
190
|
+
|------|-------------|
|
|
191
|
+
| 65536 (64K) | 4096 |
|
|
192
|
+
| 131072 (128K) | **8192** |
|
|
193
|
+
| 200000 | 12500 |
|
|
194
|
+
|
|
195
|
+
这样窗口越大,单次输出空间越大,避免小模型 Write 大文件时被截断;同时输出**绝不超过窗口一半**,保证输入有足够空间、永不超窗。
|
|
196
|
+
|
|
197
|
+
> 可用 `--max-output-tokens N` 或 `config.maxOutputTokens` 覆盖(作为输出下限)。
|
|
198
|
+
|
|
180
199
|
### 🤖 小模型适配模式(--small-model)
|
|
181
200
|
|
|
182
201
|
> 专为 **本地小模型**(如 27B Q3 量化)设计,让编程工具在弱模型下也能可靠工作。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.16",
|
|
4
4
|
"description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming, rich media upload, multi-account management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/core/index.js",
|
|
@@ -189,3 +189,40 @@ test('多步计划:普通任务走简单意图引导', () => {
|
|
|
189
189
|
assert.ok(g, '应生成引导')
|
|
190
190
|
assert.ok(g.includes('任务引导'), '应走简单意图引导')
|
|
191
191
|
})
|
|
192
|
+
|
|
193
|
+
// ---- 动态 max_tokens 计算 ----
|
|
194
|
+
test('max_tokens:根据窗口大小动态计算', async () => {
|
|
195
|
+
const { QueryEngine } = await import('../core/query-engine.js')
|
|
196
|
+
const { TokenBudget } = await import('../core/token-budget.js')
|
|
197
|
+
// 131072 窗口 → 8192(窗口的 1/16)
|
|
198
|
+
const qe1 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 131072 }) })
|
|
199
|
+
assert.equal(qe1._computeMaxOutputTokens(), 8192)
|
|
200
|
+
// 65536 → 4096
|
|
201
|
+
const qe2 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 65536 }) })
|
|
202
|
+
assert.equal(qe2._computeMaxOutputTokens(), 4096)
|
|
203
|
+
// 无窗口 → 兜底 4096
|
|
204
|
+
const qe3 = new QueryEngine({})
|
|
205
|
+
assert.equal(qe3._computeMaxOutputTokens(), 4096)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('max_tokens:可配置输出比例与下限覆盖', async () => {
|
|
209
|
+
const { QueryEngine } = await import('../core/query-engine.js')
|
|
210
|
+
const { TokenBudget } = await import('../core/token-budget.js')
|
|
211
|
+
// outputRatio=0.1 → 131072*0.1=13107
|
|
212
|
+
const qe1 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 131072 }), outputRatio: 0.1 })
|
|
213
|
+
assert.equal(qe1._computeMaxOutputTokens(), 13107)
|
|
214
|
+
// maxOutputTokens=8192 作为下限
|
|
215
|
+
const qe2 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 131072 }), maxOutputTokens: 8192 })
|
|
216
|
+
assert.equal(qe2._computeMaxOutputTokens(), 8192)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('max_tokens:绝不超过窗口一半(防超窗)', async () => {
|
|
220
|
+
const { QueryEngine } = await import('../core/query-engine.js')
|
|
221
|
+
const { TokenBudget } = await import('../core/token-budget.js')
|
|
222
|
+
// 小窗口 8192:窗口一半 = 4096,1/16 = 512,取 max(4096,512)=4096 ≤ 4096
|
|
223
|
+
const qe1 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 8192 }) })
|
|
224
|
+
assert.ok(qe1._computeMaxOutputTokens() <= 4096, `max_tokens=${qe1._computeMaxOutputTokens()} 不应超过窗口一半 4096`)
|
|
225
|
+
// 极端 outputRatio=1.0 也不应超过窗口一半
|
|
226
|
+
const qe2 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 10000 }), outputRatio: 1.0 })
|
|
227
|
+
assert.ok(qe2._computeMaxOutputTokens() <= 5000, `max_tokens=${qe2._computeMaxOutputTokens()} 不应超过窗口一半 5000`)
|
|
228
|
+
})
|
package/src/core/cli.js
CHANGED
|
@@ -332,6 +332,7 @@ function parseArgs(argv) {
|
|
|
332
332
|
noStream: false,
|
|
333
333
|
maxMessages: 0,
|
|
334
334
|
smallModel: false,
|
|
335
|
+
maxOutputTokens: 0,
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
let i = 2
|
|
@@ -349,6 +350,7 @@ function parseArgs(argv) {
|
|
|
349
350
|
case '--no-stream': args.noStream = true; break
|
|
350
351
|
case '--max-messages': args.maxMessages = parseInt(argv[++i], 10); break
|
|
351
352
|
case '--small-model': args.smallModel = true; break
|
|
353
|
+
case '--max-output-tokens': args.maxOutputTokens = parseInt(argv[++i], 10); break
|
|
352
354
|
case '--stdio': args.stdio = true; break
|
|
353
355
|
case '--with-notify': args.withNotify = true; break
|
|
354
356
|
case '--version':
|
|
@@ -371,6 +373,7 @@ Options:
|
|
|
371
373
|
--no-stream Disable streaming
|
|
372
374
|
--max-messages N Fold history when message count exceeds N (default: 0 = off)
|
|
373
375
|
--small-model Enable small-model adaptation (tool-call enforcement, filler retry, intent guidance)
|
|
376
|
+
--max-output-tokens N Override max single-response output tokens (default: computed from window size)
|
|
374
377
|
--with-notify Start built-in channel listener (Telegram)
|
|
375
378
|
(replaces cc-notify daemon — no external script needed)
|
|
376
379
|
-h, --help Show this help
|
|
@@ -535,6 +538,8 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
|
|
|
535
538
|
maxMessages: cliArgs.maxMessages || config.get('maxMessages') || 0,
|
|
536
539
|
// 小模型适配模式(强制工具调用 + 敷衍重试 + 意图引导 + 工具精简)
|
|
537
540
|
smallModel: cliArgs.smallModel || config.get('smallModel') || false,
|
|
541
|
+
// 单次输出上限覆盖(默认根据窗口动态计算)
|
|
542
|
+
maxOutputTokens: cliArgs.maxOutputTokens || config.get('maxOutputTokens') || 0,
|
|
538
543
|
})
|
|
539
544
|
const engine = new QueryEngine(engineConfig)
|
|
540
545
|
|
package/src/core/query-engine.js
CHANGED
|
@@ -53,6 +53,11 @@ export class QueryEngineConfig {
|
|
|
53
53
|
// - 工具数量精简 + 意图引导
|
|
54
54
|
// 默认关闭,通过 config.smallModel=true 或 --small-model 开启
|
|
55
55
|
this.smallModel = options.smallModel || false
|
|
56
|
+
// 单次输出上限动态计算:
|
|
57
|
+
// outputRatio — 输出占窗口比例(默认 1/16),窗口越大输出越大
|
|
58
|
+
// maxOutputTokens — 输出下限(默认 4096),也可作为硬性覆盖
|
|
59
|
+
this.outputRatio = options.outputRatio != null ? options.outputRatio : 1 / 16
|
|
60
|
+
this.maxOutputTokens = options.maxOutputTokens || 0
|
|
56
61
|
this.permissionMode = options.permissionMode || 'ask'
|
|
57
62
|
this.verbose = options.verbose || false
|
|
58
63
|
// API 配置 — 通用 OpenAI 兼容协议
|
|
@@ -312,6 +317,32 @@ export class QueryEngine {
|
|
|
312
317
|
}
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 动态计算单次输出上限 max_tokens
|
|
322
|
+
*
|
|
323
|
+
* 不再写死 4096,而是根据当前上下文窗口(/window 设置、tokenBudget.maxTokens)
|
|
324
|
+
* 按比例动态得出——窗口越大,允许的单次输出越大,避免小模型写大文件时被截断。
|
|
325
|
+
*
|
|
326
|
+
* 公式:max_tokens = max(基础下限, 窗口 × 输出比例)
|
|
327
|
+
* - 输出比例默认 1/16(6.25%):131072 窗口 → 8192;65536 → 4096
|
|
328
|
+
* - 下限 4096:窗口很小时也有足够输出空间
|
|
329
|
+
* - 上限不超过窗口的一半(留足输入空间,绝不超窗)
|
|
330
|
+
*
|
|
331
|
+
* @returns {number}
|
|
332
|
+
*/
|
|
333
|
+
_computeMaxOutputTokens() {
|
|
334
|
+
const windowSize = this.tokenBudget?.maxTokens
|
|
335
|
+
// 可配置:默认输出比例 1/16,下限 4096
|
|
336
|
+
const ratio = this.config.outputRatio != null ? this.config.outputRatio : 1 / 16
|
|
337
|
+
const min = this.config.maxOutputTokens || 4096
|
|
338
|
+
if (!windowSize || !Number.isFinite(windowSize) || windowSize <= 0) return min
|
|
339
|
+
const byWindow = Math.floor(windowSize * ratio)
|
|
340
|
+
// 上限 = 窗口一半,保证输入有足够空间,绝不超窗
|
|
341
|
+
const cap = Math.floor(windowSize / 2)
|
|
342
|
+
const maxOut = Math.min(cap, Math.max(min, byWindow))
|
|
343
|
+
return maxOut
|
|
344
|
+
}
|
|
345
|
+
|
|
315
346
|
/**
|
|
316
347
|
* 构建 LLM 请求消息列表 — 统一 OpenAI 兼容格式
|
|
317
348
|
*/
|
|
@@ -475,7 +506,8 @@ export class QueryEngine {
|
|
|
475
506
|
const body = {
|
|
476
507
|
model: this.config.model,
|
|
477
508
|
messages,
|
|
478
|
-
|
|
509
|
+
// 根据当前上下文窗口动态计算输出上限(不再写死 4096)
|
|
510
|
+
max_tokens: this._computeMaxOutputTokens(),
|
|
479
511
|
...(tools.length && { tools }),
|
|
480
512
|
...(useStream && { stream: true }),
|
|
481
513
|
}
|