From 844baa997aab02204a8282e1b6e5bebace0c80e7 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 12 Nov 2025 00:19:17 -0500 Subject: [PATCH] chore: update to v3.4.6 with reasoning enforcer and GLMT improvements --- CHANGELOG.md | 18 ++ CLAUDE.md | 15 +- VERSION | 2 +- bin/glmt/glmt-transformer.js | 14 +- bin/glmt/reasoning-enforcer.js | 173 ++++++++++++ installers/install.ps1 | 2 +- installers/install.sh | 2 +- lib/ccs | 2 +- lib/ccs.ps1 | 2 +- package.json | 2 +- tests/unit/glmt/reasoning-enforcer.test.js | 293 +++++++++++++++++++++ 11 files changed, 514 insertions(+), 11 deletions(-) create mode 100644 bin/glmt/reasoning-enforcer.js create mode 100644 tests/unit/glmt/reasoning-enforcer.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ed974a..16aa4841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 3b6a6f5b..b040965e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: ``, `` -- Precedence: CLI parameter > message tags > keywords +**Reasoning control mechanisms (hybrid approach)**: +- **Keywords**: `think`, `think hard`, `think harder`, `ultrathink` +- **Tags**: ``, `` +- **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 `` 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 diff --git a/VERSION b/VERSION index 4f5e6973..1cf82530 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.5 +3.4.6 diff --git a/bin/glmt/glmt-transformer.js b/bin/glmt/glmt-transformer.js index 5df2ca95..6878b5dc 100644 --- a/bin/glmt/glmt-transformer.js +++ b/bin/glmt/glmt-transformer.js @@ -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 }; diff --git a/bin/glmt/reasoning-enforcer.js b/bin/glmt/reasoning-enforcer.js new file mode 100644 index 00000000..c589760c --- /dev/null +++ b/bin/glmt/reasoning-enforcer.js @@ -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 () + * 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 tags. + +OUTPUT FORMAT: + +(Brief analysis: what is the problem? what's the approach?) + + +(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 tags +3. Never skip your chain of thought, even for simple problems + +OUTPUT FORMAT: + +(Write your detailed thinking here: analyze the problem, explore approaches, +evaluate trade-offs, and arrive at a conclusion) + + +(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 tags +3. Explore multiple approaches and evaluate trade-offs +4. Show all steps in your problem-solving process + +OUTPUT FORMAT: + +(Write exhaustive analysis here: + - Problem decomposition + - Multiple approach exploration + - Trade-off analysis for each approach + - Edge case consideration + - Final conclusion with justification) + + +(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 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: + +(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) + + +(Write your final answer here based on your exhaustive reasoning above)` + }; + } +} + +module.exports = ReasoningEnforcer; diff --git a/installers/install.ps1 b/installers/install.ps1 index 74155b50..7ba5f7db 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or ( # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (irm | iex) # For git installations, VERSION file is read if available -$CcsVersion = "3.4.5" +$CcsVersion = "3.4.6" # Try to read VERSION file for git installations if ($ScriptDir) { diff --git a/installers/install.sh b/installers/install.sh index 7629e10d..1cf386e7 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -32,7 +32,7 @@ fi # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (curl | bash) # For git installations, VERSION file is read if available -CCS_VERSION="3.4.5" +CCS_VERSION="3.4.6" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then diff --git a/lib/ccs b/lib/ccs index e5536bbe..105656ce 100755 --- a/lib/ccs +++ b/lib/ccs @@ -2,7 +2,7 @@ set -euo pipefail # Version (updated by scripts/bump-version.sh) -CCS_VERSION="3.4.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" diff --git a/lib/ccs.ps1 b/lib/ccs.ps1 index 38cfac90..46706817 100644 --- a/lib/ccs.ps1 +++ b/lib/ccs.ps1 @@ -12,7 +12,7 @@ param( $ErrorActionPreference = "Stop" # Version (updated by scripts/bump-version.sh) -$CcsVersion = "3.4.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" diff --git a/package.json b/package.json index 4c91de06..13a1001d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/unit/glmt/reasoning-enforcer.test.js b/tests/unit/glmt/reasoning-enforcer.test.js new file mode 100644 index 00000000..1d3b4cb6 --- /dev/null +++ b/tests/unit/glmt/reasoning-enforcer.test.js @@ -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, ''); +}); + +// 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); +});