@kaitranntt/ccs 3.4.1 → 3.4.3

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