@kaitranntt/ccs 3.4.2 → 3.4.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/VERSION +1 -1
- package/bin/glmt/glmt-proxy.js +16 -9
- package/bin/glmt/glmt-transformer.js +67 -28
- package/bin/glmt/locale-enforcer.js +2 -10
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/bin/glmt/budget-calculator.js +0 -114
- package/bin/glmt/task-classifier.js +0 -162
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.4.
|
|
1
|
+
3.4.4
|
package/bin/glmt/glmt-proxy.js
CHANGED
|
@@ -23,7 +23,7 @@ const DeltaAccumulator = require('./delta-accumulator');
|
|
|
23
23
|
*
|
|
24
24
|
* Debugging:
|
|
25
25
|
* - Verbose: Pass --verbose to see request/response logs
|
|
26
|
-
* - Debug: Set
|
|
26
|
+
* - Debug: Set CCS_DEBUG=1 to write logs to ~/.ccs/logs/
|
|
27
27
|
*
|
|
28
28
|
* Usage:
|
|
29
29
|
* const proxy = new GlmtProxy({ verbose: true });
|
|
@@ -33,7 +33,7 @@ class GlmtProxy {
|
|
|
33
33
|
constructor(config = {}) {
|
|
34
34
|
this.transformer = new GlmtTransformer({
|
|
35
35
|
verbose: config.verbose,
|
|
36
|
-
debugLog: config.debugLog || process.env.CCS_DEBUG_LOG === '1'
|
|
36
|
+
debugLog: config.debugLog || process.env.CCS_DEBUG === '1' || process.env.CCS_DEBUG_LOG === '1'
|
|
37
37
|
});
|
|
38
38
|
// Use ANTHROPIC_BASE_URL from environment (set by settings.json) or fallback to Z.AI default
|
|
39
39
|
this.upstreamUrl = process.env.ANTHROPIC_BASE_URL || 'https://api.z.ai/api/coding/paas/v4/chat/completions';
|
|
@@ -41,8 +41,6 @@ class GlmtProxy {
|
|
|
41
41
|
this.port = null;
|
|
42
42
|
this.verbose = config.verbose || false;
|
|
43
43
|
this.timeout = config.timeout || 120000; // 120s default
|
|
44
|
-
this.streamingEnabled = process.env.CCS_GLMT_STREAMING !== 'disabled';
|
|
45
|
-
this.forceStreaming = process.env.CCS_GLMT_STREAMING === 'force';
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/**
|
|
@@ -63,8 +61,7 @@ class GlmtProxy {
|
|
|
63
61
|
|
|
64
62
|
// Info message (only show in verbose mode)
|
|
65
63
|
if (this.verbose) {
|
|
66
|
-
|
|
67
|
-
console.error(`[glmt] Proxy listening on port ${this.port} (${mode})`);
|
|
64
|
+
console.error(`[glmt] Proxy listening on port ${this.port} (streaming with auto-fallback)`);
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
// Debug mode notice
|
|
@@ -127,11 +124,21 @@ class GlmtProxy {
|
|
|
127
124
|
this.log(`Request does NOT contain thinking parameter (will use message tags or default)`);
|
|
128
125
|
}
|
|
129
126
|
|
|
130
|
-
//
|
|
131
|
-
const useStreaming =
|
|
127
|
+
// Try streaming first (default), fallback to buffered on error
|
|
128
|
+
const useStreaming = anthropicRequest.stream !== false;
|
|
132
129
|
|
|
133
130
|
if (useStreaming) {
|
|
134
|
-
|
|
131
|
+
try {
|
|
132
|
+
await this._handleStreamingRequest(req, res, anthropicRequest, startTime);
|
|
133
|
+
} catch (streamError) {
|
|
134
|
+
this.log(`Streaming failed: ${streamError.message}, retrying buffered mode`);
|
|
135
|
+
try {
|
|
136
|
+
await this._handleBufferedRequest(req, res, anthropicRequest, startTime);
|
|
137
|
+
} catch (bufferedError) {
|
|
138
|
+
// Both modes failed, propagate error
|
|
139
|
+
throw bufferedError;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
135
142
|
} else {
|
|
136
143
|
await this._handleBufferedRequest(req, res, anthropicRequest, startTime);
|
|
137
144
|
}
|
|
@@ -8,8 +8,6 @@ const os = require('os');
|
|
|
8
8
|
const SSEParser = require('./sse-parser');
|
|
9
9
|
const DeltaAccumulator = require('./delta-accumulator');
|
|
10
10
|
const LocaleEnforcer = require('./locale-enforcer');
|
|
11
|
-
const BudgetCalculator = require('./budget-calculator');
|
|
12
|
-
const TaskClassifier = require('./task-classifier');
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* GlmtTransformer - Convert between Anthropic and OpenAI formats with thinking and tool support
|
|
@@ -33,10 +31,23 @@ const TaskClassifier = require('./task-classifier');
|
|
|
33
31
|
* <Effort:Low|Medium|High> - Control reasoning depth
|
|
34
32
|
*/
|
|
35
33
|
class GlmtTransformer {
|
|
34
|
+
static _warnedDeprecation = false;
|
|
35
|
+
|
|
36
36
|
constructor(config = {}) {
|
|
37
37
|
this.defaultThinking = config.defaultThinking ?? true;
|
|
38
38
|
this.verbose = config.verbose || false;
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
// Support both CCS_DEBUG and CCS_DEBUG_LOG (with deprecation warning)
|
|
41
|
+
const oldVar = process.env.CCS_DEBUG_LOG === '1';
|
|
42
|
+
const newVar = process.env.CCS_DEBUG === '1';
|
|
43
|
+
this.debugLog = config.debugLog ?? (newVar || oldVar);
|
|
44
|
+
|
|
45
|
+
// Show deprecation warning once
|
|
46
|
+
if (oldVar && !newVar && !GlmtTransformer._warnedDeprecation) {
|
|
47
|
+
console.warn('[glmt] Warning: CCS_DEBUG_LOG is deprecated, use CCS_DEBUG instead');
|
|
48
|
+
GlmtTransformer._warnedDeprecation = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
this.debugLogDir = config.debugLogDir || path.join(os.homedir(), '.ccs', 'logs');
|
|
41
52
|
this.modelMaxTokens = {
|
|
42
53
|
'GLM-4.6': 128000,
|
|
@@ -47,14 +58,8 @@ class GlmtTransformer {
|
|
|
47
58
|
this.EFFORT_LOW_THRESHOLD = 2048;
|
|
48
59
|
this.EFFORT_HIGH_THRESHOLD = 8192;
|
|
49
60
|
|
|
50
|
-
// Initialize locale enforcer
|
|
51
|
-
this.localeEnforcer = new LocaleEnforcer(
|
|
52
|
-
forceEnglish: process.env.CCS_GLMT_FORCE_ENGLISH !== 'false'
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Initialize budget calculator and task classifier
|
|
56
|
-
this.budgetCalculator = new BudgetCalculator();
|
|
57
|
-
this.taskClassifier = new TaskClassifier();
|
|
61
|
+
// Initialize locale enforcer (always enforce English)
|
|
62
|
+
this.localeEnforcer = new LocaleEnforcer();
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
@@ -73,25 +78,15 @@ class GlmtTransformer {
|
|
|
73
78
|
);
|
|
74
79
|
const hasControlTags = this._hasThinkingTags(anthropicRequest.messages || []);
|
|
75
80
|
|
|
76
|
-
// 2.
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const shouldThink = this.budgetCalculator.shouldEnableThinking(taskType, envBudget);
|
|
83
|
-
this.log(`Budget decision: thinking=${shouldThink} (budget: ${envBudget || 'default'}, type: ${taskType})`);
|
|
84
|
-
|
|
85
|
-
// Apply budget-based thinking control ONLY if:
|
|
86
|
-
// - No Claude CLI thinking parameter AND
|
|
87
|
-
// - No control tags in messages AND
|
|
88
|
-
// - Budget env var is explicitly set
|
|
89
|
-
if (!anthropicRequest.thinking && !hasControlTags && envBudget) {
|
|
90
|
-
thinkingConfig.thinking = shouldThink;
|
|
91
|
-
this.log('Applied budget-based thinking control');
|
|
81
|
+
// 2. Detect "think" keywords in user prompts (Anthropic-style)
|
|
82
|
+
const keywordConfig = this._detectThinkKeywords(anthropicRequest.messages || []);
|
|
83
|
+
if (keywordConfig && !anthropicRequest.thinking && !hasControlTags) {
|
|
84
|
+
thinkingConfig.thinking = keywordConfig.thinking;
|
|
85
|
+
thinkingConfig.effort = keywordConfig.effort;
|
|
86
|
+
this.log(`Detected think keyword: ${keywordConfig.keyword}, effort=${keywordConfig.effort}`);
|
|
92
87
|
}
|
|
93
88
|
|
|
94
|
-
//
|
|
89
|
+
// 3. Check anthropicRequest.thinking parameter (takes precedence)
|
|
95
90
|
// Claude CLI sends this when alwaysThinkingEnabled is configured
|
|
96
91
|
if (anthropicRequest.thinking) {
|
|
97
92
|
if (anthropicRequest.thinking.type === 'enabled') {
|
|
@@ -440,6 +435,50 @@ class GlmtTransformer {
|
|
|
440
435
|
};
|
|
441
436
|
}
|
|
442
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Detect Anthropic-style "think" keywords in user prompts
|
|
440
|
+
* Maps: "ultrathink" > "think harder" > "think hard" > "think"
|
|
441
|
+
* @param {Array} messages - Messages array
|
|
442
|
+
* @returns {Object|null} { thinking, effort, keyword } or null
|
|
443
|
+
* @private
|
|
444
|
+
*/
|
|
445
|
+
_detectThinkKeywords(messages) {
|
|
446
|
+
if (!messages || messages.length === 0) return null;
|
|
447
|
+
|
|
448
|
+
// Extract text from user messages
|
|
449
|
+
const text = messages
|
|
450
|
+
.filter(m => m.role === 'user')
|
|
451
|
+
.map(m => {
|
|
452
|
+
if (typeof m.content === 'string') return m.content;
|
|
453
|
+
if (Array.isArray(m.content)) {
|
|
454
|
+
return m.content
|
|
455
|
+
.filter(block => block.type === 'text')
|
|
456
|
+
.map(block => block.text || '')
|
|
457
|
+
.join(' ');
|
|
458
|
+
}
|
|
459
|
+
return '';
|
|
460
|
+
})
|
|
461
|
+
.join(' ');
|
|
462
|
+
|
|
463
|
+
// Priority: ultrathink > think harder > think hard > think
|
|
464
|
+
// Effort levels: max > high > medium > low (matches Anthropic's 4-tier system)
|
|
465
|
+
// Use word boundaries to avoid matching "thinking", "rethink", etc.
|
|
466
|
+
if (/\bultrathink\b/i.test(text)) {
|
|
467
|
+
return { thinking: true, effort: 'max', keyword: 'ultrathink' };
|
|
468
|
+
}
|
|
469
|
+
if (/\bthink\s+harder\b/i.test(text)) {
|
|
470
|
+
return { thinking: true, effort: 'high', keyword: 'think harder' };
|
|
471
|
+
}
|
|
472
|
+
if (/\bthink\s+hard\b/i.test(text)) {
|
|
473
|
+
return { thinking: true, effort: 'medium', keyword: 'think hard' };
|
|
474
|
+
}
|
|
475
|
+
if (/\bthink\b/i.test(text)) {
|
|
476
|
+
return { thinking: true, effort: 'low', keyword: 'think' };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return null; // No keywords detected
|
|
480
|
+
}
|
|
481
|
+
|
|
443
482
|
/**
|
|
444
483
|
* Inject reasoning parameters into OpenAI request
|
|
445
484
|
* @param {Object} openaiRequest - OpenAI request to modify
|
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
* LocaleEnforcer - Force English output from GLM models
|
|
6
6
|
*
|
|
7
7
|
* Purpose: GLM models default to Chinese when prompts are ambiguous or contain Chinese context.
|
|
8
|
-
* This module injects "MUST respond in English" instruction into system prompt or first user message.
|
|
8
|
+
* This module always injects "MUST respond in English" instruction into system prompt or first user message.
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
* const enforcer = new LocaleEnforcer(
|
|
11
|
+
* const enforcer = new LocaleEnforcer();
|
|
12
12
|
* const modifiedMessages = enforcer.injectInstruction(messages);
|
|
13
13
|
*
|
|
14
|
-
* Configuration:
|
|
15
|
-
* CCS_GLMT_FORCE_ENGLISH=false - Disable locale enforcement (allow multilingual)
|
|
16
|
-
*
|
|
17
14
|
* Strategy:
|
|
18
15
|
* 1. If system prompt exists: Prepend instruction
|
|
19
16
|
* 2. If no system prompt: Prepend to first user message
|
|
@@ -21,7 +18,6 @@
|
|
|
21
18
|
*/
|
|
22
19
|
class LocaleEnforcer {
|
|
23
20
|
constructor(options = {}) {
|
|
24
|
-
this.forceEnglish = options.forceEnglish ?? true;
|
|
25
21
|
this.instruction = "CRITICAL: You MUST respond in English only, regardless of the input language or context. This is a strict requirement.";
|
|
26
22
|
}
|
|
27
23
|
|
|
@@ -31,10 +27,6 @@ class LocaleEnforcer {
|
|
|
31
27
|
* @returns {Array} Modified messages array
|
|
32
28
|
*/
|
|
33
29
|
injectInstruction(messages) {
|
|
34
|
-
if (!this.forceEnglish) {
|
|
35
|
-
return messages;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
30
|
// Clone messages to avoid mutation
|
|
39
31
|
const modifiedMessages = JSON.parse(JSON.stringify(messages));
|
|
40
32
|
|
package/lib/ccs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
# Version (updated by scripts/bump-version.sh)
|
|
5
|
-
CCS_VERSION="3.4.
|
|
5
|
+
CCS_VERSION="3.4.3"
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
7
|
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
|
8
8
|
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
|
package/lib/ccs.ps1
CHANGED
|
@@ -12,7 +12,7 @@ param(
|
|
|
12
12
|
$ErrorActionPreference = "Stop"
|
|
13
13
|
|
|
14
14
|
# Version (updated by scripts/bump-version.sh)
|
|
15
|
-
$CcsVersion = "3.4.
|
|
15
|
+
$CcsVersion = "3.4.3"
|
|
16
16
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
17
17
|
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
|
|
18
18
|
$ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -93,7 +93,7 @@ function createConfigFiles() {
|
|
|
93
93
|
// Migrate from v3.1.1 to v3.2.0 (symlink architecture)
|
|
94
94
|
console.log('');
|
|
95
95
|
try {
|
|
96
|
-
const SharedManager = require('../bin/shared-manager');
|
|
96
|
+
const SharedManager = require('../bin/management/shared-manager');
|
|
97
97
|
const sharedManager = new SharedManager();
|
|
98
98
|
sharedManager.migrateFromV311();
|
|
99
99
|
sharedManager.ensureSharedDirectories();
|
|
@@ -1,114 +0,0 @@
|
|
|
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;
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* TaskClassifier - Classify user prompts as reasoning, execution, or mixed tasks
|
|
6
|
-
*
|
|
7
|
-
* Purpose: Determine task type to inform thinking enable/disable decision.
|
|
8
|
-
* Uses keyword-based matching for fast, deterministic classification.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* const classifier = new TaskClassifier();
|
|
12
|
-
* const taskType = classifier.classify(messages);
|
|
13
|
-
*
|
|
14
|
-
* Task types:
|
|
15
|
-
* - reasoning: Planning, design, analysis (enable thinking)
|
|
16
|
-
* - execution: Implementation, fixes, debugging (disable thinking for speed)
|
|
17
|
-
* - mixed: Ambiguous or both (default to safe thinking mode)
|
|
18
|
-
*
|
|
19
|
-
* Classification strategy:
|
|
20
|
-
* 1. Extract text from all user messages
|
|
21
|
-
* 2. Score against reasoning and execution keyword lists
|
|
22
|
-
* 3. Return type with highest score (or 'mixed' if tied/no matches)
|
|
23
|
-
*/
|
|
24
|
-
class TaskClassifier {
|
|
25
|
-
constructor(options = {}) {
|
|
26
|
-
this.keywords = {
|
|
27
|
-
reasoning: [
|
|
28
|
-
'plan', 'design', 'analyze', 'architecture', 'strategy',
|
|
29
|
-
'approach', 'consider', 'evaluate', 'research', 'explore',
|
|
30
|
-
'brainstorm', 'think about', 'pros and cons', 'alternatives',
|
|
31
|
-
'compare', 'recommend', 'assess', 'review', 'investigate'
|
|
32
|
-
],
|
|
33
|
-
execution: [
|
|
34
|
-
'fix', 'implement', 'debug', 'refactor', 'optimize',
|
|
35
|
-
'add', 'remove', 'update', 'create', 'delete',
|
|
36
|
-
'change', 'modify', 'replace', 'move', 'rename',
|
|
37
|
-
'test', 'run', 'execute', 'deploy', 'build'
|
|
38
|
-
]
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// Allow custom keywords via options
|
|
42
|
-
if (options.customKeywords) {
|
|
43
|
-
this.keywords = { ...this.keywords, ...options.customKeywords };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Classify messages as reasoning, execution, or mixed
|
|
49
|
-
* @param {Array} messages - Messages array
|
|
50
|
-
* @returns {string} 'reasoning', 'execution', or 'mixed'
|
|
51
|
-
*/
|
|
52
|
-
classify(messages) {
|
|
53
|
-
if (!messages || messages.length === 0) {
|
|
54
|
-
return 'mixed'; // Default to safe mode
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Extract text from all user messages
|
|
58
|
-
const text = messages
|
|
59
|
-
.filter(m => m.role === 'user')
|
|
60
|
-
.map(m => this._extractText(m.content))
|
|
61
|
-
.join(' ')
|
|
62
|
-
.toLowerCase();
|
|
63
|
-
|
|
64
|
-
if (!text.trim()) {
|
|
65
|
-
return 'mixed'; // No text found
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Score against keyword lists
|
|
69
|
-
const reasoningScore = this._matchScore(text, this.keywords.reasoning);
|
|
70
|
-
const executionScore = this._matchScore(text, this.keywords.execution);
|
|
71
|
-
|
|
72
|
-
// Classify based on scores
|
|
73
|
-
if (reasoningScore > executionScore) {
|
|
74
|
-
return 'reasoning';
|
|
75
|
-
} else if (executionScore > reasoningScore) {
|
|
76
|
-
return 'execution';
|
|
77
|
-
} else {
|
|
78
|
-
return 'mixed'; // Tied or no matches
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Extract text from message content
|
|
84
|
-
* @param {string|Array} content - Message content
|
|
85
|
-
* @returns {string} Extracted text
|
|
86
|
-
* @private
|
|
87
|
-
*/
|
|
88
|
-
_extractText(content) {
|
|
89
|
-
if (typeof content === 'string') {
|
|
90
|
-
return content;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (Array.isArray(content)) {
|
|
94
|
-
return content
|
|
95
|
-
.filter(block => block.type === 'text')
|
|
96
|
-
.map(block => block.text || '')
|
|
97
|
-
.join(' ');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return '';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Calculate keyword match score
|
|
105
|
-
* @param {string} text - Text to search
|
|
106
|
-
* @param {Array} keywords - Keywords to match
|
|
107
|
-
* @returns {number} Number of matches
|
|
108
|
-
* @private
|
|
109
|
-
*/
|
|
110
|
-
_matchScore(text, keywords) {
|
|
111
|
-
return keywords.reduce((score, keyword) => {
|
|
112
|
-
// Support both exact match and word boundary match
|
|
113
|
-
const regex = new RegExp(`\\b${this._escapeRegex(keyword)}\\b`, 'i');
|
|
114
|
-
return score + (regex.test(text) ? 1 : 0);
|
|
115
|
-
}, 0);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Escape special regex characters
|
|
120
|
-
* @param {string} str - String to escape
|
|
121
|
-
* @returns {string} Escaped string
|
|
122
|
-
* @private
|
|
123
|
-
*/
|
|
124
|
-
_escapeRegex(str) {
|
|
125
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Get classification details (for debugging)
|
|
130
|
-
* @param {Array} messages - Messages array
|
|
131
|
-
* @returns {Object} { type, reasoningScore, executionScore, text }
|
|
132
|
-
*/
|
|
133
|
-
classifyWithDetails(messages) {
|
|
134
|
-
const text = messages
|
|
135
|
-
.filter(m => m.role === 'user')
|
|
136
|
-
.map(m => this._extractText(m.content))
|
|
137
|
-
.join(' ')
|
|
138
|
-
.toLowerCase();
|
|
139
|
-
|
|
140
|
-
const reasoningScore = this._matchScore(text, this.keywords.reasoning);
|
|
141
|
-
const executionScore = this._matchScore(text, this.keywords.execution);
|
|
142
|
-
|
|
143
|
-
let type;
|
|
144
|
-
if (reasoningScore > executionScore) {
|
|
145
|
-
type = 'reasoning';
|
|
146
|
-
} else if (executionScore > reasoningScore) {
|
|
147
|
-
type = 'execution';
|
|
148
|
-
} else {
|
|
149
|
-
type = 'mixed';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
type,
|
|
154
|
-
reasoningScore,
|
|
155
|
-
executionScore,
|
|
156
|
-
textLength: text.length,
|
|
157
|
-
textPreview: text.substring(0, 100) + (text.length > 100 ? '...' : '')
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = TaskClassifier;
|