mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 22:17:20 +00:00
feat: add GLMT proxy and transformer tools
- Add GLMT proxy server for GLM model routing - Add GLMT transformer for output format conversion - Update CLI with new proxy and transformer commands - Add comprehensive test suite for new functionality - Update documentation and architecture guides - Bump version and update changelog
This commit is contained in:
1 parent
daf075dc69
commit
657f99bfe8
21 files changed
+2597
-347
No files matched your search
Executable
+245
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const GlmtTransformer = require('../bin/glmt-transformer');
|
||||
|
||||
/**
|
||||
* Manual test for debug mode file logging
|
||||
*/
|
||||
|
||||
const logDir = path.join(os.homedir(), '.ccs', 'logs');
|
||||
|
||||
// Clean up any existing logs
|
||||
console.log('Cleaning up existing logs...');
|
||||
if (fs.existsSync(logDir)) {
|
||||
fs.rmSync(logDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log('\n=== Test 1: Debug Mode OFF (default) ===');
|
||||
{
|
||||
const transformer = new GlmtTransformer({ verbose: false });
|
||||
console.log(`Debug logging: ${transformer.debugLog}`);
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test without debug' }]
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
|
||||
const openaiResponse = {
|
||||
id: 'test-1',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Response without debug',
|
||||
reasoning_content: 'Some reasoning...'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
|
||||
};
|
||||
|
||||
transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
// Check that no logs were created
|
||||
const logExists = fs.existsSync(logDir);
|
||||
console.log(`Log directory exists: ${logExists}`);
|
||||
if (logExists) {
|
||||
console.log('ERROR: Logs created when debug mode is OFF!');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✓ No logs created (correct)');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Test 2: Debug Mode ON (via config) ===');
|
||||
{
|
||||
const transformer = new GlmtTransformer({ verbose: true, debugLog: true });
|
||||
console.log(`Debug logging: ${transformer.debugLog}`);
|
||||
console.log(`Log directory: ${transformer.debugLogDir}`);
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test with debug' }]
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
|
||||
const openaiResponse = {
|
||||
id: 'test-2',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Response with debug',
|
||||
reasoning_content: 'Detailed reasoning process for debugging...'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
|
||||
};
|
||||
|
||||
transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
// Check that logs were created
|
||||
const logExists = fs.existsSync(logDir);
|
||||
console.log(`\nLog directory exists: ${logExists}`);
|
||||
|
||||
if (!logExists) {
|
||||
console.log('ERROR: No logs created when debug mode is ON!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(logDir);
|
||||
console.log(`Files created: ${files.length}`);
|
||||
console.log(`Files:`, files);
|
||||
|
||||
if (files.length !== 4) {
|
||||
console.log(`ERROR: Expected 4 files, got ${files.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check file names
|
||||
const hasRequestAnthropic = files.some(f => f.includes('request-anthropic'));
|
||||
const hasRequestOpenai = files.some(f => f.includes('request-openai'));
|
||||
const hasResponseOpenai = files.some(f => f.includes('response-openai'));
|
||||
const hasResponseAnthropic = files.some(f => f.includes('response-anthropic'));
|
||||
|
||||
console.log(`\nFile types found:`);
|
||||
console.log(` request-anthropic: ${hasRequestAnthropic ? '✓' : '✗'}`);
|
||||
console.log(` request-openai: ${hasRequestOpenai ? '✓' : '✗'}`);
|
||||
console.log(` response-openai: ${hasResponseOpenai ? '✓' : '✗'}`);
|
||||
console.log(` response-anthropic: ${hasResponseAnthropic ? '✓' : '✗'}`);
|
||||
|
||||
if (!hasRequestAnthropic || !hasRequestOpenai || !hasResponseOpenai || !hasResponseAnthropic) {
|
||||
console.log('ERROR: Missing expected file types!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check file contents
|
||||
const responseOpenaiFile = files.find(f => f.includes('response-openai'));
|
||||
const responseOpenaiPath = path.join(logDir, responseOpenaiFile);
|
||||
const responseOpenaiData = JSON.parse(fs.readFileSync(responseOpenaiPath, 'utf8'));
|
||||
|
||||
console.log(`\nChecking response-openai.json for reasoning_content...`);
|
||||
if (responseOpenaiData.choices[0].message.reasoning_content) {
|
||||
console.log('✓ reasoning_content found in response-openai.json');
|
||||
console.log(` Length: ${responseOpenaiData.choices[0].message.reasoning_content.length} chars`);
|
||||
} else {
|
||||
console.log('ERROR: reasoning_content NOT found in response-openai.json!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const responseAnthropicFile = files.find(f => f.includes('response-anthropic'));
|
||||
const responseAnthropicPath = path.join(logDir, responseAnthropicFile);
|
||||
const responseAnthropicData = JSON.parse(fs.readFileSync(responseAnthropicPath, 'utf8'));
|
||||
|
||||
console.log(`\nChecking response-anthropic.json for thinking block...`);
|
||||
const thinkingBlock = responseAnthropicData.content.find(b => b.type === 'thinking');
|
||||
if (thinkingBlock) {
|
||||
console.log('✓ Thinking block found in response-anthropic.json');
|
||||
console.log(` Length: ${thinkingBlock.thinking.length} chars`);
|
||||
} else {
|
||||
console.log('ERROR: Thinking block NOT found in response-anthropic.json!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n✓ All checks passed for debug mode ON');
|
||||
}
|
||||
|
||||
console.log('\n=== Test 3: Debug Mode via CCS_DEBUG_LOG=1 ===');
|
||||
{
|
||||
// Clean up
|
||||
fs.rmSync(logDir, { recursive: true, force: true });
|
||||
|
||||
process.env.CCS_DEBUG_LOG = '1';
|
||||
const transformer = new GlmtTransformer({ verbose: false });
|
||||
console.log(`Debug logging: ${transformer.debugLog}`);
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test with env var' }]
|
||||
};
|
||||
|
||||
transformer.transformRequest(input);
|
||||
|
||||
const openaiResponse = {
|
||||
id: 'test-3',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Response',
|
||||
reasoning_content: 'Reasoning...'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
|
||||
};
|
||||
|
||||
transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
const files = fs.readdirSync(logDir);
|
||||
console.log(`Files created: ${files.length}`);
|
||||
|
||||
if (files.length === 4) {
|
||||
console.log('✓ Debug mode enabled via CCS_DEBUG_LOG=1');
|
||||
} else {
|
||||
console.log(`ERROR: Expected 4 files, got ${files.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
delete process.env.CCS_DEBUG_LOG;
|
||||
}
|
||||
|
||||
console.log('\n=== Test 4: Error Handling (No Write Permission) ===');
|
||||
{
|
||||
// Create a read-only directory to test error handling
|
||||
const readOnlyDir = path.join(os.tmpdir(), 'ccs-readonly-test');
|
||||
if (fs.existsSync(readOnlyDir)) {
|
||||
fs.chmodSync(readOnlyDir, 0o755);
|
||||
fs.rmSync(readOnlyDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(readOnlyDir, { recursive: true });
|
||||
fs.chmodSync(readOnlyDir, 0o444); // Read-only
|
||||
|
||||
const transformer = new GlmtTransformer({
|
||||
debugLog: true,
|
||||
debugLogDir: readOnlyDir,
|
||||
verbose: false
|
||||
});
|
||||
|
||||
console.log('Testing graceful error handling with read-only directory...');
|
||||
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test error handling' }]
|
||||
};
|
||||
|
||||
try {
|
||||
transformer.transformRequest(input);
|
||||
console.log('✓ No crash on write error (graceful handling)');
|
||||
} catch (error) {
|
||||
console.log('ERROR: Transformer crashed on write error!');
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up
|
||||
fs.chmodSync(readOnlyDir, 0o755);
|
||||
fs.rmSync(readOnlyDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up test logs
|
||||
console.log('\n=== Cleaning up test logs ===');
|
||||
if (fs.existsSync(logDir)) {
|
||||
fs.rmSync(logDir, { recursive: true, force: true });
|
||||
console.log(`Removed ${logDir}`);
|
||||
}
|
||||
|
||||
console.log('\n=== ALL TESTS PASSED ===');
|
||||
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# GLMT Integration Test Suite
|
||||
# Tests proxy startup, configuration, and basic functionality
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CCS_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=== GLMT Integration Test Suite ==="
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
test_pass() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
((PASSED++))
|
||||
}
|
||||
|
||||
test_fail() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
((FAILED++))
|
||||
}
|
||||
|
||||
test_info() {
|
||||
echo -e "${YELLOW}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
# Test 1: Check GLMT profile exists in config
|
||||
echo "Test 1: GLMT profile configuration"
|
||||
if grep -q '"glmt"' ~/.ccs/config.json; then
|
||||
test_pass "GLMT profile found in config.json"
|
||||
else
|
||||
test_fail "GLMT profile NOT found in config.json"
|
||||
fi
|
||||
|
||||
# Test 2: Check GLMT settings file exists
|
||||
echo "Test 2: GLMT settings file"
|
||||
if [ -f ~/.ccs/glmt.settings.json ]; then
|
||||
test_pass "GLMT settings file exists"
|
||||
|
||||
# Check if API key is configured
|
||||
if grep -q "YOUR_GLM_API_KEY_HERE" ~/.ccs/glmt.settings.json; then
|
||||
test_info "API key not configured (still has placeholder)"
|
||||
else
|
||||
test_pass "API key configured"
|
||||
fi
|
||||
else
|
||||
test_fail "GLMT settings file NOT found"
|
||||
fi
|
||||
|
||||
# Test 3: Check transformer module
|
||||
echo "Test 3: Transformer module"
|
||||
if [ -f "$CCS_DIR/bin/glmt-transformer.js" ]; then
|
||||
test_pass "Transformer module exists"
|
||||
|
||||
# Test syntax
|
||||
if node --check "$CCS_DIR/bin/glmt-transformer.js" 2>/dev/null; then
|
||||
test_pass "Transformer syntax valid"
|
||||
else
|
||||
test_fail "Transformer syntax invalid"
|
||||
fi
|
||||
else
|
||||
test_fail "Transformer module NOT found"
|
||||
fi
|
||||
|
||||
# Test 4: Check proxy module
|
||||
echo "Test 4: Proxy module"
|
||||
if [ -f "$CCS_DIR/bin/glmt-proxy.js" ]; then
|
||||
test_pass "Proxy module exists"
|
||||
|
||||
# Test syntax
|
||||
if node --check "$CCS_DIR/bin/glmt-proxy.js" 2>/dev/null; then
|
||||
test_pass "Proxy syntax valid"
|
||||
else
|
||||
test_fail "Proxy syntax invalid"
|
||||
fi
|
||||
else
|
||||
test_fail "Proxy module NOT found"
|
||||
fi
|
||||
|
||||
# Test 5: Test proxy startup
|
||||
echo "Test 5: Proxy startup test"
|
||||
test_info "Starting proxy in background..."
|
||||
|
||||
# Start proxy
|
||||
node "$CCS_DIR/bin/glmt-proxy.js" &
|
||||
PROXY_PID=$!
|
||||
|
||||
# Wait for PROXY_READY signal (with timeout)
|
||||
TIMEOUT=5
|
||||
PORT=""
|
||||
for i in $(seq 1 $TIMEOUT); do
|
||||
if ps -p $PROXY_PID > /dev/null 2>&1; then
|
||||
sleep 0.2
|
||||
# Check if proxy outputted anything (we can't easily capture it in background)
|
||||
if [ $i -eq $TIMEOUT ]; then
|
||||
test_fail "Proxy started but PROXY_READY signal not captured (this is OK - proxy works)"
|
||||
PORT="unknown"
|
||||
fi
|
||||
else
|
||||
test_fail "Proxy failed to start or exited immediately"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ps -p $PROXY_PID > /dev/null 2>&1; then
|
||||
test_pass "Proxy process is running (PID: $PROXY_PID)"
|
||||
|
||||
# Kill proxy
|
||||
kill $PROXY_PID 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
if ! ps -p $PROXY_PID > /dev/null 2>&1; then
|
||||
test_pass "Proxy terminated gracefully"
|
||||
else
|
||||
test_fail "Proxy did not terminate (killing forcefully)"
|
||||
kill -9 $PROXY_PID 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 6: Unit tests
|
||||
echo "Test 6: Unit tests"
|
||||
if [ -f "$CCS_DIR/tests/glmt-transformer.test.js" ]; then
|
||||
test_info "Running transformer unit tests..."
|
||||
if node "$CCS_DIR/tests/glmt-transformer.test.js" | grep -q "Passed: 12/12"; then
|
||||
test_pass "All 12 unit tests passed"
|
||||
else
|
||||
test_fail "Some unit tests failed"
|
||||
fi
|
||||
else
|
||||
test_fail "Unit test file NOT found"
|
||||
fi
|
||||
|
||||
# Test 7: Help text
|
||||
echo "Test 7: Help text verification"
|
||||
if ccs --help | grep -q "ccs glmt"; then
|
||||
test_pass "GLMT appears in help text"
|
||||
else
|
||||
test_fail "GLMT NOT in help text"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=== Test Summary ==="
|
||||
echo -e "Passed: ${GREEN}$PASSED${NC}"
|
||||
echo -e "Failed: ${RED}$FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Test with Claude Code: ccs glmt \"What is 2+2?\""
|
||||
echo " 2. Test thinking tags: ccs glmt \"<Thinking:On> Explain recursion\""
|
||||
echo " 3. Check thinking blocks appear in Claude Code UI"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed. Please review above.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const GlmtTransformer = require('../bin/glmt-transformer');
|
||||
|
||||
/**
|
||||
* 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=== GLM Thinking Transformer Tests ===\n');
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`✓ ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`✗ ${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 assertExists(value, message) {
|
||||
if (value === undefined || value === null) {
|
||||
throw new Error(message || 'Value should exist');
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeepEqual(actual, expected, message) {
|
||||
const actualStr = JSON.stringify(actual);
|
||||
const expectedStr = JSON.stringify(expected);
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
`${message || 'Deep equality failed'}\n` +
|
||||
` Expected: ${expectedStr}\n` +
|
||||
` Actual: ${actualStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test suite
|
||||
const runner = new TestRunner();
|
||||
|
||||
// Test 1: Transform Anthropic request to OpenAI format
|
||||
runner.test('transforms Anthropic request to OpenAI format', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
max_tokens: 4096
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
|
||||
assertEqual(openaiRequest.model, 'GLM-4.6', 'Model should be GLM-4.6');
|
||||
assertEqual(openaiRequest.do_sample, true, 'do_sample should be true');
|
||||
assertEqual(openaiRequest.max_tokens, 128000, 'max_tokens should be 128000');
|
||||
assertExists(openaiRequest.messages, 'messages should exist');
|
||||
});
|
||||
|
||||
// Test 2: Extract thinking control tags
|
||||
runner.test('extracts thinking control tags', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: '<Thinking:On> <Effort:High> Solve this problem'
|
||||
}]
|
||||
};
|
||||
|
||||
const { thinkingConfig } = transformer.transformRequest(input);
|
||||
|
||||
assertEqual(thinkingConfig.thinking, true, 'thinking should be On');
|
||||
assertEqual(thinkingConfig.effort, 'high', 'effort should be high');
|
||||
});
|
||||
|
||||
// Test 3: Extract thinking off tag
|
||||
runner.test('respects Thinking:Off tag', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: '<Thinking:Off> Quick question'
|
||||
}]
|
||||
};
|
||||
|
||||
const { thinkingConfig } = transformer.transformRequest(input);
|
||||
|
||||
assertEqual(thinkingConfig.thinking, false, 'thinking should be Off');
|
||||
});
|
||||
|
||||
// Test 4: Convert reasoning_content to thinking block
|
||||
runner.test('converts reasoning_content to thinking block', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-123',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Here is the answer',
|
||||
reasoning_content: 'Let me think through this problem...'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertEqual(result.content.length, 2, 'Should have 2 content blocks');
|
||||
assertEqual(result.content[0].type, 'thinking', 'First block should be thinking');
|
||||
assertEqual(
|
||||
result.content[0].thinking,
|
||||
'Let me think through this problem...',
|
||||
'Thinking content should match'
|
||||
);
|
||||
assertEqual(result.content[1].type, 'text', 'Second block should be text');
|
||||
assertEqual(
|
||||
result.content[1].text,
|
||||
'Here is the answer',
|
||||
'Text content should match'
|
||||
);
|
||||
});
|
||||
|
||||
// Test 5: Handle response without reasoning_content
|
||||
runner.test('handles response without reasoning_content', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const openaiResponse = {
|
||||
id: 'chatcmpl-123',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Simple answer'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}]
|
||||
};
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, {});
|
||||
|
||||
assertEqual(result.content.length, 1, 'Should have 1 content block');
|
||||
assertEqual(result.content[0].type, 'text', 'Block should be text');
|
||||
assertEqual(result.content[0].text, 'Simple answer', 'Text should match');
|
||||
});
|
||||
|
||||
// Test 6: Thinking signature generation
|
||||
runner.test('generates thinking signature', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const thinking = 'This is my reasoning process';
|
||||
const signature = transformer._generateThinkingSignature(thinking);
|
||||
|
||||
assertExists(signature.type, 'signature.type should exist');
|
||||
assertEqual(signature.type, 'thinking_signature', 'type should be thinking_signature');
|
||||
assertExists(signature.hash, 'signature.hash should exist');
|
||||
assertEqual(signature.hash.length, 16, 'hash should be 16 chars');
|
||||
assertEqual(signature.length, thinking.length, 'length should match thinking length');
|
||||
assertExists(signature.timestamp, 'timestamp should exist');
|
||||
});
|
||||
|
||||
// Test 7: Stop reason mapping
|
||||
runner.test('maps OpenAI stop reasons to Anthropic', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
|
||||
assertEqual(transformer._mapStopReason('stop'), 'end_turn', 'stop → end_turn');
|
||||
assertEqual(transformer._mapStopReason('length'), 'max_tokens', 'length → max_tokens');
|
||||
assertEqual(transformer._mapStopReason('tool_calls'), 'tool_use', 'tool_calls → tool_use');
|
||||
assertEqual(
|
||||
transformer._mapStopReason('unknown'),
|
||||
'end_turn',
|
||||
'unknown → end_turn (default)'
|
||||
);
|
||||
});
|
||||
|
||||
// Test 8: Preserve temperature and top_p
|
||||
runner.test('preserves temperature and top_p parameters', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test' }],
|
||||
temperature: 0.7,
|
||||
top_p: 0.9
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
|
||||
assertEqual(openaiRequest.temperature, 0.7, 'temperature should be preserved');
|
||||
assertEqual(openaiRequest.top_p, 0.9, 'top_p should be preserved');
|
||||
});
|
||||
|
||||
// Test 9: Default thinking enabled
|
||||
runner.test('enables thinking by default', () => {
|
||||
const transformer = new GlmtTransformer({ defaultThinking: true });
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test' }]
|
||||
};
|
||||
|
||||
const { openaiRequest, thinkingConfig } = transformer.transformRequest(input);
|
||||
|
||||
assertEqual(thinkingConfig.thinking, true, 'thinking should be enabled by default');
|
||||
assertEqual(openaiRequest.reasoning, true, 'reasoning should be in request');
|
||||
});
|
||||
|
||||
// Test 10: Error handling in transformRequest
|
||||
runner.test('handles errors in transformRequest gracefully', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const invalidInput = null; // Invalid input
|
||||
|
||||
const { openaiRequest, thinkingConfig, error } = transformer.transformRequest(invalidInput);
|
||||
|
||||
assertExists(error, 'error should be present');
|
||||
assertEqual(thinkingConfig.thinking, false, 'thinking should be disabled on error');
|
||||
});
|
||||
|
||||
// Test 11: Error handling in transformResponse
|
||||
runner.test('handles errors in transformResponse gracefully', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const invalidResponse = {}; // Missing choices
|
||||
|
||||
const result = transformer.transformResponse(invalidResponse, {});
|
||||
|
||||
assertEqual(result.type, 'message', 'should return valid message type');
|
||||
assertEqual(result.role, 'assistant', 'should be assistant role');
|
||||
assertExists(result.content[0].text, 'should have error text');
|
||||
});
|
||||
|
||||
// Test 12: Streaming disabled (not yet supported)
|
||||
runner.test('disables streaming (not yet supported)', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const input = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test' }],
|
||||
stream: true
|
||||
};
|
||||
|
||||
const { openaiRequest } = transformer.transformRequest(input);
|
||||
|
||||
assertEqual(openaiRequest.stream, false, 'stream should be disabled');
|
||||
});
|
||||
|
||||
// Test 13: Debug mode disabled by default
|
||||
runner.test('debug mode disabled by default', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
assertEqual(transformer.debugLog, false, 'debugLog should be false by default');
|
||||
});
|
||||
|
||||
// Test 14: Debug mode enabled via config
|
||||
runner.test('debug mode enabled via config', () => {
|
||||
const transformer = new GlmtTransformer({ debugLog: true });
|
||||
assertEqual(transformer.debugLog, true, 'debugLog should be true when enabled');
|
||||
});
|
||||
|
||||
// Test 15: Debug mode enabled via env var
|
||||
runner.test('debug mode enabled via CCS_DEBUG_LOG=1', () => {
|
||||
process.env.CCS_DEBUG_LOG = '1';
|
||||
const transformer = new GlmtTransformer();
|
||||
assertEqual(transformer.debugLog, true, 'debugLog should be true when CCS_DEBUG_LOG=1');
|
||||
delete process.env.CCS_DEBUG_LOG;
|
||||
});
|
||||
|
||||
// Test 16: Debug log directory path
|
||||
runner.test('debug log directory uses ~/.ccs/logs by default', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const expectedPath = path.join(os.homedir(), '.ccs', 'logs');
|
||||
assertEqual(transformer.debugLogDir, expectedPath, 'debugLogDir should be ~/.ccs/logs');
|
||||
});
|
||||
|
||||
// Test 17: Validate transformation checks
|
||||
runner.test('validates transformation with all checks', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const validResponse = {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'thinking', thinking: 'reasoning...' },
|
||||
{ type: 'text', text: 'answer' }
|
||||
],
|
||||
usage: { input_tokens: 10, output_tokens: 20 }
|
||||
};
|
||||
|
||||
const validation = transformer._validateTransformation(validResponse);
|
||||
assertEqual(validation.passed, 5, 'All 5 checks should pass');
|
||||
assertEqual(validation.total, 5, 'Should have 5 total checks');
|
||||
assertEqual(validation.valid, true, 'Should be valid');
|
||||
assertEqual(validation.checks.hasContent, true, 'hasContent check');
|
||||
assertEqual(validation.checks.hasThinking, true, 'hasThinking check');
|
||||
assertEqual(validation.checks.hasText, true, 'hasText check');
|
||||
assertEqual(validation.checks.validStructure, true, 'validStructure check');
|
||||
assertEqual(validation.checks.hasUsage, true, 'hasUsage check');
|
||||
});
|
||||
|
||||
// Test 18: Validate transformation with missing thinking
|
||||
runner.test('validates transformation without thinking block', () => {
|
||||
const transformer = new GlmtTransformer();
|
||||
const response = {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'text', text: 'answer' }
|
||||
],
|
||||
usage: { input_tokens: 10, output_tokens: 20 }
|
||||
};
|
||||
|
||||
const validation = transformer._validateTransformation(response);
|
||||
assertEqual(validation.passed, 4, '4 checks should pass (no thinking)');
|
||||
assertEqual(validation.checks.hasThinking, false, 'hasThinking should be false');
|
||||
assertEqual(validation.checks.hasText, true, 'hasText should be true');
|
||||
});
|
||||
|
||||
// Run tests
|
||||
runner.run().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
}).catch(error => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const GlmtTransformer = require('../bin/glmt-transformer');
|
||||
|
||||
console.log('=== Performance Test: Debug Mode Impact ===\n');
|
||||
|
||||
const iterations = 1000;
|
||||
|
||||
const sampleRequest = {
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'Test performance' }]
|
||||
};
|
||||
|
||||
const sampleResponse = {
|
||||
id: 'perf-test',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Quick response',
|
||||
reasoning_content: 'Some reasoning content here...'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
|
||||
};
|
||||
|
||||
// Test 1: Debug OFF
|
||||
console.log(`Test 1: Debug OFF (${iterations} iterations)`);
|
||||
const transformerOff = new GlmtTransformer({ debugLog: false, verbose: false });
|
||||
const startOff = Date.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
transformerOff.transformRequest(sampleRequest);
|
||||
transformerOff.transformResponse(sampleResponse, {});
|
||||
}
|
||||
const endOff = Date.now();
|
||||
const timeOff = endOff - startOff;
|
||||
console.log(` Total time: ${timeOff}ms`);
|
||||
console.log(` Average per request: ${(timeOff / iterations).toFixed(2)}ms`);
|
||||
|
||||
// Test 2: Debug ON
|
||||
console.log(`\nTest 2: Debug ON (${iterations} iterations)`);
|
||||
const transformerOn = new GlmtTransformer({ debugLog: true, verbose: false });
|
||||
const startOn = Date.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
transformerOn.transformRequest(sampleRequest);
|
||||
transformerOn.transformResponse(sampleResponse, {});
|
||||
}
|
||||
const endOn = Date.now();
|
||||
const timeOn = endOn - startOn;
|
||||
console.log(` Total time: ${timeOn}ms`);
|
||||
console.log(` Average per request: ${(timeOn / iterations).toFixed(2)}ms`);
|
||||
|
||||
// Calculate overhead
|
||||
const overhead = timeOn - timeOff;
|
||||
const overheadPercent = ((overhead / timeOff) * 100).toFixed(2);
|
||||
|
||||
console.log(`\n=== Results ===`);
|
||||
console.log(`Debug OFF: ${timeOff}ms`);
|
||||
console.log(`Debug ON: ${timeOn}ms`);
|
||||
console.log(`Overhead: ${overhead}ms (${overheadPercent}%)`);
|
||||
|
||||
// Note: High overhead is expected due to file I/O
|
||||
console.log(`\nNote: Debug mode is opt-in only and disabled by default.`);
|
||||
console.log(`When disabled, there is NO performance impact (early return in _writeDebugLog).`);
|
||||
|
||||
// Cleanup
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const logDir = path.join(os.homedir(), '.ccs', 'logs');
|
||||
if (fs.existsSync(logDir)) {
|
||||
fs.rmSync(logDir, { recursive: true, force: true });
|
||||
console.log(`\nCleaned up ${iterations * 4} debug log files.`);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const GlmtTransformer = require('../bin/glmt-transformer');
|
||||
|
||||
console.log('=== Demo: Verbose Output with Reasoning Detection ===\n');
|
||||
|
||||
const transformer = new GlmtTransformer({ verbose: true, debugLog: false });
|
||||
|
||||
const openaiResponse = {
|
||||
id: 'demo-response',
|
||||
model: 'GLM-4.6',
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'The answer is 42. This is based on careful analysis of the problem.',
|
||||
reasoning_content: `Let me think through this problem step by step.
|
||||
|
||||
First, I need to understand what's being asked. The question relates to the fundamental nature of the universe.
|
||||
|
||||
Based on Douglas Adams' work, we know that after Deep Thought computed for 7.5 million years, it determined that the answer to the Ultimate Question of Life, the Universe, and Everything is 42.
|
||||
|
||||
However, the actual question itself was never properly formulated. This suggests that knowing the answer is meaningless without understanding the question.
|
||||
|
||||
In a practical sense, this teaches us an important lesson about problem-solving: we must first ensure we're asking the right questions before seeking answers.
|
||||
|
||||
Therefore, my response will acknowledge both the famous answer and the philosophical implications of the question-answer relationship.`
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: { prompt_tokens: 50, completion_tokens: 150, total_tokens: 200 }
|
||||
};
|
||||
|
||||
console.log('Transforming OpenAI response with reasoning_content to Anthropic format...\n');
|
||||
|
||||
const result = transformer.transformResponse(openaiResponse, { thinking: true });
|
||||
|
||||
console.log('\n=== Transformation Complete ===');
|
||||
console.log(`\nResult structure:`);
|
||||
console.log(` Type: ${result.type}`);
|
||||
console.log(` Role: ${result.role}`);
|
||||
console.log(` Content blocks: ${result.content.length}`);
|
||||
console.log(` - Block 1: ${result.content[0].type}`);
|
||||
console.log(` - Block 2: ${result.content[1].type}`);
|
||||
console.log(` Stop reason: ${result.stop_reason}`);
|
||||
console.log(` Usage: input=${result.usage.prompt_tokens}, output=${result.usage.completion_tokens}`);
|
||||
Reference in new issue
Block a user