refactor(glmt)!: simplify with keyword-based thinking control (v3.4.3)

YAGNI/KISS/DRY refactor removing 926 LOC of unused complexity:

- Replace budget-calculator + task-classifier with 40 LOC keyword detection

- Add 4-tier thinking: think < think hard < think harder < ultrathink

- Remove env vars: CCS_GLMT_THINKING_BUDGET, CCS_GLMT_STREAMING, CCS_GLMT_FORCE_ENGLISH

- Add streaming auto-fallback on error

- Rename CCS_DEBUG_LOG → CCS_DEBUG (backward compatible)

- Fix ultrathink effort: high → max

- Fix GLMT proxy path after restructure

Test coverage: 27 → 35 tests, all passing

Net change: +251, -1177 lines (-926 LOC, 78% reduction)

BREAKING CHANGE: Removed CCS_GLMT_THINKING_BUDGET, CCS_GLMT_STREAMING, CCS_GLMT_FORCE_ENGLISH env vars
This commit is contained in:
kaitranntt
2025-11-11 18:14:05 -05:00
parent 9b9182d418
commit d3e6be171f
16 changed files with 251 additions and 1177 deletions
+26
View File
@@ -2,6 +2,32 @@
Format: [Keep a Changelog](https://keepachangelog.com/)
## [3.4.3] - 2025-11-11
### Added
- Keyword thinking control: `think` < `think hard` < `think harder` < `ultrathink`
- Streaming auto-fallback on error
### Changed
- YAGNI/KISS: Removed budget-calculator.js, task-classifier.js (-272 LOC)
- `CCS_DEBUG_LOG``CCS_DEBUG` (backward compatible)
### Removed
- `CCS_GLMT_THINKING_BUDGET`, `CCS_GLMT_STREAMING`, `CCS_GLMT_FORCE_ENGLISH` env vars
### Fixed
- GLMT proxy path (glmt/glmt-proxy.js)
- `ultrathink` effort: `high``max`
---
## [3.4.2] - 2025-11-11
### Changed
- Version bump for npm CI workaround
---
## [3.4.1] - 2025-11-11
### Added
+40 -51
View File
@@ -51,53 +51,48 @@ CCS (Claude Code Switch): CLI wrapper for instant switching between multiple Cla
- OpenAI tool_calls → Anthropic tool_use blocks
- Streaming tool calls with input_json deltas
- MCP tools execute correctly (no XML tag output)
- **Streaming mode** (default):
- **Streaming mode** (default with auto-fallback):
- `SSEParser` parses incremental SSE events from Z.AI
- `DeltaAccumulator` tracks content block state
- `glmt-transformer.js` converts OpenAI deltas → Anthropic events
- Real-time delivery to Claude CLI (TTFB <500ms)
- **Buffered mode** (`CCS_GLMT_STREAMING=disabled`):
- Waits for complete response
- Single transformation pass
- Higher latency (2-10s TTFB)
6. Thinking blocks and tool calls appear in Claude Code UI (real-time or complete)
- Auto-fallback to buffered mode on streaming errors
6. Thinking blocks and tool calls appear in Claude Code UI (real-time delivery)
**Thinking parameter support**:
- Claude CLI `thinking` parameter recognized and processed
- Parameter precedence: Claude CLI `thinking` > message tags > default
- Parameter precedence: Claude CLI `thinking` > message tags > keywords
- `thinking.type`: 'enabled'/'disabled' controls reasoning blocks
- `thinking.budget_tokens` mapped to effort levels:
- <= 2048: low effort
- <= 8192: medium effort
- > 8192: high effort
- Input validation: logs warnings for invalid values
- Backward compatible: control tags still work
- Backward compatible: control tags and keywords work
**Files**:
- `bin/glmt-proxy.js` (463 lines): HTTP proxy server with streaming
- `bin/glmt-transformer.js` (685 lines): Format conversion + delta handling + tool transformation + control mechanisms
- `bin/locale-enforcer.js` (85 lines): Force English output (prevents Chinese responses)
- `bin/budget-calculator.js` (109 lines): Thinking on/off based on task type + budget
- `bin/task-classifier.js` (146 lines): Classify tasks (reasoning vs execution)
- `bin/sse-parser.js` (97 lines): SSE stream parser
- `bin/delta-accumulator.js` (156 lines): State tracking for streaming + tool calls + loop detection
- `bin/glmt/glmt-proxy.js`: HTTP proxy server with streaming + auto-fallback
- `bin/glmt/glmt-transformer.js`: Format conversion + delta handling + tool transformation + keyword detection
- `bin/glmt/locale-enforcer.js`: Always enforces English output (prevents Chinese responses)
- `bin/glmt/sse-parser.js`: SSE stream parser
- `bin/glmt/delta-accumulator.js`: State tracking for streaming + tool calls + loop detection
- `config/base-glmt.settings.json`: Template with Z.AI endpoint
- `tests/glmt-transformer.test.js`: Unit tests (110 tests passing)
- `tests/unit/glmt/glmt-transformer.test.js`: Unit tests (35 tests passing)
**Control tags**:
**Thinking control** (natural language):
- `think` - Enable reasoning (low effort)
- `think hard` - Enable reasoning (medium effort)
- `think harder` - Enable reasoning (high effort)
- `ultrathink` - Maximum reasoning depth (max effort)
Example:
```bash
ccs glmt "think about the architecture before implementing"
ccs glmt "ultrathink this complex algorithm optimization"
```
**Control tags** (advanced):
- `<Thinking:On|Off>` - Enable/disable reasoning
- `<Effort:Low|Medium|High>` - Control reasoning depth (deprecated - Z.AI only supports binary thinking)
- `<Effort:Low|Medium|High>` - Control reasoning depth
**Environment variables**:
- `CCS_GLMT_STREAMING=disabled` - Force buffered mode
- `CCS_GLMT_STREAMING=force` - Force streaming (override client)
- `CCS_DEBUG_LOG=1` - Enable debug file logging
- `CCS_GLMT_FORCE_ENGLISH=true` - Force English output (default: true)
- `CCS_GLMT_THINKING_BUDGET=8192` - Control thinking on/off based on task type
- 0 or "unlimited": Always enable thinking
- 1-2048: Disable thinking (fast execution)
- 2049-8192: Enable for reasoning tasks only
- >8192: Always enable thinking
- `CCS_DEBUG=1` - Enable debug file logging to ~/.ccs/logs/
**Security limits** (DoS protection):
- SSE buffer: 1MB max
@@ -107,11 +102,12 @@ CCS (Claude Code Switch): CLI wrapper for instant switching between multiple Cla
**Confirmed working**: Z.AI (1498 reasoning chunks tested)
**Control mechanisms** (v3.6):
1. **Locale enforcement**: Injects "MUST respond in English" into system prompts to prevent Chinese output
2. **Budget control**: Thinking on/off based on task type + budget (Z.AI only supports binary thinking, NOT effort levels)
3. **Task classification**: Keywords-based (reasoning vs execution) - triggers thinking for problem-solving tasks
4. **Loop detection**: Triggers after 3 consecutive thinking blocks with no tool calls (prevents unbounded planning loops)
**Control mechanisms**:
1. **Locale enforcement**: Always injects "MUST respond in English" into system prompts
2. **Thinking keywords**: 4-tier system matching Anthropic's levels
- `think` (low) < `think hard` (medium) < `think harder` (high) < `ultrathink` (max)
- Priority: ultrathink wins when multiple keywords present
3. **Loop detection**: Triggers after 3 consecutive thinking blocks with no tool calls (DoS protection)
### v3.1 Shared Data
@@ -373,32 +369,28 @@ All values = strings (not booleans/objects) to prevent PowerShell crashes.
**No Thinking Blocks**:
- Check Z.AI API plan supports reasoning_content
- Verify `<Thinking:On>` tag not overridden
- Check `CCS_GLMT_THINKING_BUDGET` value (default: 8192 - reasoning tasks only)
- Set `CCS_GLMT_THINKING_BUDGET=0` or `CCS_GLMT_THINKING_BUDGET=unlimited` to always enable thinking
- Try using "think" keywords in prompt: `ccs glmt "think about the solution"`
- Test with `ccs glm` (no thinking) to isolate proxy issues
**Chinese Output / Unexpected Language**:
- Default: `CCS_GLMT_FORCE_ENGLISH=true` (enabled)
- Disable: `export CCS_GLMT_FORCE_ENGLISH=false`
- Locale enforcer injects "MUST respond in English" into system prompts
- Locale enforcer always injects "MUST respond in English" into system prompts
- If issues persist, check Z.AI API configuration
**Unbounded Planning Loops**:
- Loop detection triggers after 3 consecutive thinking blocks with no tool calls
- Token waste mitigation: Budget control disables thinking for execution tasks
- Override: Set `CCS_GLMT_THINKING_BUDGET=0` or `unlimited` to always enable
- Loop detection triggers after 3 consecutive thinking blocks with no tool calls (auto-stops)
- Use specific execution keywords: "fix", "implement", "debug" to avoid excessive planning
**Tool Execution Issues**:
- **MCP tools outputting XML**: Fixed in v3.5 - upgrade CCS
- **Tool calls not recognized**: Ensure Z.AI API supports function calling
- **Incomplete tool arguments**: Streaming tool calls require complete JSON accumulation
- **Tool results not processed**: Check tool_result format matches Anthropic spec
- Debug with `CCS_DEBUG_LOG=1` to inspect request/response transformation
- Debug with `CCS_DEBUG=1` to inspect request/response transformation
**Streaming Issues**:
- Buffer errors: Hit DoS protection limits (1MB SSE, 10MB content)
- Slow TTFB: Try disabling streaming: `CCS_GLMT_STREAMING=disabled`
- Slow TTFB: Streaming auto-fallback to buffered mode on error
- Incomplete reasoning: Z.AI may not support incremental delivery for all models
- Fallback: Buffered mode always available
**Debug Mode**:
```bash
@@ -406,12 +398,9 @@ All values = strings (not booleans/objects) to prevent PowerShell crashes.
ccs glmt --verbose "test"
# File logging
export CCS_DEBUG_LOG=1
export CCS_DEBUG=1
ccs glmt --verbose "test"
# Logs: ~/.ccs/logs/
# Test streaming vs buffered
CCS_GLMT_STREAMING=disabled ccs glmt "compare latency"
```
## Error Handling
+1 -1
View File
@@ -1 +1 @@
3.4.2
3.4.3
-114
View File
@@ -1,114 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* BudgetCalculator - Control thinking enable/disable based on task complexity
*
* Purpose: Z.AI API only supports binary thinking (on/off), not reasoning_effort levels.
* This module decides when to enable thinking based on task type and budget preferences.
*
* Usage:
* const calculator = new BudgetCalculator();
* const shouldThink = calculator.shouldEnableThinking(taskType, envBudget);
*
* Configuration:
* CCS_GLMT_THINKING_BUDGET:
* - 0 or "unlimited": Always enable thinking (power user mode)
* - 1-2048: Disable thinking (fast execution, low budget)
* - 2049-8192: Enable thinking for reasoning tasks only (default)
* - >8192: Always enable thinking (high budget)
*
* Task type mapping:
* - reasoning: Enable thinking (planning, design, analysis)
* - execution: Disable thinking (fix, implement, debug) unless high budget
* - mixed: Enable thinking if budget >= medium threshold
*/
class BudgetCalculator {
constructor(options = {}) {
this.budgetThresholds = {
low: 2048, // Disable thinking (fast execution)
medium: 8192 // Enable thinking for reasoning tasks
};
this.defaultBudget = options.defaultBudget || 8192; // Default: enable thinking for reasoning
}
/**
* Determine if thinking should be enabled based on task type and budget
* @param {string} taskType - 'reasoning', 'execution', or 'mixed'
* @param {string|number} envBudget - CCS_GLMT_THINKING_BUDGET value
* @returns {boolean} True if thinking should be enabled
*/
shouldEnableThinking(taskType, envBudget) {
const budget = this._parseBudget(envBudget);
// Unlimited budget (0): Always enable thinking
if (budget === 0) {
return true;
}
// Low budget (<= 2048): Disable thinking (fast execution mode)
if (budget <= this.budgetThresholds.low) {
return false;
}
// High budget (> 8192): Always enable thinking
if (budget > this.budgetThresholds.medium) {
return true;
}
// Medium budget (2049-8192): Task-aware decision
if (taskType === 'reasoning') {
return true; // Enable thinking for planning/design tasks
} else if (taskType === 'execution') {
return false; // Disable thinking for quick fixes
} else {
return true; // Enable for mixed/ambiguous tasks (default safe)
}
}
/**
* Parse budget from environment variable or use default
* @param {string|number} envBudget - Budget value
* @returns {number} Parsed budget (0 = unlimited)
* @private
*/
_parseBudget(envBudget) {
// CRITICAL: Check for undefined/null explicitly, not falsy (0 is valid!)
if (envBudget === undefined || envBudget === null || envBudget === '') {
return this.defaultBudget;
}
// Handle string values
if (typeof envBudget === 'string') {
if (envBudget.toLowerCase() === 'unlimited') {
return 0;
}
const parsed = parseInt(envBudget, 10);
if (isNaN(parsed)) {
return this.defaultBudget;
}
return parsed < 0 ? 0 : parsed;
}
// Handle number values
if (typeof envBudget === 'number') {
return envBudget < 0 ? 0 : envBudget;
}
return this.defaultBudget;
}
/**
* Get human-readable budget description
* @param {number} budget - Budget value
* @returns {string} Description
*/
getBudgetDescription(budget) {
if (budget === 0) return 'unlimited (always think)';
if (budget <= this.budgetThresholds.low) return 'low (fast execution, no thinking)';
if (budget <= this.budgetThresholds.medium) return 'medium (task-aware thinking)';
return 'high (always think)';
}
}
module.exports = BudgetCalculator;
+16 -9
View File
@@ -23,7 +23,7 @@ const DeltaAccumulator = require('./delta-accumulator');
*
* Debugging:
* - Verbose: Pass --verbose to see request/response logs
* - Debug: Set CCS_DEBUG_LOG=1 to write logs to ~/.ccs/logs/
* - Debug: Set CCS_DEBUG=1 to write logs to ~/.ccs/logs/
*
* Usage:
* const proxy = new GlmtProxy({ verbose: true });
@@ -33,7 +33,7 @@ class GlmtProxy {
constructor(config = {}) {
this.transformer = new GlmtTransformer({
verbose: config.verbose,
debugLog: config.debugLog || process.env.CCS_DEBUG_LOG === '1'
debugLog: config.debugLog || process.env.CCS_DEBUG === '1' || process.env.CCS_DEBUG_LOG === '1'
});
// Use ANTHROPIC_BASE_URL from environment (set by settings.json) or fallback to Z.AI default
this.upstreamUrl = process.env.ANTHROPIC_BASE_URL || 'https://api.z.ai/api/coding/paas/v4/chat/completions';
@@ -41,8 +41,6 @@ class GlmtProxy {
this.port = null;
this.verbose = config.verbose || false;
this.timeout = config.timeout || 120000; // 120s default
this.streamingEnabled = process.env.CCS_GLMT_STREAMING !== 'disabled';
this.forceStreaming = process.env.CCS_GLMT_STREAMING === 'force';
}
/**
@@ -63,8 +61,7 @@ class GlmtProxy {
// Info message (only show in verbose mode)
if (this.verbose) {
const mode = this.streamingEnabled ? 'streaming mode' : 'buffered mode';
console.error(`[glmt] Proxy listening on port ${this.port} (${mode})`);
console.error(`[glmt] Proxy listening on port ${this.port} (streaming with auto-fallback)`);
}
// Debug mode notice
@@ -127,11 +124,21 @@ class GlmtProxy {
this.log(`Request does NOT contain thinking parameter (will use message tags or default)`);
}
// Branch: streaming or buffered
const useStreaming = (anthropicRequest.stream && this.streamingEnabled) || this.forceStreaming;
// Try streaming first (default), fallback to buffered on error
const useStreaming = anthropicRequest.stream !== false;
if (useStreaming) {
await this._handleStreamingRequest(req, res, anthropicRequest, startTime);
try {
await this._handleStreamingRequest(req, res, anthropicRequest, startTime);
} catch (streamError) {
this.log(`Streaming failed: ${streamError.message}, retrying buffered mode`);
try {
await this._handleBufferedRequest(req, res, anthropicRequest, startTime);
} catch (bufferedError) {
// Both modes failed, propagate error
throw bufferedError;
}
}
} else {
await this._handleBufferedRequest(req, res, anthropicRequest, startTime);
}
+67 -28
View File
@@ -8,8 +8,6 @@ const os = require('os');
const SSEParser = require('./sse-parser');
const DeltaAccumulator = require('./delta-accumulator');
const LocaleEnforcer = require('./locale-enforcer');
const BudgetCalculator = require('./budget-calculator');
const TaskClassifier = require('./task-classifier');
/**
* GlmtTransformer - Convert between Anthropic and OpenAI formats with thinking and tool support
@@ -33,10 +31,23 @@ const TaskClassifier = require('./task-classifier');
* <Effort:Low|Medium|High> - Control reasoning depth
*/
class GlmtTransformer {
static _warnedDeprecation = false;
constructor(config = {}) {
this.defaultThinking = config.defaultThinking ?? true;
this.verbose = config.verbose || false;
this.debugLog = config.debugLog ?? process.env.CCS_DEBUG_LOG === '1';
// Support both CCS_DEBUG and CCS_DEBUG_LOG (with deprecation warning)
const oldVar = process.env.CCS_DEBUG_LOG === '1';
const newVar = process.env.CCS_DEBUG === '1';
this.debugLog = config.debugLog ?? (newVar || oldVar);
// Show deprecation warning once
if (oldVar && !newVar && !GlmtTransformer._warnedDeprecation) {
console.warn('[glmt] Warning: CCS_DEBUG_LOG is deprecated, use CCS_DEBUG instead');
GlmtTransformer._warnedDeprecation = true;
}
this.debugLogDir = config.debugLogDir || path.join(os.homedir(), '.ccs', 'logs');
this.modelMaxTokens = {
'GLM-4.6': 128000,
@@ -47,14 +58,8 @@ class GlmtTransformer {
this.EFFORT_LOW_THRESHOLD = 2048;
this.EFFORT_HIGH_THRESHOLD = 8192;
// Initialize locale enforcer
this.localeEnforcer = new LocaleEnforcer({
forceEnglish: process.env.CCS_GLMT_FORCE_ENGLISH !== 'false'
});
// Initialize budget calculator and task classifier
this.budgetCalculator = new BudgetCalculator();
this.taskClassifier = new TaskClassifier();
// Initialize locale enforcer (always enforce English)
this.localeEnforcer = new LocaleEnforcer();
}
/**
@@ -73,25 +78,15 @@ class GlmtTransformer {
);
const hasControlTags = this._hasThinkingTags(anthropicRequest.messages || []);
// 2. Classify task type for intelligent thinking control
const taskType = this.taskClassifier.classify(anthropicRequest.messages || []);
this.log(`Task classified as: ${taskType}`);
// 3. Check budget and decide if thinking should be enabled
const envBudget = process.env.CCS_GLMT_THINKING_BUDGET;
const shouldThink = this.budgetCalculator.shouldEnableThinking(taskType, envBudget);
this.log(`Budget decision: thinking=${shouldThink} (budget: ${envBudget || 'default'}, type: ${taskType})`);
// Apply budget-based thinking control ONLY if:
// - No Claude CLI thinking parameter AND
// - No control tags in messages AND
// - Budget env var is explicitly set
if (!anthropicRequest.thinking && !hasControlTags && envBudget) {
thinkingConfig.thinking = shouldThink;
this.log('Applied budget-based thinking control');
// 2. Detect "think" keywords in user prompts (Anthropic-style)
const keywordConfig = this._detectThinkKeywords(anthropicRequest.messages || []);
if (keywordConfig && !anthropicRequest.thinking && !hasControlTags) {
thinkingConfig.thinking = keywordConfig.thinking;
thinkingConfig.effort = keywordConfig.effort;
this.log(`Detected think keyword: ${keywordConfig.keyword}, effort=${keywordConfig.effort}`);
}
// 4. Check anthropicRequest.thinking parameter (takes precedence over budget)
// 3. Check anthropicRequest.thinking parameter (takes precedence)
// Claude CLI sends this when alwaysThinkingEnabled is configured
if (anthropicRequest.thinking) {
if (anthropicRequest.thinking.type === 'enabled') {
@@ -440,6 +435,50 @@ class GlmtTransformer {
};
}
/**
* Detect Anthropic-style "think" keywords in user prompts
* Maps: "ultrathink" > "think harder" > "think hard" > "think"
* @param {Array} messages - Messages array
* @returns {Object|null} { thinking, effort, keyword } or null
* @private
*/
_detectThinkKeywords(messages) {
if (!messages || messages.length === 0) return null;
// Extract text from user messages
const text = messages
.filter(m => m.role === 'user')
.map(m => {
if (typeof m.content === 'string') return m.content;
if (Array.isArray(m.content)) {
return m.content
.filter(block => block.type === 'text')
.map(block => block.text || '')
.join(' ');
}
return '';
})
.join(' ');
// Priority: ultrathink > think harder > think hard > think
// Effort levels: max > high > medium > low (matches Anthropic's 4-tier system)
// Use word boundaries to avoid matching "thinking", "rethink", etc.
if (/\bultrathink\b/i.test(text)) {
return { thinking: true, effort: 'max', keyword: 'ultrathink' };
}
if (/\bthink\s+harder\b/i.test(text)) {
return { thinking: true, effort: 'high', keyword: 'think harder' };
}
if (/\bthink\s+hard\b/i.test(text)) {
return { thinking: true, effort: 'medium', keyword: 'think hard' };
}
if (/\bthink\b/i.test(text)) {
return { thinking: true, effort: 'low', keyword: 'think' };
}
return null; // No keywords detected
}
/**
* Inject reasoning parameters into OpenAI request
* @param {Object} openaiRequest - OpenAI request to modify
+2 -10
View File
@@ -5,15 +5,12 @@
* LocaleEnforcer - Force English output from GLM models
*
* Purpose: GLM models default to Chinese when prompts are ambiguous or contain Chinese context.
* This module injects "MUST respond in English" instruction into system prompt or first user message.
* This module always injects "MUST respond in English" instruction into system prompt or first user message.
*
* Usage:
* const enforcer = new LocaleEnforcer({ forceEnglish: true });
* const enforcer = new LocaleEnforcer();
* const modifiedMessages = enforcer.injectInstruction(messages);
*
* Configuration:
* CCS_GLMT_FORCE_ENGLISH=false - Disable locale enforcement (allow multilingual)
*
* Strategy:
* 1. If system prompt exists: Prepend instruction
* 2. If no system prompt: Prepend to first user message
@@ -21,7 +18,6 @@
*/
class LocaleEnforcer {
constructor(options = {}) {
this.forceEnglish = options.forceEnglish ?? true;
this.instruction = "CRITICAL: You MUST respond in English only, regardless of the input language or context. This is a strict requirement.";
}
@@ -31,10 +27,6 @@ class LocaleEnforcer {
* @returns {Array} Modified messages array
*/
injectInstruction(messages) {
if (!this.forceEnglish) {
return messages;
}
// Clone messages to avoid mutation
const modifiedMessages = JSON.parse(JSON.stringify(messages));
-162
View File
@@ -1,162 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* TaskClassifier - Classify user prompts as reasoning, execution, or mixed tasks
*
* Purpose: Determine task type to inform thinking enable/disable decision.
* Uses keyword-based matching for fast, deterministic classification.
*
* Usage:
* const classifier = new TaskClassifier();
* const taskType = classifier.classify(messages);
*
* Task types:
* - reasoning: Planning, design, analysis (enable thinking)
* - execution: Implementation, fixes, debugging (disable thinking for speed)
* - mixed: Ambiguous or both (default to safe thinking mode)
*
* Classification strategy:
* 1. Extract text from all user messages
* 2. Score against reasoning and execution keyword lists
* 3. Return type with highest score (or 'mixed' if tied/no matches)
*/
class TaskClassifier {
constructor(options = {}) {
this.keywords = {
reasoning: [
'plan', 'design', 'analyze', 'architecture', 'strategy',
'approach', 'consider', 'evaluate', 'research', 'explore',
'brainstorm', 'think about', 'pros and cons', 'alternatives',
'compare', 'recommend', 'assess', 'review', 'investigate'
],
execution: [
'fix', 'implement', 'debug', 'refactor', 'optimize',
'add', 'remove', 'update', 'create', 'delete',
'change', 'modify', 'replace', 'move', 'rename',
'test', 'run', 'execute', 'deploy', 'build'
]
};
// Allow custom keywords via options
if (options.customKeywords) {
this.keywords = { ...this.keywords, ...options.customKeywords };
}
}
/**
* Classify messages as reasoning, execution, or mixed
* @param {Array} messages - Messages array
* @returns {string} 'reasoning', 'execution', or 'mixed'
*/
classify(messages) {
if (!messages || messages.length === 0) {
return 'mixed'; // Default to safe mode
}
// Extract text from all user messages
const text = messages
.filter(m => m.role === 'user')
.map(m => this._extractText(m.content))
.join(' ')
.toLowerCase();
if (!text.trim()) {
return 'mixed'; // No text found
}
// Score against keyword lists
const reasoningScore = this._matchScore(text, this.keywords.reasoning);
const executionScore = this._matchScore(text, this.keywords.execution);
// Classify based on scores
if (reasoningScore > executionScore) {
return 'reasoning';
} else if (executionScore > reasoningScore) {
return 'execution';
} else {
return 'mixed'; // Tied or no matches
}
}
/**
* Extract text from message content
* @param {string|Array} content - Message content
* @returns {string} Extracted text
* @private
*/
_extractText(content) {
if (typeof content === 'string') {
return content;
}
if (Array.isArray(content)) {
return content
.filter(block => block.type === 'text')
.map(block => block.text || '')
.join(' ');
}
return '';
}
/**
* Calculate keyword match score
* @param {string} text - Text to search
* @param {Array} keywords - Keywords to match
* @returns {number} Number of matches
* @private
*/
_matchScore(text, keywords) {
return keywords.reduce((score, keyword) => {
// Support both exact match and word boundary match
const regex = new RegExp(`\\b${this._escapeRegex(keyword)}\\b`, 'i');
return score + (regex.test(text) ? 1 : 0);
}, 0);
}
/**
* Escape special regex characters
* @param {string} str - String to escape
* @returns {string} Escaped string
* @private
*/
_escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Get classification details (for debugging)
* @param {Array} messages - Messages array
* @returns {Object} { type, reasoningScore, executionScore, text }
*/
classifyWithDetails(messages) {
const text = messages
.filter(m => m.role === 'user')
.map(m => this._extractText(m.content))
.join(' ')
.toLowerCase();
const reasoningScore = this._matchScore(text, this.keywords.reasoning);
const executionScore = this._matchScore(text, this.keywords.execution);
let type;
if (reasoningScore > executionScore) {
type = 'reasoning';
} else if (executionScore > reasoningScore) {
type = 'execution';
} else {
type = 'mixed';
}
return {
type,
reasoningScore,
executionScore,
textLength: text.length,
textPreview: text.substring(0, 100) + (text.length > 100 ? '...' : '')
};
}
}
module.exports = TaskClassifier;
+1 -1
View File
@@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (irm | iex)
# For git installations, VERSION file is read if available
$CcsVersion = "3.4.1"
$CcsVersion = "3.4.3"
# Try to read VERSION file for git installations
if ($ScriptDir) {
+1 -1
View File
@@ -32,7 +32,7 @@ fi
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (curl | bash)
# For git installations, VERSION file is read if available
CCS_VERSION="3.4.1"
CCS_VERSION="3.4.3"
# Try to read VERSION file for git installations
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
# Version (updated by scripts/bump-version.sh)
CCS_VERSION="3.4.1"
CCS_VERSION="3.4.3"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
+1 -1
View File
@@ -12,7 +12,7 @@ param(
$ErrorActionPreference = "Stop"
# Version (updated by scripts/bump-version.sh)
$CcsVersion = "3.4.1"
$CcsVersion = "3.4.3"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
$ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "3.4.2",
"version": "3.4.3",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
-338
View File
@@ -1,338 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* BudgetCalculator Unit Tests
*
* Tests 4 scenarios:
* 1. Default budget (8192) → Thinking enabled for reasoning tasks
* 2. Low budget (2048) → Thinking disabled (fast execution)
* 3. High budget (16384) → Thinking always enabled
* 4. Unlimited (0) → Thinking always enabled
*/
const assert = require('assert');
const BudgetCalculator = require('../../../bin/glmt/budget-calculator');
describe('BudgetCalculator', () => {
describe('Scenario 1: Default budget (8192) - Task-aware thinking', () => {
it('should enable thinking for reasoning tasks with default budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('reasoning', null);
assert.strictEqual(result, true);
});
it('should disable thinking for execution tasks with default budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('execution', null);
assert.strictEqual(result, false);
});
it('should enable thinking for mixed tasks with default budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('mixed', null);
assert.strictEqual(result, true);
});
it('should use default budget (8192) when not specified', () => {
const calculator = new BudgetCalculator();
const budget = calculator._parseBudget(null);
assert.strictEqual(budget, 8192);
});
it('should describe default budget correctly', () => {
const calculator = new BudgetCalculator();
const description = calculator.getBudgetDescription(8192);
assert.strictEqual(description, 'medium (task-aware thinking)');
});
});
describe('Scenario 2: Low budget (2048) - Fast execution, no thinking', () => {
it('should disable thinking for reasoning tasks with low budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('reasoning', 2048);
assert.strictEqual(result, false);
});
it('should disable thinking for execution tasks with low budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('execution', 2048);
assert.strictEqual(result, false);
});
it('should disable thinking for mixed tasks with low budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('mixed', 2048);
assert.strictEqual(result, false);
});
it('should parse low budget from string', () => {
const calculator = new BudgetCalculator();
const budget = calculator._parseBudget('2048');
assert.strictEqual(budget, 2048);
});
it('should describe low budget correctly', () => {
const calculator = new BudgetCalculator();
const description = calculator.getBudgetDescription(2048);
assert.strictEqual(description, 'low (fast execution, no thinking)');
});
it('should treat budget <= 2048 as low budget', () => {
const calculator = new BudgetCalculator();
assert.strictEqual(calculator.shouldEnableThinking('reasoning', 1024), false);
assert.strictEqual(calculator.shouldEnableThinking('reasoning', 2000), false);
assert.strictEqual(calculator.shouldEnableThinking('reasoning', 2048), false);
});
});
describe('Scenario 3: High budget (16384) - Always enable thinking', () => {
it('should enable thinking for reasoning tasks with high budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('reasoning', 16384);
assert.strictEqual(result, true);
});
it('should enable thinking for execution tasks with high budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('execution', 16384);
assert.strictEqual(result, true);
});
it('should enable thinking for mixed tasks with high budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('mixed', 16384);
assert.strictEqual(result, true);
});
it('should parse high budget from string', () => {
const calculator = new BudgetCalculator();
const budget = calculator._parseBudget('16384');
assert.strictEqual(budget, 16384);
});
it('should describe high budget correctly', () => {
const calculator = new BudgetCalculator();
const description = calculator.getBudgetDescription(16384);
assert.strictEqual(description, 'high (always think)');
});
it('should treat budget > 8192 as high budget', () => {
const calculator = new BudgetCalculator();
assert.strictEqual(calculator.shouldEnableThinking('execution', 8193), true);
assert.strictEqual(calculator.shouldEnableThinking('execution', 10000), true);
assert.strictEqual(calculator.shouldEnableThinking('execution', 32768), true);
});
});
describe('Scenario 4: Unlimited budget (0) - Always enable thinking', () => {
it('should enable thinking for reasoning tasks with unlimited budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('reasoning', 0);
assert.strictEqual(result, true);
});
it('should enable thinking for execution tasks with unlimited budget', () => {
const calculator = new BudgetCalculator();
// FIXED: _parseBudget(0) now correctly returns 0 (unlimited)
const result = calculator.shouldEnableThinking('execution', 0);
// Unlimited budget should always enable thinking
assert.strictEqual(result, true);
});
it('should enable thinking for mixed tasks with unlimited budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('mixed', 0);
assert.strictEqual(result, true);
});
it('should parse unlimited from string "unlimited"', () => {
const calculator = new BudgetCalculator();
const budget = calculator._parseBudget('unlimited');
assert.strictEqual(budget, 0);
});
it('should parse unlimited from string "UNLIMITED" (case insensitive)', () => {
const calculator = new BudgetCalculator();
const budget = calculator._parseBudget('UNLIMITED');
assert.strictEqual(budget, 0);
});
it('should parse unlimited from number 0', () => {
const calculator = new BudgetCalculator();
// FIXED: _parseBudget(0) now correctly returns 0 (unlimited)
const budget = calculator._parseBudget(0);
// Should return 0 (unlimited)
assert.strictEqual(budget, 0);
});
it('should describe unlimited budget correctly', () => {
const calculator = new BudgetCalculator();
const description = calculator.getBudgetDescription(0);
assert.strictEqual(description, 'unlimited (always think)');
});
it('should treat negative numbers as unlimited', () => {
const calculator = new BudgetCalculator();
const budget1 = calculator._parseBudget(-1);
const budget2 = calculator._parseBudget(-100);
assert.strictEqual(budget1, 0);
assert.strictEqual(budget2, 0);
assert.strictEqual(calculator.shouldEnableThinking('execution', -1), true);
});
});
describe('Edge cases and boundary conditions', () => {
it('should handle medium budget boundaries (2049-8192)', () => {
const calculator = new BudgetCalculator();
// Just above low threshold
assert.strictEqual(calculator.shouldEnableThinking('reasoning', 2049), true);
assert.strictEqual(calculator.shouldEnableThinking('execution', 2049), false);
// At medium threshold
assert.strictEqual(calculator.shouldEnableThinking('reasoning', 8192), true);
assert.strictEqual(calculator.shouldEnableThinking('execution', 8192), false);
});
it('should handle invalid budget strings gracefully', () => {
const calculator = new BudgetCalculator();
const budget1 = calculator._parseBudget('invalid');
const budget2 = calculator._parseBudget('abc123');
const budget3 = calculator._parseBudget('');
assert.strictEqual(budget1, 8192); // Default
assert.strictEqual(budget2, 8192); // Default
assert.strictEqual(budget3, 8192); // Default
});
it('should handle custom default budget', () => {
const calculator = new BudgetCalculator({ defaultBudget: 4096 });
const budget = calculator._parseBudget(null);
assert.strictEqual(budget, 4096);
});
it('should handle undefined task type as mixed', () => {
const calculator = new BudgetCalculator();
const result1 = calculator.shouldEnableThinking(undefined, 8192);
const result2 = calculator.shouldEnableThinking(null, 8192);
// Should default to safe mode (true for medium budget)
assert.strictEqual(result1, true);
assert.strictEqual(result2, true);
});
it('should handle number type budgets directly', () => {
const calculator = new BudgetCalculator();
const result1 = calculator.shouldEnableThinking('execution', 16384);
const result2 = calculator.shouldEnableThinking('execution', 2048);
assert.strictEqual(result1, true); // High budget
assert.strictEqual(result2, false); // Low budget
});
it('should describe all budget ranges correctly', () => {
const calculator = new BudgetCalculator();
assert.strictEqual(calculator.getBudgetDescription(0), 'unlimited (always think)');
assert.strictEqual(calculator.getBudgetDescription(1024), 'low (fast execution, no thinking)');
assert.strictEqual(calculator.getBudgetDescription(2048), 'low (fast execution, no thinking)');
assert.strictEqual(calculator.getBudgetDescription(4096), 'medium (task-aware thinking)');
assert.strictEqual(calculator.getBudgetDescription(8192), 'medium (task-aware thinking)');
assert.strictEqual(calculator.getBudgetDescription(16384), 'high (always think)');
});
});
describe('Real-world scenarios', () => {
it('should handle planning task with default budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('reasoning', process.env.CCS_GLMT_THINKING_BUDGET);
assert.strictEqual(result, true);
});
it('should handle quick fix task with low budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('execution', 1024);
assert.strictEqual(result, false);
});
it('should handle complex analysis with high budget', () => {
const calculator = new BudgetCalculator();
const result = calculator.shouldEnableThinking('reasoning', 32768);
assert.strictEqual(result, true);
});
});
});
// Run tests if executed directly
if (require.main === module) {
const Mocha = require('mocha');
const mocha = new Mocha({ reporter: 'spec' });
mocha.suite.emit('pre-require', global, null, mocha);
// Load this test file
require(module.filename);
mocha.run(failures => {
process.exitCode = failures ? 1 : 0;
});
}
+94
View File
@@ -511,6 +511,100 @@ runner.test('handles invalid thinking type gracefully', () => {
assertExists(thinkingConfig, 'thinkingConfig should exist');
});
// Test 28: Keyword detection - "think"
runner.test('detects "think" keyword (low effort)', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'think about the solution' }
]);
assertEqual(result.thinking, true, 'thinking should be enabled');
assertEqual(result.effort, 'low', 'effort should be low');
assertEqual(result.keyword, 'think', 'keyword should be "think"');
});
// Test 29: Keyword detection - "think hard"
runner.test('detects "think hard" keyword (medium effort)', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'think hard about edge cases' }
]);
assertEqual(result.thinking, true, 'thinking should be enabled');
assertEqual(result.effort, 'medium', 'effort should be medium');
assertEqual(result.keyword, 'think hard', 'keyword should be "think hard"');
});
// Test 30: Keyword detection - "think harder"
runner.test('detects "think harder" keyword (high effort)', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'think harder about edge cases' }
]);
assertEqual(result.thinking, true, 'thinking should be enabled');
assertEqual(result.effort, 'high', 'effort should be high');
assertEqual(result.keyword, 'think harder', 'keyword should be "think harder"');
});
// Test 31: Keyword detection - "ultrathink"
runner.test('detects "ultrathink" keyword (max effort)', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'ultrathink this complex problem' }
]);
assertEqual(result.thinking, true, 'thinking should be enabled');
assertEqual(result.effort, 'max', 'effort should be max');
assertEqual(result.keyword, 'ultrathink', 'keyword should be "ultrathink"');
});
// Test 32: Keyword detection - ignores "thinking" (not exact match)
runner.test('ignores "thinking" word (not exact match)', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'I am thinking about the solution' }
]);
assertEqual(result, null, 'should return null for non-exact match');
});
// Test 33: Keyword detection - returns null when no keywords
runner.test('returns null when no keywords present', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'fix the bug quickly' }
]);
assertEqual(result, null, 'should return null when no keywords found');
});
// Test 34: Keyword priority - ultrathink wins when multiple keywords present
runner.test('ultrathink has highest priority when multiple keywords present', () => {
const transformer = new GlmtTransformer();
const result = transformer._detectThinkKeywords([
{ role: 'user', content: 'think hard and think harder, or maybe ultrathink about this' }
]);
assertEqual(result.thinking, true, 'thinking should be enabled');
assertEqual(result.effort, 'max', 'ultrathink should win with max effort');
assertEqual(result.keyword, 'ultrathink', 'keyword should be ultrathink');
});
// Test 35: Keyword detection integrates with transformRequest
runner.test('keyword detection triggers thinking in transformRequest', () => {
const transformer = new GlmtTransformer();
const input = {
model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'think hard about this architecture problem' }]
};
const { thinkingConfig } = transformer.transformRequest(input);
assertEqual(thinkingConfig.thinking, true, 'keyword should enable thinking');
assertEqual(thinkingConfig.effort, 'medium', 'keyword should set medium effort');
});
// Run tests
runner.run().then(success => {
process.exit(success ? 0 : 1);
-459
View File
@@ -1,459 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* TaskClassifier Unit Tests
*
* Tests 3 scenarios:
* 1. Reasoning prompt ("plan architecture") → 'reasoning' classification
* 2. Execution prompt ("fix bug") → 'execution' classification
* 3. Mixed prompt ("analyze and fix") → 'mixed' classification
*/
const assert = require('assert');
const TaskClassifier = require('../../../bin/glmt/task-classifier');
describe('TaskClassifier', () => {
describe('Scenario 1: Reasoning tasks', () => {
it('should classify "plan architecture" as reasoning', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Plan a microservices architecture' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should classify "design system" as reasoning', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Design a database schema for e-commerce' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should classify "analyze performance" as reasoning', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Analyze the performance bottlenecks' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should detect multiple reasoning keywords', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Evaluate different approaches and recommend the best strategy' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should classify research tasks as reasoning', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Research best practices for API authentication' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should handle case-insensitive reasoning keywords', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'PLAN THE ARCHITECTURE' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should detect reasoning in array content', () => {
const classifier = new TaskClassifier();
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: 'Consider the pros and cons of GraphQL vs REST' }
]
}
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
});
describe('Scenario 2: Execution tasks', () => {
it('should classify "fix bug" as execution', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Fix the bug in login.js' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should classify "implement feature" as execution', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Implement user authentication' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should classify "debug issue" as execution', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Debug the memory leak in worker.js' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should classify "refactor code" as execution', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Refactor the database queries' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should detect multiple execution keywords', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Add validation and update the form component' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should handle case-insensitive execution keywords', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'FIX THE BUG IN AUTH MODULE' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should classify test tasks as execution', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Run the integration tests' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
it('should detect execution in array content', () => {
const classifier = new TaskClassifier();
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: 'Create a new API endpoint for users' }
]
}
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'execution');
});
});
describe('Scenario 3: Mixed or ambiguous tasks', () => {
it('should classify "analyze and fix" as mixed (tied scores)', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Analyze the issue and fix it' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
it('should classify tasks with equal reasoning and execution keywords as mixed', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Design the API structure and implement it' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
it('should classify tasks with no keywords as mixed', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Help me with the code' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
it('should classify empty content as mixed', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: '' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
it('should return mixed for empty messages array', () => {
const classifier = new TaskClassifier();
const messages = [];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
it('should return mixed when no user messages exist', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'assistant', content: 'Hello!' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
it('should handle ambiguous prompts', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'What should I do about the authentication?' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'mixed');
});
});
describe('classifyWithDetails method', () => {
it('should return detailed classification for reasoning task', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Plan and design the system architecture' }
];
const result = classifier.classifyWithDetails(messages);
assert.strictEqual(result.type, 'reasoning');
assert.ok(result.reasoningScore > 0);
assert.ok(result.reasoningScore > result.executionScore);
assert.ok(result.textLength > 0);
assert.ok(result.textPreview.includes('plan'));
});
it('should return detailed classification for execution task', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Fix the bug and run tests' }
];
const result = classifier.classifyWithDetails(messages);
assert.strictEqual(result.type, 'execution');
assert.ok(result.executionScore > 0);
assert.ok(result.executionScore > result.reasoningScore);
assert.ok(result.textLength > 0);
});
it('should show scores for mixed task', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Evaluate the options and implement the best one' }
];
const result = classifier.classifyWithDetails(messages);
assert.strictEqual(result.type, 'mixed');
assert.strictEqual(result.reasoningScore, result.executionScore);
});
it('should truncate long text in preview', () => {
const classifier = new TaskClassifier();
const longText = 'a'.repeat(200);
const messages = [
{ role: 'user', content: longText }
];
const result = classifier.classifyWithDetails(messages);
assert.strictEqual(result.textPreview.length, 103); // 100 + '...'
assert.ok(result.textPreview.endsWith('...'));
});
});
describe('Edge cases and special scenarios', () => {
it('should handle multiple user messages', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Plan the architecture' },
{ role: 'assistant', content: 'Here is a plan...' },
{ role: 'user', content: 'Implement it' }
];
const result = classifier.classify(messages);
// ACTUAL BEHAVIOR: Combines both user messages: "plan the architecture implement it"
// "plan" matches reasoning keyword, "implement" matches execution keyword
// Score: reasoning=2 (plan, architecture), execution=1 (implement)
// Result: reasoning wins
assert.strictEqual(result, 'reasoning'); // Changed from 'mixed'
});
it('should handle word boundary matching', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Update the replanning module' } // "plan" in "replanning"
];
const result = classifier.classify(messages);
// Should not match "plan" in "replanning" due to word boundary
assert.strictEqual(result, 'execution'); // Only "update" should match
});
it('should handle custom keywords', () => {
const classifier = new TaskClassifier({
customKeywords: {
reasoning: ['brainstorm', 'strategize'],
execution: ['deploy', 'ship']
}
});
const messages1 = [{ role: 'user', content: 'Brainstorm ideas' }];
const messages2 = [{ role: 'user', content: 'Deploy to production' }];
assert.strictEqual(classifier.classify(messages1), 'reasoning');
assert.strictEqual(classifier.classify(messages2), 'execution');
});
it('should extract text from multiple content blocks', () => {
const classifier = new TaskClassifier();
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: 'Plan the' },
{ type: 'text', text: 'architecture' }
]
}
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should ignore non-text content blocks', () => {
const classifier = new TaskClassifier();
const messages = [
{
role: 'user',
content: [
{ type: 'image', source: 'data:...' },
{ type: 'text', text: 'Analyze this screenshot' }
]
}
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning');
});
it('should handle special characters in keywords', () => {
const classifier = new TaskClassifier();
const messages = [
{ role: 'user', content: 'Think about the pros and cons' }
];
const result = classifier.classify(messages);
assert.strictEqual(result, 'reasoning'); // "think about" and "pros and cons" match
});
});
describe('Real-world prompts', () => {
const testCases = [
{ prompt: 'Create a React component for user profile', expected: 'execution' },
{ prompt: 'What is the best approach for state management?', expected: 'reasoning' },
{ prompt: 'Compare Redux vs MobX', expected: 'reasoning' },
{ prompt: 'Add error handling to the API', expected: 'execution' },
{ prompt: 'Investigate why the tests are failing', expected: 'reasoning' },
{ prompt: 'Optimize database queries', expected: 'execution' },
{ prompt: 'Review the security implications', expected: 'reasoning' },
{ prompt: 'Build and deploy the application', expected: 'execution' },
{ prompt: 'Should I use TypeScript or JavaScript?', expected: 'mixed' },
{ prompt: 'Help me understand this code', expected: 'mixed' }
];
testCases.forEach(({ prompt, expected }) => {
it(`should classify "${prompt}" as ${expected}`, () => {
const classifier = new TaskClassifier();
const messages = [{ role: 'user', content: prompt }];
const result = classifier.classify(messages);
assert.strictEqual(result, expected);
});
});
});
});
// Run tests if executed directly
if (require.main === module) {
const Mocha = require('mocha');
const mocha = new Mocha({ reporter: 'spec' });
mocha.suite.emit('pre-require', global, null, mocha);
// Load this test file
require(module.filename);
mocha.run(failures => {
process.exitCode = failures ? 1 : 0;
});
}