chore: update to v3.4.6 with reasoning enforcer and GLMT improvements

This commit is contained in:
kaitranntt
2025-11-12 00:19:17 -05:00
parent 7bb2fb92c8
commit 844baa997a
11 changed files with 514 additions and 11 deletions
+18
View File
@@ -2,6 +2,24 @@
Format: [Keep a Changelog](https://keepachangelog.com/)
## [3.4.6] - 2025-11-12
### Added
- GLMT ReasoningEnforcer: Prompt injection + API params hybrid (4 effort levels, always enabled)
### Changed
- Added GLMT production warnings (NOT PRODUCTION READY)
- Streamlined CLAUDE.md (-337 lines)
- Simplified GLMT controls: 4 mechanisms → 3 automatic
- Locale + reasoning enforcement now always enabled
### Removed
- GLMT Budget Calculator mechanism (consolidated into automatic controls)
- Deprecated GLMT environment variables (`CCS_GLMT_FORCE_ENGLISH`, `CCS_GLMT_THINKING_BUDGET`, `CCS_GLMT_STREAMING`)
- Outdated test scenarios for removed environment variables
---
## [3.4.5] - 2025-11-11
### Fixed
+11 -4
View File
@@ -32,14 +32,21 @@ CLI wrapper for instant switching between multiple Claude accounts and alternati
- `bin/glmt/glmt-proxy.js`: HTTP proxy server with streaming + auto-fallback
- `bin/glmt/glmt-transformer.js`: Format conversion + delta handling + tool transformation
- `bin/glmt/locale-enforcer.js`: Enforces English output
- `bin/glmt/reasoning-enforcer.js`: Injects explicit reasoning instructions (hybrid approach)
- `bin/glmt/sse-parser.js`: SSE stream parser
- `bin/glmt/delta-accumulator.js`: State tracking for streaming + tool calls
- `tests/unit/glmt/glmt-transformer.test.js`: Unit tests (35 tests passing)
- `tests/unit/glmt/reasoning-enforcer.test.js`: ReasoningEnforcer unit tests (15 tests passing)
**Thinking control mechanisms**:
- Keywords: `think`, `think hard`, `think harder`, `ultrathink`
- Tags: `<Thinking:On|Off>`, `<Effort:Low|Medium|High>`
- Precedence: CLI parameter > message tags > keywords
**Reasoning control mechanisms (hybrid approach)**:
- **Keywords**: `think`, `think hard`, `think harder`, `ultrathink`
- **Tags**: `<Thinking:On|Off>`, `<Effort:Low|Medium|High>`
- **Precedence**: CLI parameter > message tags > keywords
- **Hybrid mode**: Uses BOTH API parameters (`reasoning: true`) AND prompt injection
- API params: Native Z.AI support (deterministic, zero overhead)
- Prompt injection: Explicit format instructions using `<reasoning_content>` tags
- ReasoningEnforcer has 4 effort-aware prompts (low/medium/high/max)
- **Enabled by default** for all GLMT usage (always active)
**Security limits** (DoS protection):
- SSE buffer: 1MB max
+1 -1
View File
@@ -1 +1 @@
3.4.5
3.4.6
+13 -1
View File
@@ -8,6 +8,7 @@ const os = require('os');
const SSEParser = require('./sse-parser');
const DeltaAccumulator = require('./delta-accumulator');
const LocaleEnforcer = require('./locale-enforcer');
const ReasoningEnforcer = require('./reasoning-enforcer');
/**
* GlmtTransformer - Convert between Anthropic and OpenAI formats with thinking and tool support
@@ -54,6 +55,11 @@ class GlmtTransformer {
// Initialize locale enforcer (always enforce English)
this.localeEnforcer = new LocaleEnforcer();
// Initialize reasoning enforcer (enabled by default for all GLMT usage)
this.reasoningEnforcer = new ReasoningEnforcer({
enabled: config.explicitReasoning ?? true
});
}
/**
@@ -104,10 +110,16 @@ class GlmtTransformer {
anthropicRequest.messages || []
);
// 4.5. Inject reasoning instruction (if enabled or thinking requested)
const messagesWithReasoning = this.reasoningEnforcer.injectInstruction(
messagesWithLocale,
thinkingConfig
);
// 5. Convert to OpenAI format
const openaiRequest = {
model: glmModel,
messages: this._sanitizeMessages(messagesWithLocale),
messages: this._sanitizeMessages(messagesWithReasoning),
max_tokens: this._getMaxTokens(glmModel),
stream: anthropicRequest.stream ?? false
};
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env node
'use strict';
/**
* ReasoningEnforcer - Inject explicit reasoning instructions into prompts
*
* Purpose: Force GLM models to use structured reasoning output format (<reasoning_content>)
* This complements API parameters (reasoning: true) with explicit prompt instructions.
*
* Usage:
* const enforcer = new ReasoningEnforcer({ enabled: true });
* const modifiedMessages = enforcer.injectInstruction(messages, thinkingConfig);
*
* Strategy:
* 1. If system prompt exists: Prepend reasoning instruction
* 2. If no system prompt: Prepend to first user message
* 3. Select prompt template based on effort level (low/medium/high/max)
* 4. Preserve message structure (string vs array content)
*/
class ReasoningEnforcer {
constructor(options = {}) {
this.enabled = options.enabled ?? false; // Opt-in by default
this.prompts = options.prompts || this._getDefaultPrompts();
}
/**
* Inject reasoning instruction into messages
* @param {Array} messages - Messages array to modify
* @param {Object} thinkingConfig - { thinking: boolean, effort: string }
* @returns {Array} Modified messages array
*/
injectInstruction(messages, thinkingConfig = {}) {
// Only inject if enabled or thinking explicitly requested
if (!this.enabled && !thinkingConfig.thinking) {
return messages;
}
// Clone messages to avoid mutation
const modifiedMessages = JSON.parse(JSON.stringify(messages));
// Select prompt based on effort level
const prompt = this._selectPrompt(thinkingConfig.effort || 'medium');
// Strategy 1: Inject into system prompt (preferred)
const systemIndex = modifiedMessages.findIndex(m => m.role === 'system');
if (systemIndex >= 0) {
const systemMsg = modifiedMessages[systemIndex];
if (typeof systemMsg.content === 'string') {
systemMsg.content = `${prompt}\n\n${systemMsg.content}`;
} else if (Array.isArray(systemMsg.content)) {
systemMsg.content.unshift({
type: 'text',
text: prompt
});
}
return modifiedMessages;
}
// Strategy 2: Prepend to first user message
const userIndex = modifiedMessages.findIndex(m => m.role === 'user');
if (userIndex >= 0) {
const userMsg = modifiedMessages[userIndex];
if (typeof userMsg.content === 'string') {
userMsg.content = `${prompt}\n\n${userMsg.content}`;
} else if (Array.isArray(userMsg.content)) {
userMsg.content.unshift({
type: 'text',
text: prompt
});
}
return modifiedMessages;
}
// No system or user messages found (edge case)
return modifiedMessages;
}
/**
* Select prompt template based on effort level
* @param {string} effort - 'low', 'medium', 'high', or 'max'
* @returns {string} Prompt template
* @private
*/
_selectPrompt(effort) {
const normalizedEffort = effort.toLowerCase();
return this.prompts[normalizedEffort] || this.prompts.medium;
}
/**
* Get default prompt templates
* @returns {Object} Map of effort levels to prompts
* @private
*/
_getDefaultPrompts() {
return {
low: `You are an expert reasoning model using GLM-4.6 architecture.
CRITICAL: Before answering, write 2-3 sentences of reasoning in <reasoning_content> tags.
OUTPUT FORMAT:
<reasoning_content>
(Brief analysis: what is the problem? what's the approach?)
</reasoning_content>
(Write your final answer here)`,
medium: `You are an expert reasoning model using GLM-4.6 architecture.
CRITICAL REQUIREMENTS:
1. Always think step-by-step before answering
2. Write your reasoning process explicitly in <reasoning_content> tags
3. Never skip your chain of thought, even for simple problems
OUTPUT FORMAT:
<reasoning_content>
(Write your detailed thinking here: analyze the problem, explore approaches,
evaluate trade-offs, and arrive at a conclusion)
</reasoning_content>
(Write your final answer here based on your reasoning above)`,
high: `You are an expert reasoning model using GLM-4.6 architecture.
CRITICAL REQUIREMENTS:
1. Think deeply and systematically before answering
2. Write comprehensive reasoning in <reasoning_content> tags
3. Explore multiple approaches and evaluate trade-offs
4. Show all steps in your problem-solving process
OUTPUT FORMAT:
<reasoning_content>
(Write exhaustive analysis here:
- Problem decomposition
- Multiple approach exploration
- Trade-off analysis for each approach
- Edge case consideration
- Final conclusion with justification)
</reasoning_content>
(Write your final answer here based on your systematic reasoning above)`,
max: `You are an expert reasoning model using GLM-4.6 architecture.
CRITICAL REQUIREMENTS:
1. Think exhaustively from first principles
2. Write extremely detailed reasoning in <reasoning_content> tags
3. Analyze ALL possible angles, approaches, and edge cases
4. Challenge your own assumptions and explore alternatives
5. Provide rigorous justification for every claim
OUTPUT FORMAT:
<reasoning_content>
(Write comprehensive analysis here:
- First principles breakdown
- Exhaustive approach enumeration
- Comparative analysis of all approaches
- Edge case and failure mode analysis
- Assumption validation
- Counter-argument consideration
- Final conclusion with rigorous justification)
</reasoning_content>
(Write your final answer here based on your exhaustive reasoning above)`
};
}
}
module.exports = ReasoningEnforcer;
+1 -1
View File
@@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (irm | iex)
# For git installations, VERSION file is read if available
$CcsVersion = "3.4.5"
$CcsVersion = "3.4.6"
# Try to read VERSION file for git installations
if ($ScriptDir) {
+1 -1
View File
@@ -32,7 +32,7 @@ fi
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (curl | bash)
# For git installations, VERSION file is read if available
CCS_VERSION="3.4.5"
CCS_VERSION="3.4.6"
# Try to read VERSION file for git installations
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
# Version (updated by scripts/bump-version.sh)
CCS_VERSION="3.4.5"
CCS_VERSION="3.4.6"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
+1 -1
View File
@@ -12,7 +12,7 @@ param(
$ErrorActionPreference = "Stop"
# Version (updated by scripts/bump-version.sh)
$CcsVersion = "3.4.5"
$CcsVersion = "3.4.6"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
$ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "3.4.5",
"version": "3.4.6",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env node
'use strict';
/**
* ReasoningEnforcer Unit Tests
*
* Test scenarios:
* 1. Opt-in behavior (enabled vs disabled)
* 2. System message injection
* 3. User message fallback
* 4. Effort level selection (low/medium/high/max)
* 5. Message structure handling (string vs array)
* 6. Edge cases (empty messages, no system/user)
*/
const ReasoningEnforcer = require('../../../bin/glmt/reasoning-enforcer');
/**
* Simple test runner (no external dependencies)
*/
class TestRunner {
constructor() {
this.tests = [];
this.passed = 0;
this.failed = 0;
}
test(name, fn) {
this.tests.push({ name, fn });
}
async run() {
console.log('\n=== ReasoningEnforcer Tests ===\n');
for (const { name, fn } of this.tests) {
try {
await fn();
console.log(`[OK] ${name}`);
this.passed++;
} catch (error) {
console.error(`[X] ${name}`);
console.error(` Error: ${error.message}`);
this.failed++;
}
}
console.log(`\n=== Results ===`);
console.log(`Passed: ${this.passed}/${this.tests.length}`);
console.log(`Failed: ${this.failed}/${this.tests.length}`);
return this.failed === 0;
}
}
/**
* Simple assertion helpers
*/
function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(
`${message || 'Assertion failed'}\n` +
` Expected: ${JSON.stringify(expected)}\n` +
` Actual: ${JSON.stringify(actual)}`
);
}
}
function assertTrue(condition, message) {
if (!condition) {
throw new Error(message || 'Expected condition to be true');
}
}
function assertIncludes(haystack, needle, message) {
if (!haystack.includes(needle)) {
throw new Error(
`${message || 'String does not include expected substring'}\n` +
` Expected substring: ${needle}\n` +
` Actual string: ${haystack.substring(0, 100)}...`
);
}
}
function assertDeepEqual(actual, expected, message) {
const actualStr = JSON.stringify(actual);
const expectedStr = JSON.stringify(expected);
if (actualStr !== expectedStr) {
throw new Error(
`${message || 'Deep equality assertion failed'}\n` +
` Expected: ${expectedStr}\n` +
` Actual: ${actualStr}`
);
}
}
// Create test runner
const runner = new TestRunner();
// Test 1: Opt-in behavior - disabled
runner.test('should NOT inject when disabled and thinking=false', () => {
const enforcer = new ReasoningEnforcer({ enabled: false });
const messages = [
{ role: 'user', content: 'What is 2+2?' }
];
const result = enforcer.injectInstruction(messages, { thinking: false });
assertEqual(result.length, 1);
assertEqual(result[0].content, 'What is 2+2?');
});
// Test 2: Opt-in behavior - enabled
runner.test('should inject when enabled=true', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [
{ role: 'user', content: 'What is 2+2?' }
];
const result = enforcer.injectInstruction(messages, { thinking: false });
assertIncludes(result[0].content, 'CRITICAL');
assertIncludes(result[0].content, '<reasoning_content>');
});
// Test 3: Opt-in behavior - thinking=true
runner.test('should inject when thinking=true (even if enabled=false)', () => {
const enforcer = new ReasoningEnforcer({ enabled: false });
const messages = [
{ role: 'user', content: 'What is 2+2?' }
];
const result = enforcer.injectInstruction(messages, { thinking: true });
assertIncludes(result[0].content, 'CRITICAL');
});
// Test 4: System message injection - string content
runner.test('should prepend to system message (string content)', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Calculate 2+2' }
];
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'medium' });
assertEqual(result.length, 2);
assertTrue(result[0].content.startsWith('You are an expert reasoning model'));
assertIncludes(result[0].content, 'You are a helpful assistant');
assertEqual(result[1].content, 'Calculate 2+2');
});
// Test 5: System message injection - array content
runner.test('should prepend to system message (array content)', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [
{
role: 'system',
content: [
{ type: 'text', text: 'You are a code assistant.' }
]
},
{ role: 'user', content: 'Write a function' }
];
const result = enforcer.injectInstruction(messages, { thinking: true });
assertTrue(Array.isArray(result[0].content));
assertEqual(result[0].content[0].type, 'text');
assertIncludes(result[0].content[0].text, 'CRITICAL');
assertEqual(result[0].content[1].text, 'You are a code assistant.');
});
// Test 6: User message fallback
runner.test('should prepend to first user message when no system message', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [
{ role: 'user', content: 'Explain quantum computing' }
];
const result = enforcer.injectInstruction(messages, { thinking: true });
assertEqual(result.length, 1);
assertIncludes(result[0].content, 'CRITICAL');
assertIncludes(result[0].content, 'Explain quantum computing');
});
// Test 7: Effort level - low
runner.test('should use low prompt template', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [{ role: 'user', content: 'Test' }];
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'low' });
assertIncludes(result[0].content.toLowerCase(), 'brief analysis');
});
// Test 8: Effort level - medium
runner.test('should use medium prompt template', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [{ role: 'user', content: 'Test' }];
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'medium' });
assertIncludes(result[0].content.toLowerCase(), 'think step-by-step');
});
// Test 9: Effort level - high
runner.test('should use high prompt template', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [{ role: 'user', content: 'Test' }];
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'high' });
assertIncludes(result[0].content.toLowerCase(), 'think deeply and systematically');
});
// Test 10: Effort level - max
runner.test('should use max prompt template', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [{ role: 'user', content: 'Test' }];
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'max' });
assertIncludes(result[0].content.toLowerCase(), 'exhaustively from first principles');
});
// Test 11: Default effort level
runner.test('should default to medium effort if not specified', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [{ role: 'user', content: 'Test' }];
const result = enforcer.injectInstruction(messages, { thinking: true });
assertIncludes(result[0].content, 'think step-by-step');
});
// Test 12: Empty messages array
runner.test('should handle empty messages array', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [];
const result = enforcer.injectInstruction(messages, { thinking: true });
assertEqual(result.length, 0);
});
// Test 13: No system or user role
runner.test('should handle messages with no system or user role', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const messages = [
{ role: 'assistant', content: 'Previous response' }
];
const result = enforcer.injectInstruction(messages, { thinking: true });
assertEqual(result.length, 1);
assertEqual(result[0].content, 'Previous response');
});
// Test 14: Immutability
runner.test('should not mutate original messages array', () => {
const enforcer = new ReasoningEnforcer({ enabled: true });
const originalMessages = [
{ role: 'user', content: 'Test prompt' }
];
const originalCopy = JSON.parse(JSON.stringify(originalMessages));
enforcer.injectInstruction(originalMessages, { thinking: true });
assertDeepEqual(originalMessages, originalCopy);
});
// Test 15: Custom prompts
runner.test('should handle custom prompts via constructor', () => {
const customPrompts = {
low: 'Custom low prompt',
medium: 'Custom medium prompt',
high: 'Custom high prompt',
max: 'Custom max prompt'
};
const enforcer = new ReasoningEnforcer({ enabled: true, prompts: customPrompts });
const messages = [{ role: 'user', content: 'Test' }];
const result = enforcer.injectInstruction(messages, { thinking: true, effort: 'low' });
assertIncludes(result[0].content, 'Custom low prompt');
});
// Run all tests
runner.run().then(success => {
process.exit(success ? 0 : 1);
});