mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +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:
@@ -4,6 +4,50 @@ All notable changes to CCS will be documented here.
|
||||
|
||||
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [3.3.0] - 2025-11-11
|
||||
|
||||
### Added
|
||||
|
||||
**GLMT Improvements**:
|
||||
- Debug mode: `CCS_DEBUG_LOG=1` logs raw API request/response to `~/.ccs/logs/`
|
||||
- Verbose flag support: `ccs glmt --verbose` shows detailed transformation info
|
||||
- Config defaults: Added `alwaysThinkingEnabled`, temperature, timeouts, telemetry settings
|
||||
- Reasoning detection verbose output: Shows length, preview, validation
|
||||
- Config migration: v3.2.0 users auto-upgraded with API keys preserved
|
||||
|
||||
### Changed
|
||||
|
||||
**Log Cleanup**:
|
||||
- Removed duplicate streaming warnings per request
|
||||
- Added one-time startup info message
|
||||
- Improved error messages with actionable troubleshooting steps
|
||||
|
||||
### Fixed
|
||||
|
||||
**Config Consistency**:
|
||||
- GLMT profile now has `alwaysThinkingEnabled: true` (matches Kimi)
|
||||
- Added optimal defaults for thinking mode (temperature 0.2, extended timeouts)
|
||||
- Migration preserves user-modified values
|
||||
|
||||
### Documentation
|
||||
|
||||
**Troubleshooting Guide**:
|
||||
- Clarified duplicate "Enchanting" lines are Claude CLI issue (out of CCS scope)
|
||||
- Added debugging workflow for thinking visibility issues
|
||||
- Documented new verbose and debug modes
|
||||
- Added config customization examples
|
||||
|
||||
### Important Notes
|
||||
|
||||
**GLMT Implementation Status**:
|
||||
- ✅ **Node.js version** (`bin/ccs.js`): Fully implemented and tested
|
||||
- ⏳ **Native shell versions** (`lib/ccs`, `lib/ccs.ps1`): Not yet implemented
|
||||
- **Reason**: GLMT requires embedded proxy server (Node.js HTTP server)
|
||||
- **Workaround**: Use npm package installation for GLMT support
|
||||
- **Future**: Native shell GLMT support planned for future release
|
||||
|
||||
---
|
||||
|
||||
## [3.2.0] - 2025-11-10
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -6,141 +6,166 @@ Guidance for Claude Code when working with this repository.
|
||||
|
||||
CCS (Claude Code Switch): CLI wrapper for instant switching between multiple Claude accounts (work, personal, team) and alternative models (GLM 4.6, Kimi). Built on v3.1 login-per-profile architecture with shared data support.
|
||||
|
||||
**Installation**:
|
||||
- npm: `npm install -g @kaitranntt/ccs` (recommended)
|
||||
- macOS/Linux: `curl -fsSL ccs.kaitran.ca/install | bash`
|
||||
- Windows: `irm ccs.kaitran.ca/install | iex`
|
||||
**Core function**: Switch Claude accounts/models without manual config editing.
|
||||
|
||||
## Core Design Principles
|
||||
## Design Principles
|
||||
|
||||
- **YAGNI**: No features "just in case"
|
||||
- **KISS**: Simple bash/PowerShell/Node.js, no complexity
|
||||
- **DRY**: One source of truth (config.json)
|
||||
- **CLI-First UX**: Command-line is primary interface
|
||||
- **CLI-First**: Command-line primary interface
|
||||
|
||||
Tool does ONE thing: instant switching between Claude accounts and alternative models.
|
||||
|
||||
### CLI Documentation (CRITICAL)
|
||||
|
||||
**All functionality changes MUST update `--help` in ALL implementations:**
|
||||
- `bin/ccs.js` - handleHelpCommand()
|
||||
- `lib/ccs` - show_help()
|
||||
- `lib/ccs.ps1` - Show-Help
|
||||
|
||||
Non-negotiable. If not in `--help`, it doesn't exist for users.
|
||||
|
||||
## Key Constraints
|
||||
## Critical Constraints
|
||||
|
||||
1. **NO EMOJIS** - ASCII only: [OK], [!], [X], [i]
|
||||
2. **TTY-aware colors** - Respect NO_COLOR env var
|
||||
3. **Install locations**:
|
||||
- Unix: `~/.local/bin` (auto PATH, no sudo)
|
||||
- Windows: `%USERPROFILE%\.ccs`
|
||||
4. **Auto PATH config** - Detects bash/zsh/fish, adds automatically
|
||||
4. **Auto PATH config** - Detect shell (bash/zsh/fish), add automatically
|
||||
5. **Idempotent installs** - Safe to run multiple times
|
||||
6. **Non-invasive** - Never modify `~/.claude/settings.json`
|
||||
7. **Cross-platform parity** - Identical behavior everywhere
|
||||
8. **Graceful error handling** - See tests/edge-cases.sh
|
||||
8. **CLI documentation** - ALL changes MUST update `--help` in bin/ccs.js, lib/ccs, lib/ccs.ps1
|
||||
|
||||
## Architecture
|
||||
|
||||
### v3.1 Features
|
||||
### v3.2 GLMT Profile
|
||||
|
||||
**Login-Per-Profile**: Each profile = isolated Claude instance via `ccs auth create <profile>`. No credential copying/encryption.
|
||||
**Implementation**: Embedded HTTP proxy converts Anthropic ↔ OpenAI formats
|
||||
|
||||
**Profile Types**:
|
||||
1. **Settings-based**: GLM, Kimi, default - uses `--settings` flag
|
||||
2. **Account-based**: work, personal, team - uses `CLAUDE_CONFIG_DIR`
|
||||
**[!] Important**: GLMT only available in Node.js version (`bin/ccs.js`). Native shell versions (`lib/ccs`, `lib/ccs.ps1`) do not support GLMT yet (requires HTTP server).
|
||||
|
||||
**Shared Data** (v3.1): commands/, skills/, agents/ symlinked from `~/.ccs/shared/`
|
||||
**Flow**:
|
||||
1. User: `ccs glmt "solve problem"`
|
||||
2. `bin/ccs.js` spawns `bin/glmt-proxy.js` on localhost random port
|
||||
3. Modifies `glmt.settings.json`: `ANTHROPIC_BASE_URL=http://127.0.0.1:<port>`
|
||||
4. Spawns Claude CLI with modified settings
|
||||
5. Proxy intercepts requests:
|
||||
- `glmt-transformer.js` converts Anthropic → OpenAI format
|
||||
- Injects reasoning parameters (`reasoning: true`, `reasoning_effort`)
|
||||
- Forwards to `api.z.ai/api/coding/paas/v4/chat/completions`
|
||||
- Converts response: `reasoning_content` → thinking blocks
|
||||
- Returns Anthropic format to Claude CLI
|
||||
6. Thinking blocks appear in Claude Code UI
|
||||
|
||||
**Concurrent Sessions**: Multiple profiles run simultaneously via isolated config dirs.
|
||||
**Files**:
|
||||
- `bin/glmt-proxy.js` (275 lines): HTTP proxy server
|
||||
- `bin/glmt-transformer.js` (267 lines): Format conversion
|
||||
- `config/base-glmt.settings.json`: Template with Z.AI endpoint
|
||||
- `tests/glmt-transformer.test.js` (285 lines): Unit tests
|
||||
|
||||
**Implementations**:
|
||||
- npm package: Pure Node.js (bin/ccs.js) using child_process.spawn
|
||||
- Traditional: bash (lib/ccs) or PowerShell (lib/ccs.ps1)
|
||||
**Control tags**:
|
||||
- `<Thinking:On|Off>` - Enable/disable reasoning
|
||||
- `<Effort:Low|Medium|High>` - Control reasoning depth
|
||||
|
||||
### File Structure
|
||||
**Limitations**:
|
||||
- Streaming not supported (buffered mode only)
|
||||
- Requires Z.AI API key with coding plan access
|
||||
- Proxy lifecycle tied to Claude CLI
|
||||
|
||||
### v3.1 Shared Data
|
||||
|
||||
**Commands/skills/agents symlinked from `~/.ccs/shared/`** - no duplication across profiles.
|
||||
|
||||
```
|
||||
~/.ccs/
|
||||
├── shared/ # Shared across all profiles
|
||||
│ ├── commands/
|
||||
│ ├── skills/
|
||||
│ └── agents/
|
||||
├── instances/ # Profile-specific
|
||||
│ └── work/
|
||||
│ ├── commands@ → shared/commands/
|
||||
│ ├── skills@ → shared/skills/
|
||||
│ ├── agents@ → shared/agents/
|
||||
│ ├── settings.json # API keys, credentials
|
||||
│ ├── sessions/ # Conversation history
|
||||
│ ├── todolists/
|
||||
│ └── logs/
|
||||
```
|
||||
|
||||
**Shared**: commands/, skills/, agents/
|
||||
**Profile-specific**: settings.json, sessions/, todolists/, logs/
|
||||
|
||||
**Windows fallback**: Copies dirs if symlinks unavailable (enable Developer Mode for symlinks)
|
||||
|
||||
### Profile Types
|
||||
|
||||
**Settings-based**: GLM, GLMT, Kimi, default
|
||||
- GLM: Uses `--settings` flag (Anthropic endpoint, no thinking)
|
||||
- GLMT: Embedded proxy (OpenAI endpoint, thinking enabled)
|
||||
- Kimi: Uses `--settings` flag
|
||||
|
||||
**Account-based**: work, personal, team
|
||||
- Uses `CLAUDE_CONFIG_DIR` for isolated instances
|
||||
- Create: `ccs auth create <profile>`
|
||||
|
||||
### Concurrent Sessions
|
||||
|
||||
Multiple profiles run simultaneously via isolated config dirs.
|
||||
|
||||
## File Structure
|
||||
|
||||
**Key Files**:
|
||||
- `package.json`: npm manifest + postinstall
|
||||
- `bin/ccs.js`: Node.js entry point
|
||||
- `bin/instance-manager.js`: Instance orchestration
|
||||
- `bin/shared-manager.js`: Shared data symlinks (v3.1)
|
||||
- `bin/glmt-proxy.js`: Embedded HTTP proxy (v3.2)
|
||||
- `bin/glmt-transformer.js`: Anthropic ↔ OpenAI conversion (v3.2)
|
||||
- `scripts/postinstall.js`: Auto-creates configs (idempotent)
|
||||
- `lib/ccs` / `lib/ccs.ps1`: Platform-specific executables
|
||||
- `installers/*.sh` / `installers/*.ps1`: Install/uninstall scripts
|
||||
- `lib/ccs`: bash executable
|
||||
- `lib/ccs.ps1`: PowerShell executable
|
||||
- `installers/*.sh|*.ps1`: Install/uninstall scripts
|
||||
- `tests/glmt-transformer.test.js`: GLMT unit tests
|
||||
- `VERSION`: Version source of truth (MAJOR.MINOR.PATCH)
|
||||
- `.claude/`: Commands/skills for Claude Code
|
||||
|
||||
**Executables**:
|
||||
- Unix: `~/.local/bin/ccs` → `~/.ccs/ccs`
|
||||
- Windows: `%USERPROFILE%\.ccs\ccs.ps1`
|
||||
|
||||
**Config Directory** (v3.1):
|
||||
```
|
||||
~/.ccs/
|
||||
├── ccs / ccs.ps1 # Executable
|
||||
├── config.json # Settings-based profiles
|
||||
├── profiles.json # Account-based profiles
|
||||
├── shared/ # Shared across all (v3.1)
|
||||
│ ├── commands/ # Slash commands
|
||||
│ ├── skills/ # Claude skills
|
||||
│ └── agents/ # Agent configs
|
||||
├── instances/ # Isolated per-profile
|
||||
│ └── work/
|
||||
│ ├── commands@ → shared/commands/ # Symlink
|
||||
│ ├── skills@ → shared/skills/ # Symlink
|
||||
│ ├── agents@ → shared/agents/ # Symlink
|
||||
│ ├── settings.json # Profile-specific
|
||||
│ ├── sessions/ # Profile-specific
|
||||
│ ├── todolists/ # Profile-specific
|
||||
│ └── logs/ # Profile-specific
|
||||
├── glm.settings.json # GLM template
|
||||
├── kimi.settings.json # Kimi template
|
||||
└── .claude/ # Integration
|
||||
```
|
||||
**Config Files**:
|
||||
- `~/.ccs/config.json`: Settings-based profiles
|
||||
- `~/.ccs/profiles.json`: Account-based profiles
|
||||
- `~/.ccs/glm.settings.json`: GLM template (Anthropic endpoint)
|
||||
- `~/.ccs/glmt.settings.json`: GLMT template (proxy + thinking)
|
||||
- `~/.ccs/kimi.settings.json`: Kimi template
|
||||
|
||||
**v3.1 Data Structure**:
|
||||
- **Shared**: commands/, skills/, agents/ → symlinked from `~/.ccs/shared/`
|
||||
- **Profile-specific**: settings.json, sessions/, todolists/, logs/
|
||||
- **Windows**: Copies dirs if symlinks fail (enable Developer Mode for symlinks)
|
||||
## Implementations
|
||||
|
||||
### Technical Implementation
|
||||
**npm package**: Pure Node.js (`bin/ccs.js`) using `child_process.spawn`
|
||||
|
||||
**Account-Based Profiles**:
|
||||
```bash
|
||||
# Create profile
|
||||
ccs auth create work # Opens Claude CLI for login
|
||||
**Traditional install**: bash (`lib/ccs`) or PowerShell (`lib/ccs.ps1`)
|
||||
|
||||
# Usage
|
||||
CLAUDE_CONFIG_DIR=~/.ccs/instances/work claude [args]
|
||||
```
|
||||
## Code Standards
|
||||
|
||||
**Shared Data** (v3.1):
|
||||
```javascript
|
||||
// bin/shared-manager.js
|
||||
ensureSharedDirectories() // Creates shared/{commands,skills,agents}
|
||||
linkSharedDirectories(path) // Symlinks instance to shared
|
||||
migrateToSharedStructure() // Auto-migrates v3.0 (idempotent)
|
||||
```
|
||||
### Bash
|
||||
- Compatibility: bash 3.2+
|
||||
- Quote vars: `"$VAR"` not `$VAR`
|
||||
- Tests: `[[ ]]` not `[ ]`
|
||||
- Shebang: `#!/usr/bin/env bash`
|
||||
- Safety: `set -euo pipefail`
|
||||
- Dependency: `jq` only
|
||||
|
||||
**Migration** (v3.0 → v3.1):
|
||||
1. Detect if `~/.ccs/shared/` exists (skip if present)
|
||||
2. Create `~/.ccs/shared/{commands,skills,agents}`
|
||||
3. Copy from `~/.claude/` (preserves data)
|
||||
4. Symlink all instances to shared
|
||||
5. Windows: Copy dirs if symlinks fail
|
||||
### PowerShell
|
||||
- Compatibility: PowerShell 5.1+
|
||||
- `$ErrorActionPreference = "Stop"`
|
||||
- Native JSON: ConvertFrom-Json / ConvertTo-Json
|
||||
- No external dependencies
|
||||
|
||||
**Settings-Based Profiles**:
|
||||
```bash
|
||||
claude --settings ~/.ccs/glm.settings.json [args]
|
||||
```
|
||||
### Node.js
|
||||
- Compatibility: Node.js 14+
|
||||
- `child_process.spawn` for Claude CLI
|
||||
- Handle SIGINT/SIGTERM
|
||||
- `path` module for cross-platform paths
|
||||
|
||||
**Profile Detection**:
|
||||
1. Check `profiles.json` (account-based) → use `CLAUDE_CONFIG_DIR`
|
||||
2. Check `config.json` (settings-based) → use `--settings`
|
||||
3. Not found → show error + available profiles
|
||||
### Terminal Output
|
||||
- TTY detect: `[[ -t 2 ]]` before colors
|
||||
- Respect `NO_COLOR` env var
|
||||
- ASCII only: [OK], [!], [X], [i]
|
||||
- Errors: Box borders (╔═╗║╚╝)
|
||||
- Colors: Disable when not TTY
|
||||
|
||||
## Development
|
||||
|
||||
@@ -171,8 +196,6 @@ rm -rf ~/.ccs # Clean environment
|
||||
|
||||
### Publishing
|
||||
```bash
|
||||
# First-time: npm login, add NPM_TOKEN to GitHub Secrets
|
||||
|
||||
# Release workflow
|
||||
./scripts/bump-version.sh patch
|
||||
git add VERSION package.json lib/* installers/*
|
||||
@@ -184,106 +207,26 @@ git push origin main && git push origin vX.Y.Z # Triggers CI
|
||||
npm publish --dry-run && npm publish --access public
|
||||
```
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Bash
|
||||
- Compatibility: bash 3.2+
|
||||
- Quote vars: `"$VAR"` not `$VAR`
|
||||
- Tests: `[[ ]]` not `[ ]`
|
||||
- Shebang: `#!/usr/bin/env bash`
|
||||
- Safety: `set -euo pipefail`
|
||||
- Dependency: `jq` only
|
||||
|
||||
### Terminal Output
|
||||
- TTY detect: `[[ -t 2 ]]` before colors
|
||||
- Respect `NO_COLOR` env var
|
||||
- ASCII only: [OK], [!], [X], [i]
|
||||
- Errors: Box borders (╔═╗║╚╝)
|
||||
- Colors: Disable when not TTY
|
||||
|
||||
### PowerShell
|
||||
- Compatibility: PowerShell 5.1+
|
||||
- `$ErrorActionPreference = "Stop"`
|
||||
- Native JSON: ConvertFrom-Json / ConvertTo-Json
|
||||
- No external dependencies
|
||||
|
||||
### Node.js
|
||||
- Compatibility: Node.js 14+
|
||||
- `child_process.spawn` for Claude CLI
|
||||
- Handle SIGINT/SIGTERM
|
||||
- `path` module for cross-platform paths
|
||||
|
||||
### Versioning
|
||||
Update all three atomically via `./scripts/bump-version.sh`:
|
||||
1. `VERSION`
|
||||
2. `installers/install.sh` (CCS_VERSION)
|
||||
3. `installers/install.ps1` ($CcsVersion)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Profile Detection
|
||||
- No args OR first arg starts with `-` → default profile
|
||||
- First arg no `-` → profile name
|
||||
- Special flags first: `--version`, `-v`, `--help`, `-h`
|
||||
- `ccs auth create <profile>` → create account-based profile
|
||||
|
||||
### Installation Modes
|
||||
- **Git**: Cloned repo (symlinks executables)
|
||||
- **Standalone**: curl/irm (downloads from GitHub)
|
||||
- Detection: Check if `ccs` exists in script dir/parent
|
||||
|
||||
### Idempotency
|
||||
Install scripts safe to run multiple times:
|
||||
- Check existing files before create
|
||||
- Single backup: `config.json.backup` (no timestamps)
|
||||
- Skip existing `.claude/` install
|
||||
- Handle clean + existing installs
|
||||
|
||||
### Settings Format
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "key",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
All values = strings (not booleans/objects) to prevent PowerShell crashes.
|
||||
|
||||
### Profile Files
|
||||
|
||||
**profiles.json** (account-based):
|
||||
```json
|
||||
{"profiles": {"work": "~/.ccs/instances/work"}}
|
||||
```
|
||||
|
||||
**config.json** (settings-based):
|
||||
```json
|
||||
{"profiles": {"glm": "~/.ccs/glm.settings.json"}}
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### New Feature
|
||||
|
||||
1. Verify YAGNI/KISS/DRY alignment
|
||||
2. Implement for bash/PowerShell/Node.js
|
||||
3. **Update `--help` in ALL three** (bin/ccs.js, lib/ccs, lib/ccs.ps1) - REQUIRED
|
||||
3. **REQUIRED**: Update `--help` in bin/ccs.js, lib/ccs, lib/ccs.ps1
|
||||
4. Test on macOS/Linux/Windows
|
||||
5. Update tests/edge-cases.*
|
||||
6. Update CONTRIBUTING.md if needed
|
||||
7. Update README.md if user-facing
|
||||
6. Update README.md if user-facing
|
||||
|
||||
### Bug Fix
|
||||
|
||||
1. Add test case reproducing bug
|
||||
2. Fix in bash/PowerShell/Node.js
|
||||
3. Verify no regression
|
||||
4. Test all platforms
|
||||
|
||||
### Release
|
||||
|
||||
1. `./scripts/bump-version.sh [major|minor|patch]`
|
||||
2. Review VERSION, install scripts
|
||||
3. Test git + standalone modes
|
||||
@@ -308,15 +251,93 @@ Before PR:
|
||||
- [ ] No PATH duplication
|
||||
- [ ] Manual PATH instructions clear
|
||||
- [ ] Concurrent sessions work
|
||||
- [ ] Instance isolation (no contamination)
|
||||
- [ ] `--help` updated in bin/ccs.js, lib/ccs, lib/ccs.ps1 (if feature changed)
|
||||
- [ ] Instance isolation
|
||||
- [ ] `--help` updated in bin/ccs.js, lib/ccs, lib/ccs.ps1
|
||||
- [ ] `--help` consistent across all three
|
||||
|
||||
## Claude Code Integration
|
||||
## Technical Details
|
||||
|
||||
`.claude/` contains:
|
||||
- `/ccs` command: Task delegation to different models
|
||||
- `ccs-delegation` skill: Delegation patterns
|
||||
### Profile Detection
|
||||
|
||||
1. Check `profiles.json` (account-based) → use `CLAUDE_CONFIG_DIR`
|
||||
2. Check `config.json` (settings-based) → use `--settings`
|
||||
3. Not found → show error + available profiles
|
||||
|
||||
### Installation Modes
|
||||
|
||||
- **Git**: Cloned repo (symlinks executables)
|
||||
- **Standalone**: curl/irm (downloads from GitHub)
|
||||
- Detection: Check if `ccs` exists in script dir/parent
|
||||
|
||||
### Idempotency
|
||||
|
||||
Install scripts safe to run multiple times:
|
||||
- Check existing files before create
|
||||
- Single backup: `config.json.backup` (no timestamps)
|
||||
- Skip existing `.claude/` install
|
||||
- Handle clean + existing installs
|
||||
|
||||
### Settings Format
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "key",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All values = strings (not booleans/objects) to prevent PowerShell crashes.
|
||||
|
||||
### Profile Files
|
||||
|
||||
**profiles.json** (account-based):
|
||||
```json
|
||||
{"profiles": {"work": "~/.ccs/instances/work"}}
|
||||
```
|
||||
|
||||
**config.json** (settings-based):
|
||||
```json
|
||||
{"profiles": {"glm": "~/.ccs/glm.settings.json"}}
|
||||
```
|
||||
|
||||
## GLMT Troubleshooting
|
||||
|
||||
**API Key Issues**:
|
||||
```bash
|
||||
# Error: GLMT profile requires Z.AI API key
|
||||
# Fix: Edit ~/.ccs/glmt.settings.json, set ANTHROPIC_AUTH_TOKEN
|
||||
```
|
||||
|
||||
**Proxy Failures**:
|
||||
- Timeout (>30s): Proxy didn't start → check Node.js ≥14
|
||||
- Port conflicts: Uses random port, unlikely
|
||||
- Connection refused: Firewall blocking 127.0.0.1
|
||||
|
||||
**No Thinking Blocks**:
|
||||
- Check Z.AI API plan supports reasoning_content
|
||||
- Verify `<Thinking:On>` tag not overridden
|
||||
- Test with `ccs glm` (no thinking) to isolate proxy issues
|
||||
|
||||
**Streaming Disclaimer**:
|
||||
- GLMT uses buffered mode (streaming not supported)
|
||||
- Trade-off: thinking capability vs streaming speed
|
||||
|
||||
**Debug Mode**:
|
||||
```bash
|
||||
# Verbose logging
|
||||
ccs glmt --verbose "test"
|
||||
|
||||
# File logging
|
||||
export CCS_DEBUG_LOG=1
|
||||
ccs glmt --verbose "test"
|
||||
# Logs: ~/.ccs/logs/
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
@@ -325,3 +346,9 @@ Before PR:
|
||||
- Suggest recovery steps
|
||||
- Never leave broken state
|
||||
- Guide to `ccs auth create` if profile missing
|
||||
|
||||
## Claude Code Integration
|
||||
|
||||
`.claude/` contains:
|
||||
- `/ccs` command: Task delegation to different models
|
||||
- `ccs-delegation` skill: Delegation patterns
|
||||
|
||||
@@ -30,7 +30,7 @@ Stop hitting rate limits. Keep working continuously.
|
||||
claude /login
|
||||
```
|
||||
|
||||
### Primary Installation Methods
|
||||
### Installation
|
||||
|
||||
#### Option 1: npm Package (Recommended)
|
||||
|
||||
@@ -75,6 +75,7 @@ irm ccs.kaitran.ca/install | iex
|
||||
{
|
||||
"profiles": {
|
||||
"glm": "~/.ccs/glm.settings.json",
|
||||
"glmt": "~/.ccs/glmt.settings.json",
|
||||
"kimi": "~/.ccs/kimi.settings.json",
|
||||
"default": "~/.claude/settings.json"
|
||||
}
|
||||
@@ -106,19 +107,23 @@ $env:CCS_CLAUDE_PATH = "D:\Tools\Claude\claude.exe" # Windows
|
||||
|
||||
### Your First Switch
|
||||
|
||||
> **⚠️ Important**: Before using GLM or Kimi profiles, you need to update your API keys in their respective settings files:
|
||||
> **⚠️ Important**: Before using GLM/GLMT or Kimi profiles, update API keys in settings files:
|
||||
> - **GLM**: Edit `~/.ccs/glm.settings.json` and add your GLM API key
|
||||
> - **GLMT**: Edit `~/.ccs/glmt.settings.json` and add your Z.AI API key (requires coding plan)
|
||||
> - **Kimi**: Edit `~/.ccs/kimi.settings.json` and add your Kimi API key
|
||||
|
||||
```bash
|
||||
# Use Claude subscription (default) for high-level planning
|
||||
ccs "Plan the implementation of a microservices architecture"
|
||||
# Default Claude subscription
|
||||
ccs "Plan microservices architecture"
|
||||
|
||||
# Switch to GLM for cost-optimized tasks
|
||||
ccs glm "Create a simple REST API"
|
||||
# Switch to GLM (cost-optimized)
|
||||
ccs glm "Create REST API"
|
||||
|
||||
# Switch to Kimi for its thinking capabilities
|
||||
ccs kimi "Write integration tests with proper error handling"
|
||||
# GLM with thinking mode
|
||||
ccs glmt "Solve algorithmic problem"
|
||||
|
||||
# Kimi for coding
|
||||
ccs kimi "Write integration tests"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -149,186 +154,183 @@ Manual context switching breaks your workflow. **CCS manages it seamlessly**.
|
||||
|
||||
</div>
|
||||
|
||||
**The Solution**:
|
||||
```bash
|
||||
ccs work # Use company Claude account
|
||||
ccs personal # Switch to personal Claude account
|
||||
ccs glm # Switch to GLM for cost-effective tasks
|
||||
ccs kimi # Switch to Kimi for alternative option
|
||||
# Hit rate limit? Switch instantly:
|
||||
ccs glm # Continue working with GLM
|
||||
# Need different company account?
|
||||
ccs work-2 # Switch to second company account
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Shared Data Architecture
|
||||
## Architecture
|
||||
|
||||
**v3.1 Shared Global Data**: Commands and skills are symlinked across all profiles via `~/.ccs/shared/`, eliminating duplication.
|
||||
### Profile Types
|
||||
|
||||
**Settings-based**: GLM, GLMT, Kimi, default
|
||||
- Uses `--settings` flag pointing to config files
|
||||
- GLMT: Embedded proxy for thinking mode support
|
||||
|
||||
**Account-based**: work, personal, team
|
||||
- Uses `CLAUDE_CONFIG_DIR` for isolated instances
|
||||
- Create with `ccs auth create <profile>`
|
||||
|
||||
### Shared Data (v3.1)
|
||||
|
||||
Commands and skills symlinked from `~/.ccs/shared/` - no duplication across profiles.
|
||||
|
||||
**Directory Structure**:
|
||||
```
|
||||
~/.ccs/
|
||||
├── shared/ # Shared across all profiles
|
||||
│ ├── commands/ # Custom slash commands
|
||||
│ └── skills/ # Claude Code skills
|
||||
│ ├── agents/
|
||||
│ ├── commands/
|
||||
│ └── skills/
|
||||
├── instances/ # Profile-specific data
|
||||
│ ├── work/
|
||||
│ │ ├── commands@ → ~/.ccs/shared/commands/ # Symlink
|
||||
│ │ ├── skills@ → ~/.ccs/shared/skills/ # Symlink
|
||||
│ │ ├── settings.json # Profile-specific config
|
||||
│ │ └── sessions/ # Profile-specific sessions
|
||||
│ └── personal/
|
||||
│ └── work/
|
||||
│ ├── agents@ → shared/agents/
|
||||
│ ├── commands@ → shared/commands/
|
||||
│ ├── skills@ → shared/skills/
|
||||
│ ├── settings.json # API keys, credentials
|
||||
│ └── sessions/ # Conversation history
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- No duplication of commands/skills across profiles
|
||||
- Single source of truth for shared resources
|
||||
- Automatic migration from v3.0 (runs on first use)
|
||||
- Windows fallback: copies if symlinks unavailable (enable Developer Mode for true symlinks)
|
||||
**Shared**: commands/, skills/, agents/
|
||||
**Profile-specific**: settings.json, sessions/, todolists/, logs/
|
||||
|
||||
**What's Shared**:
|
||||
- `.claude/commands/` - Custom slash commands
|
||||
- `.claude/skills/` - Claude Code skills
|
||||
|
||||
**What's Profile-Specific**:
|
||||
- `settings.json` - API keys, credentials
|
||||
- `sessions/` - Conversation history
|
||||
- `todolists/` - Task tracking
|
||||
- `logs/` - Profile-specific logs
|
||||
**[i] Windows**: Copies dirs if symlinks unavailable (enable Developer Mode for true symlinks)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
## GLM with Thinking (GLMT)
|
||||
|
||||
**v3.0 Login-Per-Profile Model**: Each profile is an isolated Claude instance where users login directly. No credential copying or vault encryption.
|
||||
> **[!] Important**: GLMT requires npm installation (`npm install -g @kaitranntt/ccs`). Not available in native shell versions (requires Node.js HTTP server).
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "User Input"
|
||||
USER["User runs: ccs <profile> [args...]"]
|
||||
end
|
||||
### GLM vs GLMT
|
||||
|
||||
subgraph "Profile Detection Engine"
|
||||
DETECT[ProfileDetector]
|
||||
PROFILE_CHECK{Profile exists?}
|
||||
| Feature | GLM (`ccs glm`) | GLMT (`ccs glmt`) |
|
||||
|---------|-----------------|-------------------|
|
||||
| **Endpoint** | Anthropic-compatible | OpenAI-compatible |
|
||||
| **Thinking** | No | Yes (reasoning_content) |
|
||||
| **Streaming** | Yes | No (buffered) |
|
||||
| **Use Case** | Fast responses | Complex reasoning |
|
||||
|
||||
subgraph "Profile Types"
|
||||
SETTINGS["Settings-based<br/>glm, kimi, default"]
|
||||
ACCOUNT["Account-based<br/>work, personal, team"]
|
||||
end
|
||||
end
|
||||
### How It Works
|
||||
|
||||
subgraph "CCS Core Processing"
|
||||
CONFIG["Read config.json<br/>and profiles.json"]
|
||||
1. CCS spawns embedded HTTP proxy on localhost
|
||||
2. Proxy converts Anthropic format → OpenAI format
|
||||
3. Forwards to Z.AI with reasoning parameters
|
||||
4. Converts `reasoning_content` → thinking blocks
|
||||
5. Thinking appears in Claude Code UI
|
||||
|
||||
subgraph "Profile Handlers"
|
||||
SETTINGS_MGR["SettingsManager<br/>→ --settings flag"]
|
||||
INSTANCE_MGR["InstanceManager<br/>→ CLAUDE_CONFIG_DIR"]
|
||||
end
|
||||
end
|
||||
### Control Tags
|
||||
|
||||
subgraph "Claude CLI Execution"
|
||||
CLAUDE_DETECT["Claude CLI Detection<br/>CCS_CLAUDE_PATH support"]
|
||||
- `<Thinking:On|Off>` - Enable/disable reasoning blocks (default: On)
|
||||
- `<Effort:Low|Medium|High>` - Control reasoning depth (default: Medium)
|
||||
|
||||
subgraph "Execution Methods"
|
||||
SETTINGS_EXEC["claude --settings <path>"]
|
||||
INSTANCE_EXEC["CLAUDE_CONFIG_DIR=<instance> claude"]
|
||||
end
|
||||
end
|
||||
### API Key Setup
|
||||
|
||||
subgraph "API Layer"
|
||||
API["API Response<br/>Claude Sonnet 4.5<br/>GLM 4.6<br/>Kimi K2 Thinking"]
|
||||
end
|
||||
```bash
|
||||
# Edit GLMT settings
|
||||
nano ~/.ccs/glmt.settings.json
|
||||
|
||||
%% Flow connections
|
||||
USER --> DETECT
|
||||
DETECT --> PROFILE_CHECK
|
||||
PROFILE_CHECK -->|Yes| SETTINGS
|
||||
PROFILE_CHECK -->|Yes| ACCOUNT
|
||||
|
||||
SETTINGS --> CONFIG
|
||||
ACCOUNT --> CONFIG
|
||||
|
||||
CONFIG --> SETTINGS_MGR
|
||||
CONFIG --> INSTANCE_MGR
|
||||
|
||||
SETTINGS_MGR --> SETTINGS_EXEC
|
||||
INSTANCE_MGR --> INSTANCE_EXEC
|
||||
|
||||
SETTINGS_EXEC --> CLAUDE_DETECT
|
||||
INSTANCE_EXEC --> CLAUDE_DETECT
|
||||
|
||||
CLAUDE_DETECT --> API
|
||||
# Set Z.AI API key (requires coding plan)
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "your-z-ai-api-key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Debugging
|
||||
|
||||
## ⚡ Features
|
||||
|
||||
- **Instant Switching** - `ccs glm` switches to GLM, no config editing
|
||||
- **Concurrent Sessions** - Run multiple profiles simultaneously in different terminals
|
||||
- **Isolated Instances** - Each profile gets own config (`~/.ccs/instances/<profile>/`)
|
||||
- **Cross-Platform** - macOS, Linux, Windows - identical behavior
|
||||
- **Zero Downtime** - Switch instantly, no workflow interruption
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 💻 Usage Examples
|
||||
|
||||
### Basic Profile Switching
|
||||
**Enable verbose logging**:
|
||||
```bash
|
||||
ccs # Use Claude subscription (default)
|
||||
ccs glm # Use GLM fallback
|
||||
ccs kimi # Use Kimi for Coding
|
||||
ccs --version # Show CCS version and install location
|
||||
ccs glmt --verbose "your prompt"
|
||||
```
|
||||
|
||||
### Concurrent Sessions (Multi-Account)
|
||||
**Enable debug file logging**:
|
||||
```bash
|
||||
# Create multiple Claude accounts
|
||||
ccs auth create work # Company account
|
||||
ccs auth create personal # Personal account
|
||||
ccs auth create team # Team account
|
||||
export CCS_DEBUG_LOG=1
|
||||
ccs glmt --verbose "your prompt"
|
||||
# Logs: ~/.ccs/logs/
|
||||
```
|
||||
|
||||
# Terminal 1 - Work account
|
||||
**Check reasoning content**:
|
||||
```bash
|
||||
cat ~/.ccs/logs/*response-openai.json | jq '.choices[0].message.reasoning_content'
|
||||
```
|
||||
|
||||
**If absent**: Z.AI API issue (verify key, account status)
|
||||
**If present**: Transformation issue (check response-anthropic.json)
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Switching
|
||||
```bash
|
||||
ccs # Claude subscription (default)
|
||||
ccs glm # GLM (no thinking)
|
||||
ccs glmt # GLM with thinking
|
||||
ccs kimi # Kimi for Coding
|
||||
ccs --version # Show version
|
||||
```
|
||||
|
||||
### Multi-Account Setup
|
||||
```bash
|
||||
# Create accounts
|
||||
ccs auth create work
|
||||
ccs auth create personal
|
||||
|
||||
# Terminal 1
|
||||
ccs work "implement feature"
|
||||
|
||||
# Terminal 2 - Personal account (runs concurrently)
|
||||
# Terminal 2 (concurrent)
|
||||
ccs personal "review code"
|
||||
```
|
||||
|
||||
### Custom Claude CLI Path
|
||||
|
||||
Non-standard installation location:
|
||||
```bash
|
||||
export CCS_CLAUDE_PATH="/path/to/claude" # Unix
|
||||
$env:CCS_CLAUDE_PATH = "D:\Tools\Claude\claude.exe" # Windows
|
||||
```
|
||||
|
||||
See [Troubleshooting Guide](./docs/en/troubleshooting.md#claude-cli-in-non-standard-location)
|
||||
|
||||
---
|
||||
|
||||
### 🗑️ Uninstall
|
||||
## Configuration
|
||||
|
||||
Auto-created during installation via npm postinstall script.
|
||||
|
||||
**~/.ccs/config.json**:
|
||||
```json
|
||||
{
|
||||
"profiles": {
|
||||
"glm": "~/.ccs/glm.settings.json",
|
||||
"glmt": "~/.ccs/glmt.settings.json",
|
||||
"kimi": "~/.ccs/kimi.settings.json",
|
||||
"default": "~/.claude/settings.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Complete guide: [docs/en/configuration.md](./docs/en/configuration.md)
|
||||
|
||||
---
|
||||
|
||||
## Uninstall
|
||||
|
||||
**Package Managers**
|
||||
```bash
|
||||
# npm
|
||||
npm uninstall -g @kaitranntt/ccs
|
||||
|
||||
# yarn
|
||||
yarn global remove @kaitranntt/ccs
|
||||
|
||||
# pnpm
|
||||
pnpm remove -g @kaitranntt/ccs
|
||||
|
||||
# bun
|
||||
bun remove -g @kaitranntt/ccs
|
||||
```
|
||||
|
||||
**Official Uninstaller**
|
||||
|
||||
**macOS / Linux**
|
||||
```bash
|
||||
# macOS / Linux
|
||||
curl -fsSL ccs.kaitran.ca/uninstall | bash
|
||||
```
|
||||
|
||||
**Windows PowerShell**
|
||||
```powershell
|
||||
# Windows
|
||||
irm ccs.kaitran.ca/uninstall | iex
|
||||
```
|
||||
|
||||
@@ -348,6 +350,7 @@ irm ccs.kaitran.ca/uninstall | iex
|
||||
- [Installation Guide](./docs/en/installation.md)
|
||||
- [Configuration](./docs/en/configuration.md)
|
||||
- [Usage Examples](./docs/en/usage.md)
|
||||
- [System Architecture](./docs/system-architecture.md)
|
||||
- [Troubleshooting](./docs/en/troubleshooting.md)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
|
||||
+120
-4
@@ -108,6 +108,8 @@ function handleHelpCommand() {
|
||||
console.log(colored('Model Switching:', 'cyan'));
|
||||
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
|
||||
console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM 4.6 model`);
|
||||
console.log(` ${colored('ccs glmt', 'yellow')} Switch to GLM with thinking mode`);
|
||||
console.log(` ${colored('ccs glmt --verbose', 'yellow')} Enable debug logging`);
|
||||
console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi for Coding`);
|
||||
console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Use GLM and run command`);
|
||||
console.log('');
|
||||
@@ -212,6 +214,114 @@ function detectProfile(args) {
|
||||
}
|
||||
}
|
||||
|
||||
// Execute Claude CLI with embedded proxy (for GLMT profile)
|
||||
async function execClaudeWithProxy(claudeCli, profileName, args) {
|
||||
const { getSettingsPath } = require('./config-manager');
|
||||
|
||||
// 1. Read settings to get API key
|
||||
const settingsPath = getSettingsPath(profileName);
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const apiKey = settings.env.ANTHROPIC_AUTH_TOKEN;
|
||||
|
||||
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
|
||||
console.error('[X] GLMT profile requires Z.AI API key');
|
||||
console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Detect verbose flag
|
||||
const verbose = args.includes('--verbose') || args.includes('-v');
|
||||
|
||||
// 2. Spawn embedded proxy with verbose flag
|
||||
const proxyPath = path.join(__dirname, 'glmt-proxy.js');
|
||||
const proxyArgs = verbose ? ['--verbose'] : [];
|
||||
const proxy = spawn('node', [proxyPath, ...proxyArgs], {
|
||||
stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit']
|
||||
});
|
||||
|
||||
// 3. Wait for proxy ready signal (with timeout)
|
||||
let port;
|
||||
try {
|
||||
port = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Proxy startup timeout (5s)'));
|
||||
}, 5000);
|
||||
|
||||
proxy.stdout.on('data', (data) => {
|
||||
const match = data.toString().match(/PROXY_READY:(\d+)/);
|
||||
if (match) {
|
||||
clearTimeout(timeout);
|
||||
resolve(parseInt(match[1]));
|
||||
}
|
||||
});
|
||||
|
||||
proxy.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
proxy.on('exit', (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Proxy exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[X] Failed to start GLMT proxy:', error.message);
|
||||
console.error('');
|
||||
console.error('Possible causes:');
|
||||
console.error(' 1. Port conflict (unlikely with random port)');
|
||||
console.error(' 2. Node.js permission issue');
|
||||
console.error(' 3. Firewall blocking localhost');
|
||||
console.error('');
|
||||
console.error('Workarounds:');
|
||||
console.error(' - Use non-thinking mode: ccs glm "prompt"');
|
||||
console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"');
|
||||
console.error(' - Check proxy logs in ~/.ccs/logs/ (if debug enabled)');
|
||||
console.error('');
|
||||
proxy.kill();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4. Spawn Claude CLI with proxy URL
|
||||
const envVars = {
|
||||
...process.env,
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_MODEL: 'glm-4.6'
|
||||
};
|
||||
|
||||
const claude = spawn(claudeCli, args, {
|
||||
stdio: 'inherit',
|
||||
env: envVars
|
||||
});
|
||||
|
||||
// 5. Cleanup: kill proxy when Claude exits
|
||||
claude.on('exit', (code, signal) => {
|
||||
proxy.kill('SIGTERM');
|
||||
if (signal) process.kill(process.pid, signal);
|
||||
else process.exit(code || 0);
|
||||
});
|
||||
|
||||
claude.on('error', (error) => {
|
||||
console.error('[X] Claude CLI error:', error);
|
||||
proxy.kill('SIGTERM');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Also handle parent process termination (use .once to avoid duplicates)
|
||||
process.once('SIGTERM', () => {
|
||||
proxy.kill('SIGTERM');
|
||||
claude.kill('SIGTERM');
|
||||
});
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
proxy.kill('SIGTERM');
|
||||
claude.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
@@ -284,10 +394,16 @@ async function main() {
|
||||
const profileInfo = detector.detectProfileType(profile);
|
||||
|
||||
if (profileInfo.type === 'settings') {
|
||||
// EXISTING FLOW: Settings-based profile (glm, kimi)
|
||||
// Use --settings flag (backward compatible)
|
||||
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
||||
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
|
||||
// Check if this is GLMT profile (requires proxy)
|
||||
if (profileInfo.name === 'glmt') {
|
||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
||||
await execClaudeWithProxy(claudeCli, profileInfo.name, remainingArgs);
|
||||
} else {
|
||||
// EXISTING FLOW: Settings-based profile (glm, kimi)
|
||||
// Use --settings flag (backward compatible)
|
||||
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
||||
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
|
||||
}
|
||||
} else if (profileInfo.type === 'account') {
|
||||
// NEW FLOW: Account-based profile (work, personal)
|
||||
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const GlmtTransformer = require('./glmt-transformer');
|
||||
|
||||
/**
|
||||
* GlmtProxy - Embedded HTTP proxy for GLM thinking support
|
||||
*
|
||||
* Architecture:
|
||||
* - Intercepts Claude CLI → Z.AI calls
|
||||
* - Transforms Anthropic format → OpenAI format
|
||||
* - Converts reasoning_content → thinking blocks
|
||||
* - Buffered mode only (streaming not supported)
|
||||
*
|
||||
* Lifecycle:
|
||||
* - Spawned by bin/ccs.js when 'glmt' profile detected
|
||||
* - Binds to 127.0.0.1:random_port (security + avoid conflicts)
|
||||
* - Terminates when parent process exits
|
||||
*
|
||||
* Debugging:
|
||||
* - Verbose: Pass --verbose to see request/response logs
|
||||
* - Debug: Set CCS_DEBUG_LOG=1 to write logs to ~/.ccs/logs/
|
||||
*
|
||||
* Usage:
|
||||
* const proxy = new GlmtProxy({ verbose: true });
|
||||
* await proxy.start();
|
||||
*/
|
||||
class GlmtProxy {
|
||||
constructor(config = {}) {
|
||||
this.transformer = new GlmtTransformer({ verbose: config.verbose });
|
||||
this.upstreamUrl = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
|
||||
this.server = null;
|
||||
this.port = null;
|
||||
this.verbose = config.verbose || false;
|
||||
this.timeout = config.timeout || 120000; // 120s default
|
||||
}
|
||||
|
||||
/**
|
||||
* Start HTTP server on random port
|
||||
* @returns {Promise<number>} Port number
|
||||
*/
|
||||
async start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// Bind to 127.0.0.1:0 (random port for security + avoid conflicts)
|
||||
this.server.listen(0, '127.0.0.1', () => {
|
||||
this.port = this.server.address().port;
|
||||
// Signal parent process
|
||||
console.log(`PROXY_READY:${this.port}`);
|
||||
// One-time info message (always shown)
|
||||
console.error(`[glmt] Proxy listening on port ${this.port} (buffered mode)`);
|
||||
|
||||
// Debug mode notice
|
||||
if (this.transformer.debugLog) {
|
||||
console.error(`[glmt] Debug logging enabled: ${this.transformer.debugLogDir}`);
|
||||
console.error(`[glmt] WARNING: Debug logs contain full request/response data`);
|
||||
}
|
||||
|
||||
this.log(`Verbose logging enabled`);
|
||||
resolve(this.port);
|
||||
});
|
||||
|
||||
this.server.on('error', (error) => {
|
||||
console.error('[glmt-proxy] Server error:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming HTTP request
|
||||
* @param {http.IncomingMessage} req - Request
|
||||
* @param {http.ServerResponse} res - Response
|
||||
*/
|
||||
async handleRequest(req, res) {
|
||||
const startTime = Date.now();
|
||||
this.log(`Request: ${req.method} ${req.url}`);
|
||||
|
||||
try {
|
||||
// Only accept POST requests
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read request body
|
||||
const body = await this._readBody(req);
|
||||
this.log(`Request body size: ${body.length} bytes`);
|
||||
|
||||
// Parse JSON with error handling
|
||||
let anthropicRequest;
|
||||
try {
|
||||
anthropicRequest = JSON.parse(body);
|
||||
} catch (jsonError) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message: 'Invalid JSON in request body: ' + jsonError.message
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform to OpenAI format
|
||||
const { openaiRequest, thinkingConfig } =
|
||||
this.transformer.transformRequest(anthropicRequest);
|
||||
|
||||
this.log(`Transformed request, thinking: ${thinkingConfig.thinking}`);
|
||||
|
||||
// Forward to Z.AI
|
||||
const openaiResponse = await this._forwardToUpstream(
|
||||
openaiRequest,
|
||||
req.headers
|
||||
);
|
||||
|
||||
this.log(`Received response from upstream`);
|
||||
|
||||
// Transform back to Anthropic format
|
||||
const anthropicResponse = this.transformer.transformResponse(
|
||||
openaiResponse,
|
||||
thinkingConfig
|
||||
);
|
||||
|
||||
// Return to Claude CLI
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(JSON.stringify(anthropicResponse));
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.log(`Request completed in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[glmt-proxy] Request error:', error.message);
|
||||
const duration = Date.now() - startTime;
|
||||
this.log(`Request failed after ${duration}ms: ${error.message}`);
|
||||
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
type: 'proxy_error',
|
||||
message: error.message
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read request body
|
||||
* @param {http.IncomingMessage} req - Request
|
||||
* @returns {Promise<string>} Body content
|
||||
* @private
|
||||
*/
|
||||
_readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB limit
|
||||
let totalSize = 0;
|
||||
|
||||
req.on('data', chunk => {
|
||||
totalSize += chunk.length;
|
||||
if (totalSize > maxSize) {
|
||||
reject(new Error('Request body too large (max 10MB)'));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward request to Z.AI upstream
|
||||
* @param {Object} openaiRequest - OpenAI format request
|
||||
* @param {Object} originalHeaders - Original request headers
|
||||
* @returns {Promise<Object>} OpenAI response
|
||||
* @private
|
||||
*/
|
||||
_forwardToUpstream(openaiRequest, originalHeaders) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.upstreamUrl);
|
||||
const requestBody = JSON.stringify(openaiRequest);
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
path: '/api/coding/paas/v4/chat/completions', // OpenAI-compatible endpoint
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(requestBody),
|
||||
// Preserve auth header from original request
|
||||
'Authorization': originalHeaders['authorization'] || '',
|
||||
'User-Agent': 'CCS-GLMT-Proxy/1.0'
|
||||
}
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
this.log(`Forwarding to: ${url.hostname}${options.path}`);
|
||||
|
||||
// Set timeout
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
req.destroy();
|
||||
reject(new Error('Upstream request timeout'));
|
||||
}, this.timeout);
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
|
||||
const chunks = [];
|
||||
res.on('data', chunk => chunks.push(chunk));
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const body = Buffer.concat(chunks).toString();
|
||||
this.log(`Upstream response size: ${body.length} bytes`);
|
||||
|
||||
// Check for non-200 status
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(
|
||||
`Upstream error: ${res.statusCode} ${res.statusMessage}\n${body}`
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = JSON.parse(body);
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(new Error('Invalid JSON from upstream: ' + error.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(requestBody);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop proxy server
|
||||
*/
|
||||
stop() {
|
||||
if (this.server) {
|
||||
this.log('Stopping proxy server');
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message if verbose
|
||||
* @param {string} message - Message to log
|
||||
* @private
|
||||
*/
|
||||
log(message) {
|
||||
if (this.verbose) {
|
||||
console.error(`[glmt-proxy] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const verbose = args.includes('--verbose') || args.includes('-v');
|
||||
|
||||
const proxy = new GlmtProxy({ verbose });
|
||||
|
||||
proxy.start().catch(error => {
|
||||
console.error('[glmt-proxy] Failed to start:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Cleanup on signals
|
||||
process.on('SIGTERM', () => {
|
||||
proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[glmt-proxy] Uncaught exception:', error);
|
||||
proxy.stop();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = GlmtProxy;
|
||||
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* GlmtTransformer - Convert between Anthropic and OpenAI formats with thinking support
|
||||
*
|
||||
* Features:
|
||||
* - Request: Anthropic → OpenAI (inject reasoning params)
|
||||
* - Response: OpenAI reasoning_content → Anthropic thinking blocks
|
||||
* - Debug mode: Log raw data to ~/.ccs/logs/ (CCS_DEBUG_LOG=1)
|
||||
* - Verbose mode: Console logging with timestamps
|
||||
* - Validation: Self-test transformation results
|
||||
*
|
||||
* Usage:
|
||||
* const transformer = new GlmtTransformer({ verbose: true, debugLog: true });
|
||||
* const { openaiRequest, thinkingConfig } = transformer.transformRequest(req);
|
||||
* const anthropicResponse = transformer.transformResponse(resp, thinkingConfig);
|
||||
*
|
||||
* Control Tags (in user prompt):
|
||||
* <Thinking:On|Off> - Enable/disable reasoning
|
||||
* <Effort:Low|Medium|High> - Control reasoning depth
|
||||
*/
|
||||
class GlmtTransformer {
|
||||
constructor(config = {}) {
|
||||
this.defaultThinking = config.defaultThinking ?? true;
|
||||
this.verbose = config.verbose || false;
|
||||
this.debugLog = config.debugLog ?? process.env.CCS_DEBUG_LOG === '1';
|
||||
this.debugLogDir = config.debugLogDir || path.join(os.homedir(), '.ccs', 'logs');
|
||||
this.modelMaxTokens = {
|
||||
'GLM-4.6': 128000,
|
||||
'GLM-4.5': 96000,
|
||||
'GLM-4.5-air': 16000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Anthropic request to OpenAI format
|
||||
* @param {Object} anthropicRequest - Anthropic Messages API request
|
||||
* @returns {Object} { openaiRequest, thinkingConfig }
|
||||
*/
|
||||
transformRequest(anthropicRequest) {
|
||||
// Log original request
|
||||
this._writeDebugLog('request-anthropic', anthropicRequest);
|
||||
|
||||
try {
|
||||
// 1. Extract thinking control from messages
|
||||
const thinkingConfig = this._extractThinkingControl(
|
||||
anthropicRequest.messages || []
|
||||
);
|
||||
this.log(`Extracted thinking control: ${JSON.stringify(thinkingConfig)}`);
|
||||
|
||||
// 2. Map model
|
||||
const glmModel = this._mapModel(anthropicRequest.model);
|
||||
|
||||
// 3. Convert to OpenAI format
|
||||
const openaiRequest = {
|
||||
model: glmModel,
|
||||
messages: this._sanitizeMessages(anthropicRequest.messages || []),
|
||||
max_tokens: this._getMaxTokens(glmModel),
|
||||
stream: anthropicRequest.stream ?? false
|
||||
};
|
||||
|
||||
// 4. Preserve optional parameters
|
||||
if (anthropicRequest.temperature !== undefined) {
|
||||
openaiRequest.temperature = anthropicRequest.temperature;
|
||||
}
|
||||
if (anthropicRequest.top_p !== undefined) {
|
||||
openaiRequest.top_p = anthropicRequest.top_p;
|
||||
}
|
||||
|
||||
// 5. Handle streaming (not yet supported)
|
||||
// Silently override to buffered mode
|
||||
if (anthropicRequest.stream) {
|
||||
openaiRequest.stream = false;
|
||||
}
|
||||
|
||||
// 6. Inject reasoning parameters
|
||||
this._injectReasoningParams(openaiRequest, thinkingConfig);
|
||||
|
||||
// Log transformed request
|
||||
this._writeDebugLog('request-openai', openaiRequest);
|
||||
|
||||
return { openaiRequest, thinkingConfig };
|
||||
} catch (error) {
|
||||
console.error('[glmt-transformer] Request transformation error:', error);
|
||||
// Return original request with warning
|
||||
return {
|
||||
openaiRequest: anthropicRequest,
|
||||
thinkingConfig: { thinking: false },
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform OpenAI response to Anthropic format
|
||||
* @param {Object} openaiResponse - OpenAI Chat Completions response
|
||||
* @param {Object} thinkingConfig - Config from request transformation
|
||||
* @returns {Object} Anthropic Messages API response
|
||||
*/
|
||||
transformResponse(openaiResponse, thinkingConfig = {}) {
|
||||
// Log original response
|
||||
this._writeDebugLog('response-openai', openaiResponse);
|
||||
|
||||
try {
|
||||
const choice = openaiResponse.choices?.[0];
|
||||
if (!choice) {
|
||||
throw new Error('No choices in OpenAI response');
|
||||
}
|
||||
|
||||
const message = choice.message;
|
||||
const content = [];
|
||||
|
||||
// Add thinking block if reasoning_content exists
|
||||
if (message.reasoning_content) {
|
||||
const length = message.reasoning_content.length;
|
||||
const lineCount = message.reasoning_content.split('\n').length;
|
||||
const preview = message.reasoning_content
|
||||
.substring(0, 100)
|
||||
.replace(/\n/g, ' ')
|
||||
.trim();
|
||||
|
||||
this.log(`Detected reasoning_content:`);
|
||||
this.log(` Length: ${length} characters`);
|
||||
this.log(` Lines: ${lineCount}`);
|
||||
this.log(` Preview: ${preview}...`);
|
||||
|
||||
content.push({
|
||||
type: 'thinking',
|
||||
thinking: message.reasoning_content,
|
||||
signature: this._generateThinkingSignature(message.reasoning_content)
|
||||
});
|
||||
} else {
|
||||
this.log('No reasoning_content in OpenAI response');
|
||||
this.log('Note: This is expected if thinking not requested or model cannot reason');
|
||||
}
|
||||
|
||||
// Add text content
|
||||
if (message.content) {
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: message.content
|
||||
});
|
||||
}
|
||||
|
||||
// Handle tool_calls if present
|
||||
if (message.tool_calls && message.tool_calls.length > 0) {
|
||||
message.tool_calls.forEach(toolCall => {
|
||||
content.push({
|
||||
type: 'tool_use',
|
||||
id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
input: JSON.parse(toolCall.function.arguments || '{}')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const anthropicResponse = {
|
||||
id: openaiResponse.id || 'msg_' + Date.now(),
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: content,
|
||||
model: openaiResponse.model || 'glm-4.6',
|
||||
stop_reason: this._mapStopReason(choice.finish_reason),
|
||||
usage: openaiResponse.usage || {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Validate transformation in verbose mode
|
||||
if (this.verbose) {
|
||||
const validation = this._validateTransformation(anthropicResponse);
|
||||
this.log(`Transformation validation: ${validation.passed}/${validation.total} checks passed`);
|
||||
if (!validation.valid) {
|
||||
this.log(`Failed checks: ${JSON.stringify(validation.checks, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log transformed response
|
||||
this._writeDebugLog('response-anthropic', anthropicResponse);
|
||||
|
||||
return anthropicResponse;
|
||||
} catch (error) {
|
||||
console.error('[glmt-transformer] Response transformation error:', error);
|
||||
// Return minimal valid response
|
||||
return {
|
||||
id: 'msg_error_' + Date.now(),
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: '[Transformation Error] ' + error.message
|
||||
}],
|
||||
stop_reason: 'end_turn',
|
||||
usage: { input_tokens: 0, output_tokens: 0 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize messages for OpenAI API compatibility
|
||||
* Remove thinking blocks and unsupported content types
|
||||
* @param {Array} messages - Messages array
|
||||
* @returns {Array} Sanitized messages
|
||||
* @private
|
||||
*/
|
||||
_sanitizeMessages(messages) {
|
||||
return messages.map(msg => {
|
||||
// If content is a string, return as-is
|
||||
if (typeof msg.content === 'string') {
|
||||
return msg;
|
||||
}
|
||||
|
||||
// If content is an array, filter out unsupported types
|
||||
if (Array.isArray(msg.content)) {
|
||||
const sanitizedContent = msg.content
|
||||
.filter(block => {
|
||||
// Keep only text content for OpenAI
|
||||
// Filter out: thinking, tool_use, tool_result, etc.
|
||||
return block.type === 'text';
|
||||
})
|
||||
.map(block => {
|
||||
// Return just the text content
|
||||
return block;
|
||||
});
|
||||
|
||||
// If we filtered everything out, return empty string
|
||||
if (sanitizedContent.length === 0) {
|
||||
return {
|
||||
role: msg.role,
|
||||
content: ''
|
||||
};
|
||||
}
|
||||
|
||||
// If only one text block, convert to string
|
||||
if (sanitizedContent.length === 1 && sanitizedContent[0].type === 'text') {
|
||||
return {
|
||||
role: msg.role,
|
||||
content: sanitizedContent[0].text
|
||||
};
|
||||
}
|
||||
|
||||
// Return array of text blocks
|
||||
return {
|
||||
role: msg.role,
|
||||
content: sanitizedContent
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: return message as-is
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thinking control tags from user messages
|
||||
* @param {Array} messages - Messages array
|
||||
* @returns {Object} { thinking: boolean, effort: string }
|
||||
* @private
|
||||
*/
|
||||
_extractThinkingControl(messages) {
|
||||
const config = {
|
||||
thinking: this.defaultThinking,
|
||||
effort: 'medium'
|
||||
};
|
||||
|
||||
// Scan user messages for control tags
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== 'user') continue;
|
||||
|
||||
const content = msg.content;
|
||||
if (typeof content !== 'string') continue;
|
||||
|
||||
// Check for <Thinking:On|Off>
|
||||
const thinkingMatch = content.match(/<Thinking:(On|Off)>/i);
|
||||
if (thinkingMatch) {
|
||||
config.thinking = thinkingMatch[1].toLowerCase() === 'on';
|
||||
}
|
||||
|
||||
// Check for <Effort:Low|Medium|High>
|
||||
const effortMatch = content.match(/<Effort:(Low|Medium|High)>/i);
|
||||
if (effortMatch) {
|
||||
config.effort = effortMatch[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate thinking signature for Claude Code UI
|
||||
* @param {string} thinking - Thinking content
|
||||
* @returns {Object} Signature object
|
||||
* @private
|
||||
*/
|
||||
_generateThinkingSignature(thinking) {
|
||||
// Generate signature hash
|
||||
const hash = crypto.createHash('sha256')
|
||||
.update(thinking)
|
||||
.digest('hex')
|
||||
.substring(0, 16);
|
||||
|
||||
return {
|
||||
type: 'thinking_signature',
|
||||
hash: hash,
|
||||
length: thinking.length,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject reasoning parameters into OpenAI request
|
||||
* @param {Object} openaiRequest - OpenAI request to modify
|
||||
* @param {Object} thinkingConfig - Thinking configuration
|
||||
* @returns {Object} Modified request
|
||||
* @private
|
||||
*/
|
||||
_injectReasoningParams(openaiRequest, thinkingConfig) {
|
||||
// Always enable sampling for temperature/top_p to work
|
||||
openaiRequest.do_sample = true;
|
||||
|
||||
// Add thinking-specific parameters if enabled
|
||||
if (thinkingConfig.thinking) {
|
||||
// Z.AI may support these parameters (based on research)
|
||||
openaiRequest.reasoning = true;
|
||||
openaiRequest.reasoning_effort = thinkingConfig.effort;
|
||||
}
|
||||
|
||||
return openaiRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Anthropic model to GLM model
|
||||
* @param {string} anthropicModel - Anthropic model name
|
||||
* @returns {string} GLM model name
|
||||
* @private
|
||||
*/
|
||||
_mapModel(anthropicModel) {
|
||||
// Default to GLM-4.6 (latest and most capable)
|
||||
return 'GLM-4.6';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max tokens for model
|
||||
* @param {string} model - Model name
|
||||
* @returns {number} Max tokens
|
||||
* @private
|
||||
*/
|
||||
_getMaxTokens(model) {
|
||||
return this.modelMaxTokens[model] || 128000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map OpenAI stop reason to Anthropic stop reason
|
||||
* @param {string} openaiReason - OpenAI finish_reason
|
||||
* @returns {string} Anthropic stop_reason
|
||||
* @private
|
||||
*/
|
||||
_mapStopReason(openaiReason) {
|
||||
const mapping = {
|
||||
'stop': 'end_turn',
|
||||
'length': 'max_tokens',
|
||||
'tool_calls': 'tool_use',
|
||||
'content_filter': 'stop_sequence'
|
||||
};
|
||||
return mapping[openaiReason] || 'end_turn';
|
||||
}
|
||||
|
||||
/**
|
||||
* Write debug log to file
|
||||
* @param {string} type - 'request-anthropic', 'request-openai', 'response-openai', 'response-anthropic'
|
||||
* @param {object} data - Data to log
|
||||
* @private
|
||||
*/
|
||||
_writeDebugLog(type, data) {
|
||||
if (!this.debugLog) return;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('.')[0];
|
||||
const filename = `${timestamp}-${type}.json`;
|
||||
const filepath = path.join(this.debugLogDir, filename);
|
||||
|
||||
// Ensure directory exists
|
||||
fs.mkdirSync(this.debugLogDir, { recursive: true });
|
||||
|
||||
// Write file (pretty-printed)
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
|
||||
if (this.verbose) {
|
||||
this.log(`Debug log written: ${filepath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[glmt-transformer] Failed to write debug log: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transformed Anthropic response
|
||||
* @param {object} anthropicResponse - Response to validate
|
||||
* @returns {object} Validation results
|
||||
* @private
|
||||
*/
|
||||
_validateTransformation(anthropicResponse) {
|
||||
const checks = {
|
||||
hasContent: Boolean(anthropicResponse.content && anthropicResponse.content.length > 0),
|
||||
hasThinking: anthropicResponse.content?.some(block => block.type === 'thinking') || false,
|
||||
hasText: anthropicResponse.content?.some(block => block.type === 'text') || false,
|
||||
validStructure: anthropicResponse.type === 'message' && anthropicResponse.role === 'assistant',
|
||||
hasUsage: Boolean(anthropicResponse.usage)
|
||||
};
|
||||
|
||||
const passed = Object.values(checks).filter(Boolean).length;
|
||||
const total = Object.keys(checks).length;
|
||||
|
||||
return { checks, passed, total, valid: passed === total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message if verbose
|
||||
* @param {string} message - Message to log
|
||||
* @private
|
||||
*/
|
||||
log(message) {
|
||||
if (this.verbose) {
|
||||
const timestamp = new Date().toTimeString().split(' ')[0]; // HH:MM:SS
|
||||
console.error(`[glmt-transformer] [${timestamp}] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GlmtTransformer;
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/coding/paas/v4/chat/completions",
|
||||
"ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_TEMPERATURE": "0.2",
|
||||
"ANTHROPIC_MAX_TOKENS": "65536",
|
||||
"MAX_THINKING_TOKENS": "32768",
|
||||
"ENABLE_STREAMING": "true",
|
||||
"ANTHROPIC_SAFE_MODE": "false",
|
||||
"API_TIMEOUT_MS": "3000000"
|
||||
},
|
||||
"alwaysThinkingEnabled": true
|
||||
}
|
||||
@@ -522,4 +522,55 @@ ccs glm
|
||||
- CI/CD environments
|
||||
- Log file generation
|
||||
- Terminals without color support
|
||||
- Accessibility preferences
|
||||
- Accessibility preferences
|
||||
|
||||
---
|
||||
|
||||
## GLMT Troubleshooting
|
||||
|
||||
### Thinking Blocks Not Visible
|
||||
|
||||
1. **Enable debug logging**:
|
||||
```bash
|
||||
export CCS_DEBUG_LOG=1
|
||||
ccs glmt --verbose "test"
|
||||
```
|
||||
|
||||
2. **Check raw response**:
|
||||
```bash
|
||||
cat ~/.ccs/logs/*response-openai.json | jq '.choices[0].message.reasoning_content'
|
||||
```
|
||||
|
||||
3. **Scenarios**:
|
||||
- **reasoning_content present**: Transformation issue → check response-anthropic.json
|
||||
- **reasoning_content absent**: Z.AI API issue → verify API key/account status
|
||||
|
||||
### Duplicate "Enchanting" Lines
|
||||
|
||||
**Symptom**: Two spinner lines during thinking
|
||||
|
||||
**Cause**: Terminal rendering (not CCS issue)
|
||||
|
||||
**Solutions**:
|
||||
- Use native terminal (iTerm2, GNOME Terminal, Windows Terminal)
|
||||
- Exit tmux/screen before running
|
||||
- Accept visual glitch (functionality unaffected)
|
||||
|
||||
### Proxy Startup Timeout
|
||||
|
||||
**Error**: "Proxy startup timeout (5s)"
|
||||
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Try non-thinking mode
|
||||
ccs glm "your prompt"
|
||||
|
||||
# Check Node.js
|
||||
node --version # Requires ≥14
|
||||
|
||||
# Check port availability
|
||||
netstat -an | grep 127.0.0.1
|
||||
|
||||
# Verbose details
|
||||
ccs glmt --verbose "test"
|
||||
```
|
||||
+267
-17
@@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
CCS (Claude Code Switch) is a lightweight CLI wrapper that provides instant profile switching between Claude Sonnet 4.5 and GLM 4.6 models. The architecture has been recently simplified to achieve a 35% reduction in codebase size while maintaining all functionality.
|
||||
CCS (Claude Code Switch) is a lightweight CLI wrapper that provides instant profile switching between Claude Sonnet 4.5 and GLM 4.6 models. Current version **v3.3.0** features GLMT thinking mode with embedded proxy architecture, debug logging, and refined configuration management.
|
||||
|
||||
## Core Architecture Principles
|
||||
|
||||
@@ -62,32 +62,57 @@ graph TB
|
||||
|
||||
**Key Responsibilities**:
|
||||
- Argument parsing and profile detection
|
||||
- Special command handling (--version, --help, auth) [--install/--uninstall WIP]
|
||||
- Special command handling (--version, --help, auth, doctor) [--install/--uninstall WIP]
|
||||
- Profile type routing (settings-based vs account-based)
|
||||
- GLMT proxy lifecycle management
|
||||
- Unified process execution through `execClaude()`
|
||||
- Error propagation and exit code management
|
||||
- Auto-recovery for missing configuration
|
||||
|
||||
**Architecture with Concurrent Sessions**:
|
||||
**Architecture with GLMT Support (v3.3.0)**:
|
||||
```mermaid
|
||||
graph LR
|
||||
graph TD
|
||||
subgraph "Entry Point"
|
||||
ARGS[Parse Arguments]
|
||||
SPECIAL[Handle Special Commands]
|
||||
RECOVER[Auto-recovery]
|
||||
DETECT[ProfileDetector]
|
||||
SETTINGS[Settings-based Profile]
|
||||
GLMT{GLMT Profile?}
|
||||
ACCOUNT[Account-based Profile]
|
||||
PROXY[Spawn Proxy]
|
||||
EXEC[Execute Claude]
|
||||
end
|
||||
|
||||
ARGS --> SPECIAL
|
||||
SPECIAL --> DETECT
|
||||
SPECIAL --> RECOVER
|
||||
RECOVER --> DETECT
|
||||
DETECT --> SETTINGS
|
||||
DETECT --> ACCOUNT
|
||||
SETTINGS --> EXEC
|
||||
SETTINGS --> GLMT
|
||||
GLMT -->|Yes| PROXY
|
||||
GLMT -->|No| EXEC
|
||||
PROXY --> EXEC
|
||||
ACCOUNT --> EXEC
|
||||
```
|
||||
|
||||
**Key Enhancement ()**: Dual-path execution supporting both `--settings` flag (backward compatible) and `CLAUDE_CONFIG_DIR` env var (concurrent sessions).
|
||||
**Key Enhancements**:
|
||||
- **v3.3.0**: GLMT proxy spawning with verbose flag detection, API key validation, 5s timeout
|
||||
- **v3.2.0**: Dual-path execution supporting both `--settings` flag (backward compatible) and `CLAUDE_CONFIG_DIR` env var (concurrent sessions)
|
||||
- **v3.1.0**: Auto-recovery manager for missing configs
|
||||
|
||||
**GLMT-Specific Logic**:
|
||||
```javascript
|
||||
// Check if GLMT profile
|
||||
if (profileInfo.name === 'glmt') {
|
||||
// 1. Read API key from settings
|
||||
// 2. Spawn proxy with --verbose flag (if detected in args)
|
||||
// 3. Wait for PROXY_READY:port signal (5s timeout)
|
||||
// 4. Spawn Claude CLI with proxy URL
|
||||
// 5. Kill proxy when Claude exits
|
||||
await execClaudeWithProxy(claudeCli, 'glmt', remainingArgs);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Configuration Manager (`bin/config-manager.js`)
|
||||
|
||||
@@ -236,6 +261,132 @@ graph TD
|
||||
"default": "work"
|
||||
}
|
||||
```
|
||||
## GLMT Architecture (v3.2.0+)
|
||||
|
||||
### Overview
|
||||
|
||||
GLMT (GLM with Thinking) uses an embedded HTTP proxy to enable thinking mode support for GLM 4.6. The proxy converts between Anthropic and OpenAI formats, injecting reasoning parameters and transforming `reasoning_content` into thinking blocks.
|
||||
|
||||
### Components
|
||||
|
||||
**1. GLMT Transformer (`bin/glmt-transformer.js`)**
|
||||
- Converts Anthropic Messages API → OpenAI Chat Completions format
|
||||
- Extracts thinking control tags: `<Thinking:On|Off>`, `<Effort:Low|Medium|High>`
|
||||
- Injects reasoning parameters: `reasoning: true`, `reasoning_effort`
|
||||
- Transforms OpenAI `reasoning_content` → Anthropic thinking blocks
|
||||
- Generates thinking signatures for Claude Code UI
|
||||
- Debug logging to `~/.ccs/logs/` when `CCS_DEBUG_LOG=1`
|
||||
|
||||
**2. GLMT Proxy (`bin/glmt-proxy.js`)**
|
||||
- Embedded HTTP server on `127.0.0.1:random_port`
|
||||
- Intercepts Claude CLI → Z.AI requests
|
||||
- Lifecycle tied to parent process
|
||||
- Buffered mode only (streaming not supported)
|
||||
- Request timeout: 120s default
|
||||
|
||||
### GLMT Execution Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CCS as ccs.js
|
||||
participant Proxy as glmt-proxy.js
|
||||
participant Transformer as glmt-transformer.js
|
||||
participant ZAI as Z.AI API
|
||||
participant Claude as Claude CLI
|
||||
|
||||
User->>CCS: ccs glmt "solve problem"
|
||||
CCS->>CCS: Read GLMT settings (API key, config)
|
||||
CCS->>Proxy: Spawn proxy with --verbose flag
|
||||
Proxy->>Proxy: Bind to 127.0.0.1:random_port
|
||||
Proxy-->>CCS: PROXY_READY:port
|
||||
CCS->>Claude: Spawn with ANTHROPIC_BASE_URL=http://127.0.0.1:port
|
||||
Claude->>Proxy: POST /v1/messages (Anthropic format)
|
||||
Proxy->>Transformer: transformRequest(anthropicRequest)
|
||||
Transformer->>Transformer: Extract thinking control tags
|
||||
Transformer->>Transformer: Inject reasoning params
|
||||
Transformer-->>Proxy: OpenAI format request
|
||||
Proxy->>ZAI: POST /api/coding/paas/v4/chat/completions
|
||||
ZAI-->>Proxy: OpenAI response (with reasoning_content)
|
||||
Proxy->>Transformer: transformResponse(openaiResponse)
|
||||
Transformer->>Transformer: Convert reasoning_content → thinking blocks
|
||||
Transformer-->>Proxy: Anthropic format response
|
||||
Proxy-->>Claude: Return Anthropic response
|
||||
Claude-->>User: Display with thinking blocks
|
||||
Claude->>CCS: Exit
|
||||
CCS->>Proxy: Kill (SIGTERM)
|
||||
```
|
||||
|
||||
### Debug Mode Architecture
|
||||
|
||||
**Verbose Logging** (`--verbose` flag):
|
||||
- Console logging with timestamps
|
||||
- Request/response size tracking
|
||||
- Transformation validation results
|
||||
- Proxy lifecycle events
|
||||
|
||||
**Debug File Logging** (`CCS_DEBUG_LOG=1`):
|
||||
- Writes to `~/.ccs/logs/`
|
||||
- Files: `{timestamp}-request-anthropic.json`, `request-openai.json`, `response-openai.json`, `response-anthropic.json`
|
||||
- Pretty-printed JSON with full request/response data
|
||||
- **[!] Warning**: Contains sensitive data (API keys, prompts)
|
||||
|
||||
**Debug Workflow**:
|
||||
```bash
|
||||
# Enable both verbose and debug logging
|
||||
export CCS_DEBUG_LOG=1
|
||||
ccs glmt --verbose "test prompt"
|
||||
|
||||
# Check reasoning content
|
||||
cat ~/.ccs/logs/*response-openai.json | jq '.choices[0].message.reasoning_content'
|
||||
|
||||
# Verify transformation
|
||||
cat ~/.ccs/logs/*response-anthropic.json | jq '.content[] | select(.type=="thinking")'
|
||||
```
|
||||
|
||||
### Configuration Migration (v3.2.0 → v3.3.0)
|
||||
|
||||
**Automatic Migration** (postinstall script):
|
||||
```javascript
|
||||
// Added fields in v3.3.0
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_TEMPERATURE": "0.2", // New
|
||||
"ANTHROPIC_MAX_TOKENS": "65536", // New
|
||||
"MAX_THINKING_TOKENS": "32768", // New
|
||||
"ENABLE_STREAMING": "true", // New
|
||||
"ANTHROPIC_SAFE_MODE": "false", // New
|
||||
"API_TIMEOUT_MS": "3000000" // New (50 minutes)
|
||||
},
|
||||
"alwaysThinkingEnabled": true // New
|
||||
}
|
||||
```
|
||||
|
||||
**Removed/Obsolete Fields** (from v3.2.0):
|
||||
- `BASH_DEFAULT_TIMEOUT_MS` - Moved to Claude CLI config
|
||||
- `BASH_MAX_TIMEOUT_MS` - Moved to Claude CLI config
|
||||
- `DISABLE_TELEMETRY` - No longer needed
|
||||
- `ENABLE_THINKING` - Replaced by `alwaysThinkingEnabled`
|
||||
|
||||
### Proxy Lifecycle Management
|
||||
|
||||
**Startup**:
|
||||
1. CCS spawns `node bin/glmt-proxy.js`
|
||||
2. Proxy binds to `127.0.0.1:0` (random port)
|
||||
3. Proxy emits `PROXY_READY:port` to stdout
|
||||
4. CCS reads port, spawns Claude CLI with proxy URL
|
||||
5. Timeout: 5s (configurable)
|
||||
|
||||
**Cleanup**:
|
||||
- Claude CLI exits → CCS kills proxy (`SIGTERM`)
|
||||
- Parent process dies → Proxy auto-terminates
|
||||
- Uncaught exception → Proxy logs and exits
|
||||
|
||||
**Error Handling**:
|
||||
- Proxy startup timeout → Show workaround (use `ccs glm`)
|
||||
- Port conflict → Uses random port (unlikely)
|
||||
- Upstream timeout → 120s default, configurable
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
### Settings-Based Profile Execution Flow (Backward Compatible)
|
||||
@@ -318,27 +469,62 @@ sequenceDiagram
|
||||
|
||||
```
|
||||
~/.ccs/
|
||||
├── config.json # Settings-based profile mappings (glm, kimi)
|
||||
├── config.json # Settings-based profile mappings (glm, glmt, kimi)
|
||||
├── profiles.json # Account-based profile metadata (work, personal)
|
||||
├── glm.settings.json # GLM configuration
|
||||
├── glm.settings.json # GLM configuration (Anthropic endpoint)
|
||||
├── glmt.settings.json # GLMT configuration (v3.3.0 with thinking mode)
|
||||
├── kimi.settings.json # Kimi configuration
|
||||
├── config.json.backup # Single backup file
|
||||
├── VERSION # Version information
|
||||
├── logs/ # Debug logs (CCS_DEBUG_LOG=1)
|
||||
│ ├── {timestamp}-request-anthropic.json
|
||||
│ ├── {timestamp}-request-openai.json
|
||||
│ ├── {timestamp}-response-openai.json
|
||||
│ └── {timestamp}-response-anthropic.json
|
||||
├── shared/ # Shared across all profiles (v3.1+)
|
||||
│ ├── commands/ # Slash commands
|
||||
│ ├── skills/ # Agent skills
|
||||
│ └── agents/ # Agent configs
|
||||
├── accounts/ # Encrypted credential vaults
|
||||
│ ├── .salt # Key derivation salt
|
||||
│ ├── work.json.enc # Work account credentials (encrypted)
|
||||
│ └── personal.json.enc # Personal account credentials (encrypted)
|
||||
└── instances/ # Isolated Claude instances (+)
|
||||
└── instances/ # Isolated Claude instances
|
||||
├── work/ # Work account instance
|
||||
│ ├── session-env/
|
||||
│ ├── todos/
|
||||
│ ├── logs/
|
||||
│ ├── commands@ → shared/commands/
|
||||
│ ├── skills@ → shared/skills/
|
||||
│ ├── agents@ → shared/agents/
|
||||
│ ├── .credentials.json
|
||||
│ └── ...
|
||||
└── personal/ # Personal account instance
|
||||
├── session-env/
|
||||
├── todos/
|
||||
└── ...
|
||||
|
||||
bin/ # CCS source files
|
||||
├── ccs.js # Main entry point (v3.3.0)
|
||||
├── glmt-proxy.js # Embedded HTTP proxy (v3.2.0+)
|
||||
├── glmt-transformer.js # Format conversion (v3.2.0+)
|
||||
├── config-manager.js # Configuration handling
|
||||
├── claude-detector.js # Claude CLI detection
|
||||
├── instance-manager.js # Instance orchestration
|
||||
├── shared-manager.js # Shared data symlinks (v3.1+)
|
||||
├── profile-detector.js # Profile type detection
|
||||
├── profile-registry.js # Account profile metadata
|
||||
├── helpers.js # Utility functions
|
||||
├── error-manager.js # Error handling
|
||||
├── recovery-manager.js # Auto-recovery
|
||||
├── auth-commands.js # Multi-account management
|
||||
└── doctor.js # Health check diagnostics
|
||||
|
||||
config/
|
||||
└── base-glmt.settings.json # GLMT template (v3.3.0)
|
||||
|
||||
scripts/
|
||||
└── postinstall.js # Auto-configuration + migration
|
||||
```
|
||||
|
||||
### Configuration Schema
|
||||
@@ -354,6 +540,7 @@ sequenceDiagram
|
||||
|
||||
### Settings File Format
|
||||
|
||||
**GLM Settings (Anthropic endpoint)**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
@@ -367,6 +554,29 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
**GLMT Settings (v3.3.0 with thinking mode)**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/coding/paas/v4/chat/completions",
|
||||
"ANTHROPIC_AUTH_TOKEN": "your_api_key",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_TEMPERATURE": "0.2",
|
||||
"ANTHROPIC_MAX_TOKENS": "65536",
|
||||
"MAX_THINKING_TOKENS": "32768",
|
||||
"ENABLE_STREAMING": "true",
|
||||
"ANTHROPIC_SAFE_MODE": "false",
|
||||
"API_TIMEOUT_MS": "3000000"
|
||||
},
|
||||
"alwaysThinkingEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**[i] Note**: All env values are strings (not booleans/numbers) for PowerShell compatibility.
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Inherent Security Model
|
||||
@@ -503,11 +713,36 @@ graph LR
|
||||
|
||||
### Installation Process
|
||||
|
||||
1. **Package Download**: User installs via npm
|
||||
2. **Post-install Script**: Automatically creates configuration
|
||||
3. **Path Configuration**: Sets up executable in system PATH
|
||||
4. **Validation**: Ensures Claude CLI is available
|
||||
5. **Ready State**: System ready for profile switching
|
||||
1. **Package Download**: User installs via npm/yarn/pnpm/bun
|
||||
2. **Post-install Script** (`scripts/postinstall.js`):
|
||||
- Creates `~/.ccs/` directory structure
|
||||
- Creates `~/.ccs/shared/` (commands, skills, agents)
|
||||
- Migrates from v3.1.1 → v3.2.0 (if needed)
|
||||
- Migrates GLMT configs from v3.2.0 → v3.3.0 (adds new fields)
|
||||
- Creates `config.json` (glm, glmt, kimi, default)
|
||||
- Creates `glm.settings.json` (Anthropic endpoint)
|
||||
- Creates `glmt.settings.json` (OpenAI endpoint + thinking mode)
|
||||
- Creates `kimi.settings.json` (Kimi endpoint)
|
||||
- Creates `~/.claude/settings.json` (if missing)
|
||||
- Validates configuration (checks JSON syntax, file existence)
|
||||
- Shows API key setup instructions
|
||||
3. **Path Configuration**: npm automatically adds to PATH
|
||||
4. **Ready State**: System ready for profile switching
|
||||
|
||||
**Idempotency**: Safe to run multiple times, preserves existing configs
|
||||
|
||||
**Migration Logic** (v3.3.0):
|
||||
```javascript
|
||||
// Auto-adds missing fields to existing GLMT configs
|
||||
const envDefaults = {
|
||||
ANTHROPIC_TEMPERATURE: '0.2',
|
||||
ANTHROPIC_MAX_TOKENS: '65536',
|
||||
MAX_THINKING_TOKENS: '32768',
|
||||
ENABLE_STREAMING: 'true',
|
||||
ANTHROPIC_SAFE_MODE: 'false',
|
||||
API_TIMEOUT_MS: '3000000'
|
||||
};
|
||||
```
|
||||
|
||||
## Concurrent Sessions Architecture ()
|
||||
|
||||
@@ -614,10 +849,25 @@ The CCS system architecture successfully balances simplicity with functionality:
|
||||
- **Performance optimization** achieves 35% code reduction with identical functionality
|
||||
- **Clean separation of concerns** makes the codebase maintainable and extensible
|
||||
|
||||
** Enhancements**:
|
||||
**v3.3.0 Features**:
|
||||
- **GLMT thinking mode**: Embedded proxy for GLM reasoning support
|
||||
- **Debug logging**: File-based logging to `~/.ccs/logs/` when `CCS_DEBUG_LOG=1`
|
||||
- **Verbose mode**: Console logging with `--verbose` flag
|
||||
- **Configuration migration**: Auto-upgrade v3.2.0 configs with new fields
|
||||
- **Enhanced settings**: Temperature, max tokens, thinking controls, API timeout
|
||||
- **Transformation validation**: Self-test request/response conversions
|
||||
|
||||
**v3.2.0 Enhancements**:
|
||||
- Concurrent sessions for account-based profiles
|
||||
- Profile type detection and routing (settings vs account)
|
||||
- Instance isolation with credential synchronization
|
||||
- Backward compatibility maintained for all existing profiles
|
||||
|
||||
The architecture demonstrates how thoughtful design can add sophisticated features (concurrent sessions, multi-account management) while maintaining simplicity, security, and backward compatibility.
|
||||
**v3.3.0 Architecture Highlights**:
|
||||
1. **Proxy Architecture**: Buffered mode with 120s timeout, random port binding
|
||||
2. **Debug Infrastructure**: Dual logging (console + file) with timestamp tracking
|
||||
3. **Config Management**: Automatic migration, string-only env vars, PowerShell compatibility
|
||||
4. **Thinking Mode**: Control tags, reasoning parameters, signature generation
|
||||
5. **Error Recovery**: Timeout handling, fallback options, clear error messages
|
||||
|
||||
The architecture demonstrates how thoughtful design can add sophisticated features (thinking mode, debug infrastructure, multi-account management) while maintaining simplicity, security, and backward compatibility.
|
||||
@@ -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.2.0"
|
||||
$CcsVersion = "3.3.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if ($ScriptDir) {
|
||||
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
# IMPORTANT: Update this version when releasing new versions!
|
||||
# This hardcoded version is used for standalone installations (curl | bash)
|
||||
# For git installations, VERSION file is read if available
|
||||
CCS_VERSION="3.2.0"
|
||||
CCS_VERSION="3.3.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
CCS_VERSION="3.2.0"
|
||||
CCS_VERSION="3.3.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
||||
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
|
||||
@@ -51,6 +51,7 @@ show_help() {
|
||||
echo -e "${CYAN}Model Switching:${RESET}"
|
||||
echo -e " ${YELLOW}ccs${RESET} Use default Claude account"
|
||||
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM 4.6 model"
|
||||
echo -e " ${YELLOW}ccs glmt${RESET} Switch to GLM with thinking mode"
|
||||
echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi for Coding"
|
||||
echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Use GLM and run command"
|
||||
echo ""
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
$CcsVersion = "3.2.0"
|
||||
$CcsVersion = "3.3.0"
|
||||
$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"
|
||||
@@ -107,6 +107,7 @@ function Show-Help {
|
||||
Write-ColorLine "Model Switching:" "Cyan"
|
||||
Write-ColorLine " ccs Use default Claude account" "Yellow"
|
||||
Write-ColorLine " ccs glm Switch to GLM 4.6 model" "Yellow"
|
||||
Write-ColorLine " ccs glmt Switch to GLM with thinking mode" "Yellow"
|
||||
Write-ColorLine " ccs kimi Switch to Kimi for Coding" "Yellow"
|
||||
Write-ColorLine " ccs glm 'debug this code' Use GLM and run command" "Yellow"
|
||||
Write-Host ""
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
+105
-1
@@ -33,6 +33,7 @@ function validateConfiguration() {
|
||||
const requiredFiles = [
|
||||
{ path: path.join(ccsDir, 'config.json'), name: 'config.json' },
|
||||
{ path: path.join(ccsDir, 'glm.settings.json'), name: 'glm.settings.json' },
|
||||
{ path: path.join(ccsDir, 'glmt.settings.json'), name: 'glmt.settings.json' },
|
||||
{ path: path.join(ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' }
|
||||
];
|
||||
|
||||
@@ -108,6 +109,7 @@ function createConfigFiles() {
|
||||
const config = {
|
||||
profiles: {
|
||||
glm: '~/.ccs/glm.settings.json',
|
||||
glmt: '~/.ccs/glmt.settings.json',
|
||||
kimi: '~/.ccs/kimi.settings.json',
|
||||
default: '~/.claude/settings.json'
|
||||
}
|
||||
@@ -120,7 +122,21 @@ function createConfigFiles() {
|
||||
|
||||
console.log('[OK] Created config: ~/.ccs/config.json');
|
||||
} else {
|
||||
console.log('[OK] Config exists: ~/.ccs/config.json (preserved)');
|
||||
// Update existing config with glmt if missing (migration for v3.x users)
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
// Ensure profiles object exists
|
||||
if (!config.profiles) {
|
||||
config.profiles = {};
|
||||
}
|
||||
if (!config.profiles.glmt) {
|
||||
config.profiles.glmt = '~/.ccs/glmt.settings.json';
|
||||
const tmpPath = `${configPath}.tmp`;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tmpPath, configPath);
|
||||
console.log('[OK] Updated config with GLMT profile');
|
||||
} else {
|
||||
console.log('[OK] Config exists: ~/.ccs/config.json (preserved)');
|
||||
}
|
||||
}
|
||||
|
||||
// Create glm.settings.json if missing
|
||||
@@ -152,6 +168,94 @@ function createConfigFiles() {
|
||||
console.log('[OK] GLM profile exists: ~/.ccs/glm.settings.json (preserved)');
|
||||
}
|
||||
|
||||
// Create glmt.settings.json if missing
|
||||
const glmtSettingsPath = path.join(ccsDir, 'glmt.settings.json');
|
||||
if (!fs.existsSync(glmtSettingsPath)) {
|
||||
const glmtSettings = {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
|
||||
ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE',
|
||||
ANTHROPIC_MODEL: 'glm-4.6',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6',
|
||||
ANTHROPIC_TEMPERATURE: '0.2',
|
||||
ANTHROPIC_MAX_TOKENS: '65536',
|
||||
MAX_THINKING_TOKENS: '32768',
|
||||
ENABLE_STREAMING: 'true',
|
||||
ANTHROPIC_SAFE_MODE: 'false',
|
||||
API_TIMEOUT_MS: '3000000'
|
||||
},
|
||||
alwaysThinkingEnabled: true
|
||||
};
|
||||
|
||||
// Atomic write
|
||||
const tmpPath = `${glmtSettingsPath}.tmp`;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(glmtSettings, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tmpPath, glmtSettingsPath);
|
||||
|
||||
console.log('[OK] Created GLMT profile: ~/.ccs/glmt.settings.json');
|
||||
console.log('');
|
||||
console.log(' [!] Configure GLMT API key:');
|
||||
console.log(' 1. Get key from: https://api.z.ai');
|
||||
console.log(' 2. Edit: ~/.ccs/glmt.settings.json');
|
||||
console.log(' 3. Replace: YOUR_GLM_API_KEY_HERE');
|
||||
console.log(' Note: GLMT enables GLM thinking mode (reasoning)');
|
||||
console.log(' Defaults: Temperature 0.2, thinking enabled, 50min timeout');
|
||||
} else {
|
||||
console.log('[OK] GLMT profile exists: ~/.ccs/glmt.settings.json (preserved)');
|
||||
}
|
||||
|
||||
// Migrate existing GLMT configs to include new defaults (v3.3.0)
|
||||
if (fs.existsSync(glmtSettingsPath)) {
|
||||
try {
|
||||
const existing = JSON.parse(fs.readFileSync(glmtSettingsPath, 'utf8'));
|
||||
let updated = false;
|
||||
|
||||
// Ensure env object exists
|
||||
if (!existing.env) {
|
||||
existing.env = {};
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Add missing env vars (preserve existing values)
|
||||
const envDefaults = {
|
||||
ANTHROPIC_TEMPERATURE: '0.2',
|
||||
ANTHROPIC_MAX_TOKENS: '65536',
|
||||
MAX_THINKING_TOKENS: '32768',
|
||||
ENABLE_STREAMING: 'true',
|
||||
ANTHROPIC_SAFE_MODE: 'false',
|
||||
API_TIMEOUT_MS: '3000000'
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(envDefaults)) {
|
||||
if (existing.env[key] === undefined) {
|
||||
existing.env[key] = value;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add alwaysThinkingEnabled if missing
|
||||
if (existing.alwaysThinkingEnabled === undefined) {
|
||||
existing.alwaysThinkingEnabled = true;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Write back if updated
|
||||
if (updated) {
|
||||
const tmpPath = `${glmtSettingsPath}.tmp`;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tmpPath, glmtSettingsPath);
|
||||
console.log('[OK] Migrated GLMT config with new defaults (v3.3.0)');
|
||||
console.log(' Added: temperature, max_tokens, thinking settings, alwaysThinkingEnabled');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[!] GLMT config migration failed:', err.message);
|
||||
console.warn(' Existing config preserved, may be missing new defaults');
|
||||
console.warn(' You can manually add fields or delete file to regenerate');
|
||||
}
|
||||
}
|
||||
|
||||
// Create kimi.settings.json if missing
|
||||
const kimiSettingsPath = path.join(ccsDir, 'kimi.settings.json');
|
||||
if (!fs.existsSync(kimiSettingsPath)) {
|
||||
|
||||
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