From d3e6be171f82c8aa73b149688d46efcec196dda7 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 11 Nov 2025 18:14:05 -0500 Subject: [PATCH] refactor(glmt)!: simplify with keyword-based thinking control (v3.4.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 26 ++ CLAUDE.md | 91 ++--- VERSION | 2 +- bin/glmt/budget-calculator.js | 114 ------ bin/glmt/glmt-proxy.js | 25 +- bin/glmt/glmt-transformer.js | 95 +++-- bin/glmt/locale-enforcer.js | 12 +- bin/glmt/task-classifier.js | 162 -------- installers/install.ps1 | 2 +- installers/install.sh | 2 +- lib/ccs | 2 +- lib/ccs.ps1 | 2 +- package.json | 2 +- tests/unit/glmt/budget-calculator.test.js | 338 ---------------- tests/unit/glmt/glmt-transformer.test.js | 94 +++++ tests/unit/glmt/task-classifier.test.js | 459 ---------------------- 16 files changed, 251 insertions(+), 1177 deletions(-) delete mode 100644 bin/glmt/budget-calculator.js delete mode 100644 bin/glmt/task-classifier.js delete mode 100644 tests/unit/glmt/budget-calculator.test.js delete mode 100644 tests/unit/glmt/task-classifier.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b2e1fb..50fe6d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index b87e3020..952b9393 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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): - `` - Enable/disable reasoning -- `` - Control reasoning depth (deprecated - Z.AI only supports binary thinking) +- `` - 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 `` 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 diff --git a/VERSION b/VERSION index 4d9d11cf..6cb9d3dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.2 +3.4.3 diff --git a/bin/glmt/budget-calculator.js b/bin/glmt/budget-calculator.js deleted file mode 100644 index aecaa645..00000000 --- a/bin/glmt/budget-calculator.js +++ /dev/null @@ -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; diff --git a/bin/glmt/glmt-proxy.js b/bin/glmt/glmt-proxy.js index 1e7dd8ef..2e428f84 100644 --- a/bin/glmt/glmt-proxy.js +++ b/bin/glmt/glmt-proxy.js @@ -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); } diff --git a/bin/glmt/glmt-transformer.js b/bin/glmt/glmt-transformer.js index 8bfe453d..d2c5c4de 100644 --- a/bin/glmt/glmt-transformer.js +++ b/bin/glmt/glmt-transformer.js @@ -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'); * - 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 diff --git a/bin/glmt/locale-enforcer.js b/bin/glmt/locale-enforcer.js index 758c52dc..581a1f8e 100644 --- a/bin/glmt/locale-enforcer.js +++ b/bin/glmt/locale-enforcer.js @@ -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)); diff --git a/bin/glmt/task-classifier.js b/bin/glmt/task-classifier.js deleted file mode 100644 index 97a17a50..00000000 --- a/bin/glmt/task-classifier.js +++ /dev/null @@ -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; diff --git a/installers/install.ps1 b/installers/install.ps1 index 8994d66d..6034a851 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -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) { diff --git a/installers/install.sh b/installers/install.sh index aa0b98ee..19896065 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -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 diff --git a/lib/ccs b/lib/ccs index 053aa2af..5a35d470 100755 --- a/lib/ccs +++ b/lib/ccs @@ -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" diff --git a/lib/ccs.ps1 b/lib/ccs.ps1 index 129af457..31cdc47b 100644 --- a/lib/ccs.ps1 +++ b/lib/ccs.ps1 @@ -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" diff --git a/package.json b/package.json index 88190d93..f36cf686 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/unit/glmt/budget-calculator.test.js b/tests/unit/glmt/budget-calculator.test.js deleted file mode 100644 index f39121b8..00000000 --- a/tests/unit/glmt/budget-calculator.test.js +++ /dev/null @@ -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; - }); -} diff --git a/tests/unit/glmt/glmt-transformer.test.js b/tests/unit/glmt/glmt-transformer.test.js index f8c022f1..e4744c90 100644 --- a/tests/unit/glmt/glmt-transformer.test.js +++ b/tests/unit/glmt/glmt-transformer.test.js @@ -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); diff --git a/tests/unit/glmt/task-classifier.test.js b/tests/unit/glmt/task-classifier.test.js deleted file mode 100644 index 32dc6a8a..00000000 --- a/tests/unit/glmt/task-classifier.test.js +++ /dev/null @@ -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; - }); -}