@kaitranntt/ccs 3.3.0 → 3.4.1
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 +66 -7
- package/VERSION +1 -1
- package/bin/{auth-commands.js → auth/auth-commands.js} +3 -3
- package/bin/ccs.js +38 -19
- package/bin/glmt/budget-calculator.js +114 -0
- package/bin/glmt/delta-accumulator.js +261 -0
- package/bin/glmt/glmt-proxy.js +488 -0
- package/bin/glmt/glmt-transformer.js +919 -0
- package/bin/glmt/locale-enforcer.js +80 -0
- package/bin/glmt/sse-parser.js +96 -0
- package/bin/glmt/task-classifier.js +162 -0
- package/bin/{doctor.js → management/doctor.js} +2 -2
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +1 -1
- package/scripts/dev-install.sh +35 -0
- package/bin/glmt-proxy.js +0 -307
- package/bin/glmt-transformer.js +0 -437
- /package/bin/{profile-detector.js → auth/profile-detector.js} +0 -0
- /package/bin/{profile-registry.js → auth/profile-registry.js} +0 -0
- /package/bin/{instance-manager.js → management/instance-manager.js} +0 -0
- /package/bin/{recovery-manager.js → management/recovery-manager.js} +0 -0
- /package/bin/{shared-manager.js → management/shared-manager.js} +0 -0
- /package/bin/{claude-detector.js → utils/claude-detector.js} +0 -0
- /package/bin/{config-manager.js → utils/config-manager.js} +0 -0
- /package/bin/{error-manager.js → utils/error-manager.js} +0 -0
- /package/bin/{helpers.js → utils/helpers.js} +0 -0
package/README.md
CHANGED
|
@@ -205,21 +205,65 @@ Commands and skills symlinked from `~/.ccs/shared/` - no duplication across prof
|
|
|
205
205
|
|---------|-----------------|-------------------|
|
|
206
206
|
| **Endpoint** | Anthropic-compatible | OpenAI-compatible |
|
|
207
207
|
| **Thinking** | No | Yes (reasoning_content) |
|
|
208
|
-
| **
|
|
209
|
-
| **
|
|
208
|
+
| **Tool Support** | Basic | **Full (v3.5+)** |
|
|
209
|
+
| **MCP Tools** | Limited | **Working (v3.5+)** |
|
|
210
|
+
| **Streaming** | Yes | **Yes (v3.4+)** |
|
|
211
|
+
| **TTFB** | <500ms | <500ms (streaming), 2-10s (buffered) |
|
|
212
|
+
| **Use Case** | Fast responses | Complex reasoning + tools |
|
|
213
|
+
|
|
214
|
+
### Tool Support (v3.5)
|
|
215
|
+
|
|
216
|
+
**GLMT now fully supports MCP tools and function calling**:
|
|
217
|
+
|
|
218
|
+
- **Bidirectional Transformation**: Anthropic tools ↔ OpenAI function calling
|
|
219
|
+
- **MCP Integration**: MCP tools execute correctly (no XML tag output)
|
|
220
|
+
- **Streaming Tool Calls**: Real-time tool calls with input_json deltas
|
|
221
|
+
- **Backward Compatible**: Works seamlessly with existing thinking support
|
|
222
|
+
- **No Configuration**: Tool support works automatically
|
|
223
|
+
|
|
224
|
+
### Streaming Support (v3.4)
|
|
225
|
+
|
|
226
|
+
**GLMT now supports real-time streaming** with incremental reasoning content delivery.
|
|
227
|
+
|
|
228
|
+
- **Default**: Streaming enabled (TTFB <500ms)
|
|
229
|
+
- **Disable**: Set `CCS_GLMT_STREAMING=disabled` for buffered mode
|
|
230
|
+
- **Force**: Set `CCS_GLMT_STREAMING=force` to override client preferences
|
|
231
|
+
- **Thinking parameter**: Claude CLI `thinking` parameter support
|
|
232
|
+
- Respects `thinking.type` and `budget_tokens`
|
|
233
|
+
- Precedence: CLI parameter > message tags > default
|
|
234
|
+
|
|
235
|
+
**Confirmed working**: Z.AI (1498 reasoning chunks tested, tool calls verified)
|
|
210
236
|
|
|
211
237
|
### How It Works
|
|
212
238
|
|
|
213
239
|
1. CCS spawns embedded HTTP proxy on localhost
|
|
214
|
-
2. Proxy converts Anthropic format → OpenAI format
|
|
215
|
-
3.
|
|
216
|
-
4.
|
|
217
|
-
5.
|
|
240
|
+
2. Proxy converts Anthropic format → OpenAI format (streaming or buffered)
|
|
241
|
+
3. Transforms Anthropic tools → OpenAI function calling format
|
|
242
|
+
4. Forwards to Z.AI with reasoning parameters and tools
|
|
243
|
+
5. Converts `reasoning_content` → thinking blocks (incremental or complete)
|
|
244
|
+
6. Converts OpenAI `tool_calls` → Anthropic tool_use blocks
|
|
245
|
+
7. Thinking and tool calls appear in Claude Code UI in real-time
|
|
218
246
|
|
|
219
247
|
### Control Tags
|
|
220
248
|
|
|
221
249
|
- `<Thinking:On|Off>` - Enable/disable reasoning blocks (default: On)
|
|
222
|
-
- `<Effort:Low|Medium|High>` - Control reasoning depth (
|
|
250
|
+
- `<Effort:Low|Medium|High>` - Control reasoning depth (deprecated - Z.AI only supports binary thinking)
|
|
251
|
+
|
|
252
|
+
### Environment Variables
|
|
253
|
+
|
|
254
|
+
**GLMT-specific**:
|
|
255
|
+
- `CCS_GLMT_FORCE_ENGLISH=true` - Force English output (default: true)
|
|
256
|
+
- `CCS_GLMT_THINKING_BUDGET=8192` - Control thinking on/off based on task type
|
|
257
|
+
- 0 or "unlimited": Always enable thinking
|
|
258
|
+
- 1-2048: Disable thinking (fast execution)
|
|
259
|
+
- 2049-8192: Enable for reasoning tasks only (default)
|
|
260
|
+
- >8192: Always enable thinking
|
|
261
|
+
- `CCS_GLMT_STREAMING=disabled` - Force buffered mode
|
|
262
|
+
- `CCS_GLMT_STREAMING=force` - Force streaming (override client)
|
|
263
|
+
|
|
264
|
+
**General**:
|
|
265
|
+
- `CCS_DEBUG_LOG=1` - Enable debug file logging
|
|
266
|
+
- `CCS_CLAUDE_PATH=/path/to/claude` - Custom Claude CLI path
|
|
223
267
|
|
|
224
268
|
### API Key Setup
|
|
225
269
|
|
|
@@ -235,6 +279,14 @@ nano ~/.ccs/glmt.settings.json
|
|
|
235
279
|
}
|
|
236
280
|
```
|
|
237
281
|
|
|
282
|
+
### Security Limits
|
|
283
|
+
|
|
284
|
+
**DoS protection** (v3.4):
|
|
285
|
+
- SSE buffer: 1MB max per event
|
|
286
|
+
- Content buffer: 10MB max per block (thinking/text)
|
|
287
|
+
- Content blocks: 100 max per message
|
|
288
|
+
- Request timeout: 120s (both streaming and buffered)
|
|
289
|
+
|
|
238
290
|
### Debugging
|
|
239
291
|
|
|
240
292
|
**Enable verbose logging**:
|
|
@@ -249,6 +301,12 @@ ccs glmt --verbose "your prompt"
|
|
|
249
301
|
# Logs: ~/.ccs/logs/
|
|
250
302
|
```
|
|
251
303
|
|
|
304
|
+
**Check streaming mode**:
|
|
305
|
+
```bash
|
|
306
|
+
# Disable streaming for debugging
|
|
307
|
+
CCS_GLMT_STREAMING=disabled ccs glmt "test"
|
|
308
|
+
```
|
|
309
|
+
|
|
252
310
|
**Check reasoning content**:
|
|
253
311
|
```bash
|
|
254
312
|
cat ~/.ccs/logs/*response-openai.json | jq '.choices[0].message.reasoning_content'
|
|
@@ -351,6 +409,7 @@ irm ccs.kaitran.ca/uninstall | iex
|
|
|
351
409
|
- [Configuration](./docs/en/configuration.md)
|
|
352
410
|
- [Usage Examples](./docs/en/usage.md)
|
|
353
411
|
- [System Architecture](./docs/system-architecture.md)
|
|
412
|
+
- [GLMT Control Mechanisms](./docs/glmt-controls.md)
|
|
354
413
|
- [Troubleshooting](./docs/en/troubleshooting.md)
|
|
355
414
|
- [Contributing](./CONTRIBUTING.md)
|
|
356
415
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.1
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
4
|
const ProfileRegistry = require('./profile-registry');
|
|
5
|
-
const InstanceManager = require('
|
|
6
|
-
const { colored } = require('
|
|
7
|
-
const { detectClaudeCli } = require('
|
|
5
|
+
const InstanceManager = require('../management/instance-manager');
|
|
6
|
+
const { colored } = require('../utils/helpers');
|
|
7
|
+
const { detectClaudeCli } = require('../utils/claude-detector');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Auth Commands (Simplified)
|
package/bin/ccs.js
CHANGED
|
@@ -5,11 +5,11 @@ const { spawn } = require('child_process');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const { error, colored } = require('./helpers');
|
|
9
|
-
const { detectClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
|
|
10
|
-
const { getSettingsPath, getConfigPath } = require('./config-manager');
|
|
11
|
-
const { ErrorManager } = require('./error-manager');
|
|
12
|
-
const RecoveryManager = require('./recovery-manager');
|
|
8
|
+
const { error, colored } = require('./utils/helpers');
|
|
9
|
+
const { detectClaudeCli, showClaudeNotFoundError } = require('./utils/claude-detector');
|
|
10
|
+
const { getSettingsPath, getConfigPath } = require('./utils/config-manager');
|
|
11
|
+
const { ErrorManager } = require('./utils/error-manager');
|
|
12
|
+
const RecoveryManager = require('./management/recovery-manager');
|
|
13
13
|
|
|
14
14
|
// Version (sync with package.json)
|
|
15
15
|
const CCS_VERSION = require('../package.json').version;
|
|
@@ -194,7 +194,7 @@ function handleUninstallCommand() {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
async function handleDoctorCommand() {
|
|
197
|
-
const Doctor = require('./doctor');
|
|
197
|
+
const Doctor = require('./management/doctor');
|
|
198
198
|
const doctor = new Doctor();
|
|
199
199
|
|
|
200
200
|
await doctor.runAllChecks();
|
|
@@ -216,7 +216,7 @@ function detectProfile(args) {
|
|
|
216
216
|
|
|
217
217
|
// Execute Claude CLI with embedded proxy (for GLMT profile)
|
|
218
218
|
async function execClaudeWithProxy(claudeCli, profileName, args) {
|
|
219
|
-
const { getSettingsPath } = require('./config-manager');
|
|
219
|
+
const { getSettingsPath } = require('./utils/config-manager');
|
|
220
220
|
|
|
221
221
|
// 1. Read settings to get API key
|
|
222
222
|
const settingsPath = getSettingsPath(profileName);
|
|
@@ -233,9 +233,10 @@ async function execClaudeWithProxy(claudeCli, profileName, args) {
|
|
|
233
233
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
234
234
|
|
|
235
235
|
// 2. Spawn embedded proxy with verbose flag
|
|
236
|
-
const proxyPath = path.join(__dirname, 'glmt-proxy.js');
|
|
236
|
+
const proxyPath = path.join(__dirname, 'glmt', 'glmt-proxy.js');
|
|
237
237
|
const proxyArgs = verbose ? ['--verbose'] : [];
|
|
238
|
-
|
|
238
|
+
// Use process.execPath for Windows compatibility (CVE-2024-27980)
|
|
239
|
+
const proxy = spawn(process.execPath, [proxyPath, ...proxyArgs], {
|
|
239
240
|
stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit']
|
|
240
241
|
});
|
|
241
242
|
|
|
@@ -286,16 +287,34 @@ async function execClaudeWithProxy(claudeCli, profileName, args) {
|
|
|
286
287
|
|
|
287
288
|
// 4. Spawn Claude CLI with proxy URL
|
|
288
289
|
const envVars = {
|
|
289
|
-
...process.env,
|
|
290
290
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
291
291
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
292
292
|
ANTHROPIC_MODEL: 'glm-4.6'
|
|
293
293
|
};
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
295
|
+
// Use existing execClaude helper for consistent Windows handling
|
|
296
|
+
const isWindows = process.platform === 'win32';
|
|
297
|
+
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
|
298
|
+
const env = { ...process.env, ...envVars };
|
|
299
|
+
|
|
300
|
+
let claude;
|
|
301
|
+
if (needsShell) {
|
|
302
|
+
// When shell needed: concatenate into string to avoid DEP0190 warning
|
|
303
|
+
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
|
304
|
+
claude = spawn(cmdString, {
|
|
305
|
+
stdio: 'inherit',
|
|
306
|
+
windowsHide: true,
|
|
307
|
+
shell: true,
|
|
308
|
+
env
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
// When no shell needed: use array form (faster, no shell overhead)
|
|
312
|
+
claude = spawn(claudeCli, args, {
|
|
313
|
+
stdio: 'inherit',
|
|
314
|
+
windowsHide: true,
|
|
315
|
+
env
|
|
316
|
+
});
|
|
317
|
+
}
|
|
299
318
|
|
|
300
319
|
// 5. Cleanup: kill proxy when Claude exits
|
|
301
320
|
claude.on('exit', (code, signal) => {
|
|
@@ -358,7 +377,7 @@ async function main() {
|
|
|
358
377
|
|
|
359
378
|
// Special case: auth command (multi-account management)
|
|
360
379
|
if (firstArg === 'auth') {
|
|
361
|
-
const AuthCommands = require('./auth-commands');
|
|
380
|
+
const AuthCommands = require('./auth/auth-commands');
|
|
362
381
|
const authCommands = new AuthCommands();
|
|
363
382
|
await authCommands.route(args.slice(1));
|
|
364
383
|
return;
|
|
@@ -383,10 +402,10 @@ async function main() {
|
|
|
383
402
|
}
|
|
384
403
|
|
|
385
404
|
// Use ProfileDetector to determine profile type
|
|
386
|
-
const ProfileDetector = require('./profile-detector');
|
|
387
|
-
const InstanceManager = require('./instance-manager');
|
|
388
|
-
const ProfileRegistry = require('./profile-registry');
|
|
389
|
-
const { getSettingsPath } = require('./config-manager');
|
|
405
|
+
const ProfileDetector = require('./auth/profile-detector');
|
|
406
|
+
const InstanceManager = require('./management/instance-manager');
|
|
407
|
+
const ProfileRegistry = require('./auth/profile-registry');
|
|
408
|
+
const { getSettingsPath } = require('./utils/config-manager');
|
|
390
409
|
|
|
391
410
|
const detector = new ProfileDetector();
|
|
392
411
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BudgetCalculator - Control thinking enable/disable based on task complexity
|
|
6
|
+
*
|
|
7
|
+
* Purpose: Z.AI API only supports binary thinking (on/off), not reasoning_effort levels.
|
|
8
|
+
* This module decides when to enable thinking based on task type and budget preferences.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const calculator = new BudgetCalculator();
|
|
12
|
+
* const shouldThink = calculator.shouldEnableThinking(taskType, envBudget);
|
|
13
|
+
*
|
|
14
|
+
* Configuration:
|
|
15
|
+
* CCS_GLMT_THINKING_BUDGET:
|
|
16
|
+
* - 0 or "unlimited": Always enable thinking (power user mode)
|
|
17
|
+
* - 1-2048: Disable thinking (fast execution, low budget)
|
|
18
|
+
* - 2049-8192: Enable thinking for reasoning tasks only (default)
|
|
19
|
+
* - >8192: Always enable thinking (high budget)
|
|
20
|
+
*
|
|
21
|
+
* Task type mapping:
|
|
22
|
+
* - reasoning: Enable thinking (planning, design, analysis)
|
|
23
|
+
* - execution: Disable thinking (fix, implement, debug) unless high budget
|
|
24
|
+
* - mixed: Enable thinking if budget >= medium threshold
|
|
25
|
+
*/
|
|
26
|
+
class BudgetCalculator {
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.budgetThresholds = {
|
|
29
|
+
low: 2048, // Disable thinking (fast execution)
|
|
30
|
+
medium: 8192 // Enable thinking for reasoning tasks
|
|
31
|
+
};
|
|
32
|
+
this.defaultBudget = options.defaultBudget || 8192; // Default: enable thinking for reasoning
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Determine if thinking should be enabled based on task type and budget
|
|
37
|
+
* @param {string} taskType - 'reasoning', 'execution', or 'mixed'
|
|
38
|
+
* @param {string|number} envBudget - CCS_GLMT_THINKING_BUDGET value
|
|
39
|
+
* @returns {boolean} True if thinking should be enabled
|
|
40
|
+
*/
|
|
41
|
+
shouldEnableThinking(taskType, envBudget) {
|
|
42
|
+
const budget = this._parseBudget(envBudget);
|
|
43
|
+
|
|
44
|
+
// Unlimited budget (0): Always enable thinking
|
|
45
|
+
if (budget === 0) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Low budget (<= 2048): Disable thinking (fast execution mode)
|
|
50
|
+
if (budget <= this.budgetThresholds.low) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// High budget (> 8192): Always enable thinking
|
|
55
|
+
if (budget > this.budgetThresholds.medium) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Medium budget (2049-8192): Task-aware decision
|
|
60
|
+
if (taskType === 'reasoning') {
|
|
61
|
+
return true; // Enable thinking for planning/design tasks
|
|
62
|
+
} else if (taskType === 'execution') {
|
|
63
|
+
return false; // Disable thinking for quick fixes
|
|
64
|
+
} else {
|
|
65
|
+
return true; // Enable for mixed/ambiguous tasks (default safe)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse budget from environment variable or use default
|
|
71
|
+
* @param {string|number} envBudget - Budget value
|
|
72
|
+
* @returns {number} Parsed budget (0 = unlimited)
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
_parseBudget(envBudget) {
|
|
76
|
+
// CRITICAL: Check for undefined/null explicitly, not falsy (0 is valid!)
|
|
77
|
+
if (envBudget === undefined || envBudget === null || envBudget === '') {
|
|
78
|
+
return this.defaultBudget;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle string values
|
|
82
|
+
if (typeof envBudget === 'string') {
|
|
83
|
+
if (envBudget.toLowerCase() === 'unlimited') {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const parsed = parseInt(envBudget, 10);
|
|
87
|
+
if (isNaN(parsed)) {
|
|
88
|
+
return this.defaultBudget;
|
|
89
|
+
}
|
|
90
|
+
return parsed < 0 ? 0 : parsed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle number values
|
|
94
|
+
if (typeof envBudget === 'number') {
|
|
95
|
+
return envBudget < 0 ? 0 : envBudget;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.defaultBudget;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get human-readable budget description
|
|
103
|
+
* @param {number} budget - Budget value
|
|
104
|
+
* @returns {string} Description
|
|
105
|
+
*/
|
|
106
|
+
getBudgetDescription(budget) {
|
|
107
|
+
if (budget === 0) return 'unlimited (always think)';
|
|
108
|
+
if (budget <= this.budgetThresholds.low) return 'low (fast execution, no thinking)';
|
|
109
|
+
if (budget <= this.budgetThresholds.medium) return 'medium (task-aware thinking)';
|
|
110
|
+
return 'high (always think)';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = BudgetCalculator;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DeltaAccumulator - Maintain state across streaming deltas
|
|
6
|
+
*
|
|
7
|
+
* Tracks:
|
|
8
|
+
* - Message metadata (id, model, role)
|
|
9
|
+
* - Content blocks (thinking, text)
|
|
10
|
+
* - Current block index
|
|
11
|
+
* - Accumulated content
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const acc = new DeltaAccumulator(thinkingConfig);
|
|
15
|
+
* const events = transformer.transformDelta(openaiEvent, acc);
|
|
16
|
+
*/
|
|
17
|
+
class DeltaAccumulator {
|
|
18
|
+
constructor(thinkingConfig = {}, options = {}) {
|
|
19
|
+
this.thinkingConfig = thinkingConfig;
|
|
20
|
+
this.messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substring(7);
|
|
21
|
+
this.model = null;
|
|
22
|
+
this.role = 'assistant';
|
|
23
|
+
|
|
24
|
+
// Content blocks
|
|
25
|
+
this.contentBlocks = [];
|
|
26
|
+
this.currentBlockIndex = -1;
|
|
27
|
+
|
|
28
|
+
// Tool calls tracking
|
|
29
|
+
this.toolCalls = [];
|
|
30
|
+
this.toolCallsIndex = {};
|
|
31
|
+
|
|
32
|
+
// Buffers
|
|
33
|
+
this.thinkingBuffer = '';
|
|
34
|
+
this.textBuffer = '';
|
|
35
|
+
|
|
36
|
+
// C-02 Fix: Limits to prevent unbounded accumulation
|
|
37
|
+
this.maxBlocks = options.maxBlocks || 100;
|
|
38
|
+
this.maxBufferSize = options.maxBufferSize || 10 * 1024 * 1024; // 10MB
|
|
39
|
+
|
|
40
|
+
// Loop detection configuration
|
|
41
|
+
this.loopDetectionThreshold = options.loopDetectionThreshold || 3;
|
|
42
|
+
this.loopDetected = false;
|
|
43
|
+
|
|
44
|
+
// State flags
|
|
45
|
+
this.messageStarted = false;
|
|
46
|
+
this.finalized = false;
|
|
47
|
+
this.usageReceived = false; // Track if usage data has arrived
|
|
48
|
+
|
|
49
|
+
// Statistics
|
|
50
|
+
this.inputTokens = 0;
|
|
51
|
+
this.outputTokens = 0;
|
|
52
|
+
this.finishReason = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get current content block
|
|
57
|
+
* @returns {Object|null} Current block or null
|
|
58
|
+
*/
|
|
59
|
+
getCurrentBlock() {
|
|
60
|
+
if (this.currentBlockIndex >= 0 && this.currentBlockIndex < this.contentBlocks.length) {
|
|
61
|
+
return this.contentBlocks[this.currentBlockIndex];
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Start new content block
|
|
68
|
+
* @param {string} type - Block type ('thinking', 'text', or 'tool_use')
|
|
69
|
+
* @returns {Object} New block
|
|
70
|
+
*/
|
|
71
|
+
startBlock(type) {
|
|
72
|
+
// C-02 Fix: Enforce max blocks limit
|
|
73
|
+
if (this.contentBlocks.length >= this.maxBlocks) {
|
|
74
|
+
throw new Error(`Maximum ${this.maxBlocks} content blocks exceeded (DoS protection)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.currentBlockIndex++;
|
|
78
|
+
const block = {
|
|
79
|
+
index: this.currentBlockIndex,
|
|
80
|
+
type: type,
|
|
81
|
+
content: '',
|
|
82
|
+
started: true,
|
|
83
|
+
stopped: false
|
|
84
|
+
};
|
|
85
|
+
this.contentBlocks.push(block);
|
|
86
|
+
|
|
87
|
+
// Reset buffer for new block (tool_use doesn't use buffers)
|
|
88
|
+
if (type === 'thinking') {
|
|
89
|
+
this.thinkingBuffer = '';
|
|
90
|
+
} else if (type === 'text') {
|
|
91
|
+
this.textBuffer = '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return block;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add delta to current block
|
|
99
|
+
* @param {string} delta - Content delta
|
|
100
|
+
*/
|
|
101
|
+
addDelta(delta) {
|
|
102
|
+
const block = this.getCurrentBlock();
|
|
103
|
+
if (block) {
|
|
104
|
+
if (block.type === 'thinking') {
|
|
105
|
+
// C-02 Fix: Enforce buffer size limit
|
|
106
|
+
if (this.thinkingBuffer.length + delta.length > this.maxBufferSize) {
|
|
107
|
+
throw new Error(`Thinking buffer exceeded ${this.maxBufferSize} bytes (DoS protection)`);
|
|
108
|
+
}
|
|
109
|
+
this.thinkingBuffer += delta;
|
|
110
|
+
block.content = this.thinkingBuffer;
|
|
111
|
+
} else if (block.type === 'text') {
|
|
112
|
+
// C-02 Fix: Enforce buffer size limit
|
|
113
|
+
if (this.textBuffer.length + delta.length > this.maxBufferSize) {
|
|
114
|
+
throw new Error(`Text buffer exceeded ${this.maxBufferSize} bytes (DoS protection)`);
|
|
115
|
+
}
|
|
116
|
+
this.textBuffer += delta;
|
|
117
|
+
block.content = this.textBuffer;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Mark current block as stopped
|
|
124
|
+
*/
|
|
125
|
+
stopCurrentBlock() {
|
|
126
|
+
const block = this.getCurrentBlock();
|
|
127
|
+
if (block) {
|
|
128
|
+
block.stopped = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Update usage statistics
|
|
134
|
+
* @param {Object} usage - Usage object from OpenAI
|
|
135
|
+
*/
|
|
136
|
+
updateUsage(usage) {
|
|
137
|
+
if (usage) {
|
|
138
|
+
this.inputTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
|
139
|
+
this.outputTokens = usage.completion_tokens || usage.output_tokens || 0;
|
|
140
|
+
this.usageReceived = true; // Mark that we've received usage data
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Add or update tool call delta
|
|
146
|
+
* @param {Object} toolCallDelta - Tool call delta from OpenAI
|
|
147
|
+
*/
|
|
148
|
+
addToolCallDelta(toolCallDelta) {
|
|
149
|
+
const index = toolCallDelta.index;
|
|
150
|
+
|
|
151
|
+
// Initialize tool call if not exists
|
|
152
|
+
if (!this.toolCallsIndex[index]) {
|
|
153
|
+
const toolCall = {
|
|
154
|
+
index: index,
|
|
155
|
+
id: '',
|
|
156
|
+
type: 'function',
|
|
157
|
+
function: {
|
|
158
|
+
name: '',
|
|
159
|
+
arguments: ''
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
this.toolCalls.push(toolCall);
|
|
163
|
+
this.toolCallsIndex[index] = toolCall;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const toolCall = this.toolCallsIndex[index];
|
|
167
|
+
|
|
168
|
+
// Update id if present
|
|
169
|
+
if (toolCallDelta.id) {
|
|
170
|
+
toolCall.id = toolCallDelta.id;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Update type if present
|
|
174
|
+
if (toolCallDelta.type) {
|
|
175
|
+
toolCall.type = toolCallDelta.type;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Update function name if present
|
|
179
|
+
if (toolCallDelta.function?.name) {
|
|
180
|
+
toolCall.function.name += toolCallDelta.function.name;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update function arguments if present
|
|
184
|
+
if (toolCallDelta.function?.arguments) {
|
|
185
|
+
toolCall.function.arguments += toolCallDelta.function.arguments;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get all tool calls
|
|
191
|
+
* @returns {Array} Tool calls array
|
|
192
|
+
*/
|
|
193
|
+
getToolCalls() {
|
|
194
|
+
return this.toolCalls;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check for planning loop pattern
|
|
199
|
+
* Loop = N consecutive thinking blocks with no tool calls
|
|
200
|
+
* @returns {boolean} True if loop detected
|
|
201
|
+
*/
|
|
202
|
+
checkForLoop() {
|
|
203
|
+
// Already detected loop
|
|
204
|
+
if (this.loopDetected) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Need minimum blocks to detect pattern
|
|
209
|
+
if (this.contentBlocks.length < this.loopDetectionThreshold) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Get last N blocks
|
|
214
|
+
const recentBlocks = this.contentBlocks.slice(-this.loopDetectionThreshold);
|
|
215
|
+
|
|
216
|
+
// Check if all recent blocks are thinking blocks
|
|
217
|
+
const allThinking = recentBlocks.every(b => b.type === 'thinking');
|
|
218
|
+
|
|
219
|
+
// Check if no tool calls have been made at all
|
|
220
|
+
const noToolCalls = this.toolCalls.length === 0;
|
|
221
|
+
|
|
222
|
+
// Loop detected if: all recent blocks are thinking AND no tool calls yet
|
|
223
|
+
if (allThinking && noToolCalls) {
|
|
224
|
+
this.loopDetected = true;
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reset loop detection state (for testing)
|
|
233
|
+
*/
|
|
234
|
+
resetLoopDetection() {
|
|
235
|
+
this.loopDetected = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get summary of accumulated state
|
|
240
|
+
* @returns {Object} Summary
|
|
241
|
+
*/
|
|
242
|
+
getSummary() {
|
|
243
|
+
return {
|
|
244
|
+
messageId: this.messageId,
|
|
245
|
+
model: this.model,
|
|
246
|
+
role: this.role,
|
|
247
|
+
blockCount: this.contentBlocks.length,
|
|
248
|
+
currentIndex: this.currentBlockIndex,
|
|
249
|
+
toolCallCount: this.toolCalls.length,
|
|
250
|
+
messageStarted: this.messageStarted,
|
|
251
|
+
finalized: this.finalized,
|
|
252
|
+
loopDetected: this.loopDetected,
|
|
253
|
+
usage: {
|
|
254
|
+
input_tokens: this.inputTokens,
|
|
255
|
+
output_tokens: this.outputTokens
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = DeltaAccumulator;
|