mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user