feat: deprecate GLMT user-facing surfaces

This commit is contained in:
Tam Nhu Tran
2026-03-17 15:49:29 -04:00
parent 33efaa77b9
commit 5525098400
33 changed files with 404 additions and 752 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ Tests set `process.env.CCS_HOME` to a temp directory. Code using `os.homedir()`
## Core Function
CLI wrapper for instant switching between multiple provider accounts and alternative models (GLM, GLMT, Kimi). See README.md for user documentation.
CLI wrapper for instant switching between multiple provider accounts and alternative models (GLM, Kimi, and other API profiles). See README.md for user documentation.
## Design Principles (ENFORCE STRICTLY)
@@ -232,7 +232,7 @@ dist/ui/ → Built UI bundle (served by Express)
1. **CLIProxy hardcoded**: gemini, codex, agy → OAuth-based, zero config
2. **CLIProxy variants**: `config.cliproxy` section → user-defined providers
3. **Settings-based**: `config.profiles` section → GLM, GLMT, Kimi
3. **Settings-based**: `config.profiles` section → GLM, legacy GLMT compatibility, Kimi
4. **Account-based**: `profiles.json` → isolated instances via `CLAUDE_CONFIG_DIR`
### Settings Format (CRITICAL)
+5 -5
View File
@@ -40,7 +40,7 @@ src/
│ ├── cli.ts # CLI types (ParsedArgs, ExitCode)
│ ├── config.ts # Config types (Settings, EnvVars)
│ ├── delegation.ts # Delegation types (sessions, events)
│ ├── glmt.ts # GLMT types (messages, transforms)
│ ├── glmt.ts # Legacy transformer types (messages, transforms)
│ └── utils.ts # Utility types (ErrorCode, LogLevel)
├── commands/ # CLI command handlers
@@ -115,11 +115,11 @@ src/
│ ├── index.ts # Barrel export
│ └── copilot-package-manager.ts # Package management (515 lines)
├── glmt/ # GLM/GLMT integration
├── glmt/ # Legacy transformer internals kept for compatibility
│ ├── index.ts # Barrel export
│ ├── pipeline/ # Processing pipeline
│ │ └── index.ts
│ ├── glmt-proxy.ts # Main proxy (675 lines)
│ ├── glmt-proxy.ts # Legacy proxy runtime kept for internal compatibility
│ └── delta-accumulator.ts # Delta processing (484 lines)
├── delegation/ # Task delegation & headless execution
@@ -201,7 +201,7 @@ src/
| Targets | `targets/` | Multi-CLI adapter pattern (Claude Code, Factory Droid, extensible) |
| Auth | `auth/`, `cliproxy/auth/` | Authentication across providers |
| Config | `config/`, `types/` | Configuration & type definitions |
| Providers | `cliproxy/`, `copilot/`, `glmt/` | Provider integrations (7 CLIProxy providers: gemini, codex, agy, qwen, iflow, kiro, ghcp) |
| Providers | `cliproxy/`, `copilot/`, `glmt/` | Provider integrations plus retained legacy transformer internals |
| Quota | `cliproxy/quota-*.ts`, `account-manager.ts` | Hybrid quota management (v7.14) |
| Remote Proxy | `cliproxy/remote-*.ts`, `proxy-config-resolver.ts` | Remote CLIProxy support (v7.1) |
| Image Analysis | `utils/image-analysis/`, `utils/hooks/` | Vision model proxying (v7.34) |
@@ -477,7 +477,7 @@ ui/src/
| File | Lines | Status |
|------|-------|--------|
| model-pricing.ts | 676 | Data file - acceptable |
| glmt-proxy.ts | 675 | Complex streaming - acceptable |
| glmt-proxy.ts | 675 | Legacy internal compatibility path - acceptable for now |
| cliproxy-executor.ts | 666 | Core logic - acceptable |
| cliproxy-command.ts | 634 | Could split if needed |
| usage/handlers.ts | 633 | Could split if needed |
+2 -1
View File
@@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic
### Recent Fixes
- **2026-03-17**: Deprecated user-facing GLMT discovery across CLI help, completions, presets, and docs. Existing `glmt` profiles now run through a compatibility path that normalizes legacy proxy settings to the direct GLM endpoint.
- **#744**: API profile creation now keeps featured providers in a horizontal rail with scroll fallback, moves Anthropic Direct API to the end, reuses the shared Claude logo, and separates the custom-endpoint entry point from advanced template discovery.
- **#724**: Codex startup is now free-plan safe. CCS defaults new Codex sessions to a cross-plan model and auto-repairs stale paid-only Codex defaults when the active account is on the free plan.
- **#737**: Dashboard model pickers in Cursor, Copilot, and CLIProxy now use a searchable combobox with autofocus and explicit no-results states for large model catalogs.
@@ -59,7 +60,7 @@ All major modularization work is complete. The codebase evolved from monolithic
**CLI** (complex core logic):
- `model-pricing.ts` (676 lines) - Data file
- `glmt-proxy.ts` (675 lines) - Streaming proxy
- `glmt-proxy.ts` (675 lines) - Legacy internal compatibility proxy
- `cliproxy-executor.ts` (666 lines) - Core execution
- `ccs.ts` (596 lines) - Entry point
+11 -8
View File
@@ -55,7 +55,7 @@ CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration w
**Key architecture:**
```
Profile Resolution (CLIProxy, GLMT, Account-based)
Profile Resolution (CLIProxy, Settings/API, Account-based)
|
v
Target Resolution (--target flag > config > argv[0] > default)
@@ -126,7 +126,7 @@ For details on the adapter architecture, see [Target Adapters](./target-adapters
| |
+---> [CLIProxy Provider] ---> execClaudeWithCLIProxy()
| |
+---> [GLMT Profile] ---> execClaudeWithProxy()
+---> [Settings/API Profile] ---> normalize legacy glmt if needed
|
v
+------------------+
@@ -185,20 +185,23 @@ For details on the adapter architecture, see [Target Adapters](./target-adapters
| v
| 7b. Spawn via Adapter
|
+---> GLMT -----------> 3c. Start Embedded Proxy
+---> Settings/API ---> 3c. Load settings env
|
v
4c. Resolve Target Adapter
4c. Normalize legacy glmt if needed
|
v
5c. Spawn via Adapter
5c. Resolve Target Adapter
|
v
6c. Spawn via Adapter
```
---
## Provider Integration Architecture
For detailed provider flows (CLIProxyAPI, GLMT, quota management), see [Provider Flows](./provider-flows.md).
For detailed provider flows (CLIProxyAPI, legacy GLMT compatibility, quota management), see [Provider Flows](./provider-flows.md).
---
@@ -303,7 +306,7 @@ See [Provider Flows](./provider-flows.md) → Authentication Flow section.
| Localhost only (127.0.0.1)
v
+------------------+
| CLIProxy/GLMT | Binds to localhost only
| CLIProxy/Legacy | Binds to localhost only
+------------------+
|
| TLS encrypted
@@ -400,5 +403,5 @@ See [Provider Flows](./provider-flows.md) → Authentication Flow section.
- [Codebase Summary](../codebase-summary.md) - Detailed directory structure
- [Code Standards](../code-standards.md) - Coding conventions & patterns
- [Target Adapters](./target-adapters.md) - Multi-CLI adapter architecture
- [Provider Flows](./provider-flows.md) - CLIProxy, GLMT, authentication flows
- [Provider Flows](./provider-flows.md) - CLIProxy, legacy GLMT compatibility, authentication flows
- [Project Roadmap](../project-roadmap.md) - Development phases
+19 -27
View File
@@ -2,7 +2,7 @@
Last Updated: 2026-02-16
Detailed provider integration flows including CLIProxyAPI, GLMT proxy, remote CLIProxy, quota management, and authentication.
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication.
---
@@ -91,63 +91,55 @@ if (hardcodedProviders.includes(profileName)) {
---
## GLMT Proxy Flow
## Legacy GLMT Compatibility Flow
### Overview
GLMT proxy enables seamless integration with GLM-compatible APIs (Z.AI, Kimi, OpenRouter, etc.) using a Node.js-based embedded proxy.
GLMT is no longer a marketed runtime surface in CCS. Existing `glmt` profiles are kept as a compatibility path and normalized at launch to the direct GLM endpoint. The `src/glmt/` module remains because Cursor response translation still imports its transformer pipeline.
```
+===========================================================================+
| GLMT Proxy Integration |
| Legacy GLMT Compatibility + Internal Transforms |
+===========================================================================+
Claude CLI
|
| ANTHROPIC_BASE_URL = localhost:XXXX
| legacy glmt settings detected
v
+------------------+
| GLMT Proxy | Embedded Node.js proxy (src/glmt/)
| (glmt-proxy.ts)|
| Compatibility | normalizeDeprecatedGlmtEnv()
| Layer | (src/utils/glmt-deprecation.ts)
+------------------+
|
v
+------------------+
| Delta Accumulator| Stream transformation
| Direct GLM API | https://api.z.ai/api/anthropic
+------------------+
|
v
+------------------+
| Pipeline | Request/Response transformation
+------------------+
|
v
+------------------+
| GLM API | Z.AI / Kimi API
| src/glmt/* | retained for Cursor translation
+------------------+
```
### Supported GLM Providers
### Supported Migration Targets
| Provider | Config Key | Endpoint | Auth |
|----------|------------|----------|------|
| Z.AI (GLM) | `glmt` | https://open.bigmodel.cn/api/paas/v4/ | API key |
| Kimi | `kimi` | https://api.moonshot.cn/v1/ | API key |
| OpenRouter | `openrouter` | https://openrouter.ai/api/v1/ | API key |
| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key |
| Kimi API | `km` | https://api.kimi.com/coding/ | API key |
| Legacy compatibility | `glmt` | normalized to direct GLM at runtime | existing profile only |
Note for `config/base-kimi.settings.json`: the default base URL is `http://127.0.0.1:8317/api/provider/kimi` (local CLIProxy route). For direct Moonshot API access, override `ANTHROPIC_BASE_URL` to `https://api.moonshot.cn/v1/`.
Use `ccs glm` for Z.AI profiles and `ccs km` for reasoning-first Kimi API profiles. Keep `glmt` only when migrating an existing settings file.
### GLMT Profile Detection
### Runtime Handling
CCS detects GLMT profiles and routes through `execClaudeWithProxy()`:
CCS detects the deprecated `glmt` profile name and normalizes legacy proxy-only settings before dispatching through the normal settings-profile flow:
```typescript
// Settings-based profile detection
const settings = loadSettings(profileName);
if (settings.env?.ANTHROPIC_BASE_URL?.includes('glm') ||
settings.env?.ANTHROPIC_BASE_URL?.includes('moonshot') ||
settings.env?.ANTHROPIC_BASE_URL?.includes('openrouter')) {
return execClaudeWithProxy(claudeCli, profileName, args);
if (isDeprecatedGlmtProfileName(profileName)) {
const normalized = normalizeDeprecatedGlmtEnv(settingsEnv);
// warn user, validate against direct GLM endpoint, continue through settings flow
}
```
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@kaitranntt/ccs",
"version": "7.54.0-dev.8",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more",
"keywords": [
"cli",
"claude",
+1 -1
View File
@@ -116,7 +116,7 @@ sudo cp scripts/completion/ccs.fish /usr/share/fish/vendor_completions.d/
```bash
$ ccs <TAB>
auth doctor glm glmt kimi work personal --help --version
auth doctor glm kimi work personal --help --version
$ ccs auth <TAB>
create list show remove default --help
+1 -2
View File
@@ -26,7 +26,7 @@ end
# Helper function to get custom/unknown settings profiles
function __fish_ccs_get_custom_settings_profiles
set -l config_path ~/.ccs/config.json
set -l known_profiles default glm glmt kimi
set -l known_profiles default glm kimi
if test -f $config_path
set -l all_profiles (jq -r '.profiles | keys[]' $config_path 2>/dev/null)
@@ -138,7 +138,6 @@ complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env
# Model profiles - grouped with [model] prefix for visual distinction
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'default' -d '[model] Default Claude Sonnet 4.5'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
# Custom model profiles - dynamic with [model] prefix
+1 -2
View File
@@ -15,7 +15,7 @@
# Set up completion styles for better formatting and colors
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|api|cliproxy|doctor|env|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
zstyle ':completion:*:*:ccs:*:proxy-profiles' list-colors '=(#b)(gemini|codex|agy|qwen)([[:space:]]#--[[:space:]]#*)==0\;35=2\;37'
zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|glmt|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37'
zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37'
zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37'
zstyle ':completion:*:*:ccs:*' group-name ''
zstyle ':completion:*:*:ccs:*:descriptions' format $'\n%B%F{yellow}── %d ──%f%b'
@@ -56,7 +56,6 @@ _ccs() {
profile_descriptions=(
'default' 'Default Claude Sonnet 4.5'
'glm' 'GLM-4.6 (cost-optimized)'
'glmt' 'GLM-4.6 with thinking mode'
'kimi' 'Kimi for Coding (long-context)'
)
+3 -3
View File
@@ -150,7 +150,7 @@ function createConfigFiles() {
// Create config.yaml if missing (primary format)
// NOTE: gemini/codex profiles NOT included - they are added on-demand when user
// runs `ccs gemini` or `ccs codex` for first time (requires OAuth auth first)
// NOTE: GLM/GLMT/Kimi profiles are now created via UI/CLI presets, not auto-created
// NOTE: GLM/Kimi profiles are now created via UI/CLI presets, not auto-created
const configYamlPath = path.join(ccsDir, 'config.yaml');
const legacyConfigPath = path.join(ccsDir, 'config.json');
@@ -250,10 +250,10 @@ function createConfigFiles() {
console.log(' config.json will be ignored - consider removing it');
}
// NOTE: GLM, GLMT, and Kimi profiles are NO LONGER auto-created during install
// NOTE: GLM and Kimi profiles are NO LONGER auto-created during install
// Users can create these via:
// - UI: Profile Create Dialog → Provider Presets
// - CLI: ccs api create --preset glm|glmt|kimi
// - CLI: ccs api create --preset glm|km
// This gives users control over which providers they want to use
// Existing profiles are preserved for backward compatibility
+75 -309
View File
@@ -1,13 +1,10 @@
import './utils/fetch-proxy-setup';
import { spawn, ChildProcess } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { detectClaudeCli } from './utils/claude-detector';
import {
getSettingsPath,
loadSettings,
getCcsDir,
setGlobalConfigDir,
detectCloudSyncPath,
} from './utils/config-manager';
@@ -43,13 +40,8 @@ import { handleError, runCleanup } from './errors';
import { tryHandleRootCommand } from './commands/root-command-router';
// Import extracted utility functions
import {
execClaude,
escapeShellArg,
stripClaudeCodeEnv,
getClaudeLaunchEnvOverrides,
} from './utils/shell-executor';
import { wireChildProcessSignals } from './utils/signal-forwarder';
import { execClaude } from './utils/shell-executor';
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
// Import target adapter system
import {
@@ -98,198 +90,6 @@ function detectProfile(args: string[]): DetectedProfile {
}
}
// ========== GLMT Proxy Execution ==========
/**
* Execute Claude CLI with embedded proxy (for GLMT profile)
*/
async function execClaudeWithProxy(
claudeCli: string,
profileName: string,
args: string[],
claudeConfigDir?: string
): Promise<void> {
// 1. Read settings to get API key
const settingsPath = getSettingsPath(profileName);
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const envData = settings.env;
const apiKey = envData['ANTHROPIC_AUTH_TOKEN'];
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
console.error(fail('GLMT profile requires Z.AI API key'));
console.error(` Edit ${getCcsDir()}/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', 'glmt-proxy.js');
const proxyArgs = verbose ? ['--verbose'] : [];
// Use process.execPath for Windows compatibility (CVE-2024-27980)
// Pass environment variables to proxy subprocess (required for auth)
const proxy = spawn(process.execPath, [proxyPath, ...proxyArgs], {
stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit'],
env: {
...process.env,
ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_BASE_URL: envData['ANTHROPIC_BASE_URL'],
},
});
const stopProxy = (): void => {
try {
if (!proxy.killed) {
proxy.kill('SIGTERM');
}
} catch {
// Best-effort cleanup on process teardown.
}
};
// 3. Wait for proxy ready signal (with timeout)
const { ProgressIndicator } = await import('./utils/progress-indicator');
const spinner = new ProgressIndicator('Starting GLMT proxy');
spinner.start();
let port: number;
try {
port = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Proxy startup timeout (5s)'));
}, 5000);
proxy.stdout?.on('data', (data: Buffer) => {
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}`));
}
});
});
spinner.succeed(`GLMT proxy ready on port ${port}`);
} catch (error) {
const err = error as Error;
spinner.fail('Failed to start GLMT proxy');
console.error(fail(`Error: ${err.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 ${getCcsDir()}/logs/ (if debug enabled)`);
console.error('');
stopProxy();
runCleanup();
process.exit(1);
}
// 4. Spawn Claude CLI with proxy URL
// Use model from user's settings (not hardcoded) - fixes issue #358
const configuredModel = envData['ANTHROPIC_MODEL'] || 'glm-5';
const envVars: NodeJS.ProcessEnv = {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_MODEL: configuredModel,
...(claudeConfigDir ? { CLAUDE_CONFIG_DIR: claudeConfigDir } : {}),
};
const isWindows = process.platform === 'win32';
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli);
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisEnv = getImageAnalysisHookEnv(profileName);
const claudeLaunchEnv = getClaudeLaunchEnvOverrides();
const env = stripClaudeCodeEnv({
...process.env,
...claudeLaunchEnv,
...envVars,
...webSearchEnv,
...imageAnalysisEnv,
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
});
let claude: ChildProcess;
if (isPowerShellScript) {
claude = spawn(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', claudeCli, ...args],
{
stdio: 'inherit',
windowsHide: true,
env,
}
);
} else if (needsShell) {
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
claude = spawn(cmdString, {
stdio: 'inherit',
windowsHide: true,
shell: true,
env,
});
} else {
claude = spawn(claudeCli, args, {
stdio: 'inherit',
windowsHide: true,
env,
});
}
// 5. Shared signal forwarding + proxy cleanup lifecycle
wireChildProcessSignals(
claude,
(err: NodeJS.ErrnoException) => {
if (err.code === 'EACCES') {
console.error(fail(`Claude CLI is not executable: ${claudeCli}`));
console.error(' Check file permissions and executable bit.');
} else if (err.code === 'ENOENT') {
if (isPowerShellScript) {
console.error(
fail('PowerShell executable not found (required for .ps1 wrapper launch).')
);
console.error(' Ensure powershell.exe is available in PATH.');
} else if (needsShell) {
console.error(fail('Windows command shell not found for Claude wrapper launch.'));
console.error(' Ensure cmd.exe is available and accessible.');
} else {
console.error(fail(`Claude CLI not found: ${claudeCli}`));
}
} else {
console.error(fail(`Claude CLI error: ${err.message}`));
}
stopProxy();
runCleanup();
process.exit(1);
},
(code: number | null, signal: NodeJS.Signals | null) => {
stopProxy();
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
}
);
}
// ========== Main Execution ==========
interface ProfileError extends Error {
@@ -549,15 +349,6 @@ async function main(): Promise<void> {
process.exit(1);
}
// GLMT always requires Claude target because it depends on embedded proxy flow.
if (profileInfo.type === 'settings' && profileInfo.name === 'glmt') {
console.error(fail(`${targetAdapter.displayName} does not support GLMT proxy profiles`));
console.error(
info('Use --target claude for glmt, or switch to a direct API profile (glm/km)')
);
process.exit(1);
}
if (profileInfo.type === 'default') {
if (!targetAdapter.supportsProfileType('default')) {
console.error(fail(`${targetAdapter.displayName} does not support default profile mode`));
@@ -878,18 +669,29 @@ async function main(): Promise<void> {
);
}
const inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir;
const expandedSettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name);
const settings = loadSettings(expandedSettingsPath);
const rawSettingsEnv = profileInfo.env ?? settings.env ?? {};
const isDeprecatedGlmtProfile = isDeprecatedGlmtProfileName(profileInfo.name);
const glmtNormalization = isDeprecatedGlmtProfile
? normalizeDeprecatedGlmtEnv(rawSettingsEnv)
: null;
const settingsEnv = glmtNormalization?.env ?? rawSettingsEnv;
// Pre-flight validation for GLM/GLMT/MiniMax profiles
if (profileInfo.name === 'glm' || profileInfo.name === 'glmt') {
const preflightSettingsPath = getSettingsPath(profileInfo.name);
const preflightSettings = loadSettings(preflightSettingsPath);
const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN'];
if (glmtNormalization) {
for (const message of glmtNormalization.warnings) {
console.error(warn(message));
}
}
// Pre-flight validation for Z.AI-compatible profiles.
if (profileInfo.name === 'glm' || isDeprecatedGlmtProfile) {
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateGlmKey(
apiKey,
preflightSettings.env?.['ANTHROPIC_BASE_URL']
);
const validation = await validateGlmKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
if (!validation.valid) {
console.error('');
@@ -906,15 +708,10 @@ async function main(): Promise<void> {
}
if (profileInfo.name === 'mm') {
const preflightSettingsPath = getSettingsPath(profileInfo.name);
const preflightSettings = loadSettings(preflightSettingsPath);
const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN'];
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateMiniMaxKey(
apiKey,
preflightSettings.env?.['ANTHROPIC_BASE_URL']
);
const validation = await validateMiniMaxKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
if (!validation.valid) {
console.error('');
@@ -932,10 +729,8 @@ async function main(): Promise<void> {
// Pre-flight validation for Anthropic direct profiles (ANTHROPIC_API_KEY + no BASE_URL)
{
const preflightSettingsPath = getSettingsPath(profileInfo.name);
const preflightSettings = loadSettings(preflightSettingsPath);
const anthropicApiKey = preflightSettings.env?.['ANTHROPIC_API_KEY'];
const hasBaseUrl = !!preflightSettings.env?.['ANTHROPIC_BASE_URL'];
const anthropicApiKey = settingsEnv['ANTHROPIC_API_KEY'];
const hasBaseUrl = !!settingsEnv['ANTHROPIC_BASE_URL'];
if (anthropicApiKey && !hasBaseUrl) {
const validation = await validateAnthropicKey(anthropicApiKey);
if (!validation.valid) {
@@ -954,89 +749,60 @@ async function main(): Promise<void> {
}
}
// Check if this is GLMT profile (requires proxy)
if (profileInfo.name === 'glmt') {
if (resolvedTarget !== 'claude') {
console.error(
fail(`${targetAdapter?.displayName || 'Target'} does not support GLMT proxy profiles`)
);
console.error(
info('Use --target claude for glmt, or switch to a direct API profile (glm/km)')
);
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisEnv = getImageAnalysisHookEnv(profileInfo.name);
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
// Log global env injection for visibility (debug mode only)
if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) {
const envNames = Object.keys(globalEnv).join(', ');
console.error(info(`Global env: ${envNames}`));
}
// Explicitly inject effective settings env vars so stale ANTHROPIC_*
// values from prior sessions cannot leak into the active profile.
const envVars: NodeJS.ProcessEnv = {
...globalEnv,
...settingsEnv,
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
...webSearchEnv,
...imageAnalysisEnv,
CCS_PROFILE_TYPE: 'settings',
};
// Dispatch through target adapter for non-claude targets
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exit(1);
}
// GLMT FLOW: Settings-based with embedded proxy for thinking support
await execClaudeWithProxy(
claudeCli,
profileInfo.name,
remainingArgs,
inheritedClaudeConfigDir
);
} else {
// EXISTING FLOW: Settings-based profile (glm)
// Use --settings flag (backward compatible)
const expandedSettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name);
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisEnv = getImageAnalysisHookEnv(profileInfo.name);
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
// Log global env injection for visibility (debug mode only)
if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) {
const envNames = Object.keys(globalEnv).join(', ');
console.error(info(`Global env: ${envNames}`));
}
// CRITICAL: Load settings and explicitly set ANTHROPIC_* env vars
// to prevent inheriting stale values from previous CLIProxy sessions.
// Environment variables take precedence over --settings file values,
// so we must explicitly set them here to ensure correct routing.
const settings = loadSettings(expandedSettingsPath);
const settingsEnv = settings.env || {};
const envVars: NodeJS.ProcessEnv = {
...globalEnv,
...settingsEnv, // Explicitly inject all settings env vars
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
...webSearchEnv,
...imageAnalysisEnv,
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
};
// Dispatch through target adapter for non-claude targets
if (resolvedTarget !== 'claude') {
const adapter = targetAdapter;
if (!adapter) {
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
process.exit(1);
}
const directAnthropicBaseUrl =
settingsEnv['ANTHROPIC_BASE_URL'] ||
(settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : '');
const creds: TargetCredentials = {
profile: profileInfo.name,
const directAnthropicBaseUrl =
settingsEnv['ANTHROPIC_BASE_URL'] ||
(settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : '');
const creds: TargetCredentials = {
profile: profileInfo.name,
baseUrl: directAnthropicBaseUrl,
apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '',
model: settingsEnv['ANTHROPIC_MODEL'],
provider: resolveDroidProvider({
provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'],
baseUrl: directAnthropicBaseUrl,
apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '',
model: settingsEnv['ANTHROPIC_MODEL'],
provider: resolveDroidProvider({
provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'],
baseUrl: directAnthropicBaseUrl,
model: settingsEnv['ANTHROPIC_MODEL'],
}),
reasoningOverride: droidReasoningOverride,
};
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs);
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars);
}),
reasoningOverride: droidReasoningOverride,
envVars,
};
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs);
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
}
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars);
} else if (profileInfo.type === 'account') {
// NEW FLOW: Account-based profile (work, personal)
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
@@ -36,6 +36,17 @@ function resolvePresetOrExit(presetId?: string): ProviderPreset | null {
process.exit(1);
}
function showPresetDeprecationNotice(presetId?: string): void {
if ((presetId || '').trim().toLowerCase() !== 'glmt') {
return;
}
console.log(warn('Preset "glmt" is deprecated and now maps to the direct "glm" preset.'));
console.log(dim(' Z.AI models already expose thinking natively, so CCS no longer needs GLMT.'));
console.log(dim(' Update scripts/docs to: ccs api create --preset glm'));
console.log('');
}
async function resolveProfileName(
providedName: string | undefined,
preset: ProviderPreset | null
@@ -237,6 +248,7 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
console.log(header('Create API Profile'));
console.log('');
showPresetDeprecationNotice(parsedArgs.preset);
const preset = resolvePresetOrExit(parsedArgs.preset);
const name = await resolveProfileName(parsedArgs.name, preset);
+5
View File
@@ -209,6 +209,11 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
let envVars: Record<string, string>;
try {
const resolved = await resolveClaudeExtensionSetup(profile);
if (resolved.warnings.length > 0) {
for (const message of resolved.warnings) {
console.error(warn(message));
}
}
envVars = resolved.extensionEnv;
if (format === 'claude-extension') {
console.log(renderClaudeExtensionSettingsJson(resolved, ide));
-1
View File
@@ -131,7 +131,6 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
[
['ccs', 'Use default Claude account'],
['ccs glm', 'GLM 5 (API key required)'],
['ccs glmt', 'GLM with thinking mode'],
['ccs km', 'Kimi for Coding (API key)'],
[
'ccs api create --preset alibaba-coding-plan',
+1 -1
View File
@@ -679,7 +679,7 @@ async function showHelp(): Promise<void> {
);
console.log('');
console.log(subheader('Supported Profile Types'));
console.log(` ${color('API profiles', 'command')} glm, glmt, km, custom API profiles`);
console.log(` ${color('API profiles', 'command')} glm, km, custom API profiles`);
console.log(` ${color('CLIProxy', 'command')} gemini, codex, agy, qwen, kiro, ghcp`);
console.log(` ${color('Copilot', 'command')} copilot (requires copilot-api daemon)`);
console.log(
+1 -1
View File
@@ -553,7 +553,7 @@ function generateYamlWithComments(config: UnifiedConfig): string {
// Profiles section
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Profiles: API-based providers (GLM, GLMT, Kimi, custom endpoints)');
lines.push('# Profiles: API-based providers (GLM, Kimi, custom endpoints)');
lines.push('# Each profile points to a *.settings.json file containing env vars.');
lines.push('# Edit the settings file directly to customize (ANTHROPIC_MAX_TOKENS, etc.)');
lines.push('# ----------------------------------------------------------------------------');
+4 -3
View File
@@ -1,5 +1,5 @@
/**
* GlmtProxy - Embedded HTTP proxy for GLM thinking support
* GlmtProxy - Legacy embedded HTTP proxy retained for compatibility work.
*
* Architecture:
* - Intercepts Claude CLI Z.AI calls
@@ -7,8 +7,9 @@
* - Converts reasoning_content thinking blocks
* - Supports both streaming and buffered modes
*
* Lifecycle:
* - Spawned by bin/ccs.js when 'glmt' profile detected
* Current status:
* - No longer started by the normal `ccs glmt` runtime path
* - Kept for legacy/internal compatibility and transformer-adjacent tests
* - Binds to 127.0.0.1:random_port (security + avoid conflicts)
* - Terminates when parent process exits
*
+2 -2
View File
@@ -200,8 +200,8 @@ class RecoveryManager {
* Run all recovery operations (lazy initialization)
* Mirrors postinstall.js behavior
*
* NOTE: GLM/GLMT/Kimi profiles are NOT auto-created.
* Users should create them via `ccs api create --preset glm` or the UI.
* NOTE: GLM/Kimi profiles are NOT auto-created.
* Users should create them via `ccs api create --preset glm|km` or the UI.
*/
recoverAll(): boolean {
this.recovered = [];
+15 -2
View File
@@ -15,6 +15,7 @@ import InstanceManager from '../management/instance-manager';
import SharedManager from '../management/shared-manager';
import { expandPath } from '../utils/helpers';
import { getClaudeSettingsPath } from '../utils/claude-config-path';
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from '../utils/glmt-deprecation';
import {
type ClaudeExtensionHost,
type ClaudeExtensionHostDefinition,
@@ -87,7 +88,12 @@ function describeProfile(profileName: string, result: ProfileDetectionResult): s
}
if (result.type === 'cliproxy')
return 'OAuth or CLIProxy-backed profile for Anthropic-compatible routing.';
if (result.type === 'settings') return 'API profile backed by a CCS settings file.';
if (result.type === 'settings') {
if (isDeprecatedGlmtProfileName(profileName)) {
return 'Deprecated GLMT compatibility profile normalized to the direct GLM API.';
}
return 'API profile backed by a CCS settings file.';
}
if (result.type === 'account')
return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.';
if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.';
@@ -184,7 +190,7 @@ async function resolveExtensionEnv(
profileType: result.type,
target: 'claude',
});
const env =
let env =
result.type === 'settings'
? (result.env ??
(result.settingsPath ? loadSettingsFromFile(expandPath(result.settingsPath)) : {}))
@@ -230,6 +236,13 @@ async function resolveExtensionEnv(
: getEffectiveEnvVars(result.provider, port, result.settingsPath);
})();
if (result.type === 'settings' && isDeprecatedGlmtProfileName(requestedProfile)) {
const normalized = normalizeDeprecatedGlmtEnv(sortEnvRecord(env));
env = normalized.env;
warnings.push(...normalized.warnings);
notes.push('Create or migrate to a glm profile when convenient to remove legacy GLMT config.');
}
if (!requestedIsDefault && continuity.claudeConfigDir && !env.CLAUDE_CONFIG_DIR) {
env.CLAUDE_CONFIG_DIR = continuity.claudeConfigDir;
notes.push(
+2 -25
View File
@@ -14,7 +14,6 @@ export const PROVIDER_PRESET_IDS = [
'llamacpp',
'anthropic',
'glm',
'glmt',
'km',
'foundry',
'mm',
@@ -54,6 +53,7 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
* Keep this minimal and explicit to avoid hidden implicit behavior.
*/
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
glmt: 'glm',
kimi: 'km',
alibaba: 'alibaba-coding-plan',
acp: 'alibaba-coding-plan',
@@ -137,7 +137,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
{
id: 'glm',
name: 'GLM',
description: 'Claude via Z.AI',
description: 'Direct Z.AI Anthropic-compatible API profile',
baseUrl: 'https://api.z.ai/api/anthropic',
defaultProfileName: 'glm',
defaultModel: 'glm-5',
@@ -148,29 +148,6 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
badge: 'Z.AI',
icon: '/icons/zai.svg',
},
{
id: 'glmt',
name: 'GLMT',
description: 'GLM with Thinking mode support',
baseUrl: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
defaultProfileName: 'glmt',
defaultModel: 'glm-5',
apiKeyPlaceholder: 'ghp_...',
apiKeyHint: 'Same API key as GLM',
category: 'alternative',
requiresApiKey: true,
extraEnv: {
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,
badge: 'Thinking',
icon: '/icons/zai.svg',
},
{
id: 'km',
name: 'Kimi',
+1 -1
View File
@@ -32,7 +32,7 @@ export type {
DelegationEvent,
} from './delegation';
// GLMT types
// Legacy GLMT transformer types
export type {
AnthropicMessage,
ContentBlock,
+1 -1
View File
@@ -337,7 +337,7 @@ export function getSettingsPath(profile: string): string {
/**
* Get display name for a profile by reading ANTHROPIC_MODEL from settings
* @param profile - Profile name (glm, glmt, kimi, custom, etc.)
* @param profile - Profile name (glm, km, glmt compatibility, custom, etc.)
* @returns Formatted display name (e.g., 'GLM-4.7', 'Kimi', 'Custom-Model')
*/
export function getModelDisplayName(profile: string): string {
+81
View File
@@ -0,0 +1,81 @@
const LEGACY_GLMT_PROFILE = 'glmt';
const LEGACY_GLMT_BASE_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
const DIRECT_GLM_BASE_URL = 'https://api.z.ai/api/anthropic';
const GLMT_PROXY_ONLY_ENV_KEYS = [
'API_TIMEOUT_MS',
'ANTHROPIC_SAFE_MODE',
'ENABLE_STREAMING',
'MAX_THINKING_TOKENS',
] as const;
export interface GlmtNormalizationResult {
env: Record<string, string>;
warnings: string[];
migrated: boolean;
}
export function isDeprecatedGlmtProfileName(profileName: string | null | undefined): boolean {
return (profileName || '').trim().toLowerCase() === LEGACY_GLMT_PROFILE;
}
export function isLegacyGlmtBaseUrl(baseUrl: string | null | undefined): boolean {
const normalized = (baseUrl || '').trim().toLowerCase().replace(/\/+$/, '');
if (!normalized) {
return false;
}
return (
normalized === LEGACY_GLMT_BASE_URL ||
normalized.includes('/api/coding/paas/v4') ||
normalized.endsWith('/chat/completions')
);
}
export function normalizeDeprecatedGlmtEnv(env: Record<string, string>): GlmtNormalizationResult {
const normalizedEnv = { ...env };
let migrated = false;
if (
!normalizedEnv['ANTHROPIC_BASE_URL'] ||
isLegacyGlmtBaseUrl(normalizedEnv['ANTHROPIC_BASE_URL'])
) {
normalizedEnv['ANTHROPIC_BASE_URL'] = DIRECT_GLM_BASE_URL;
migrated = true;
}
for (const key of GLMT_PROXY_ONLY_ENV_KEYS) {
if (key in normalizedEnv) {
delete normalizedEnv[key];
migrated = true;
}
}
return {
env: normalizedEnv,
warnings: buildGlmtCompatibilityWarnings(migrated),
migrated,
};
}
export function buildGlmtCompatibilityWarnings(migrated: boolean): string[] {
const warnings = [
'GLMT is deprecated and kept only as a compatibility path.',
'Use ccs glm for Z.AI API profiles.',
'Use ccs km for reasoning-first Kimi API profiles.',
];
if (migrated) {
warnings.splice(
1,
0,
'CCS normalized legacy GLMT proxy settings to the direct GLM endpoint for this run.'
);
}
return warnings;
}
export function getDirectGlmBaseUrl(): string {
return DIRECT_GLM_BASE_URL;
}
+3 -3
View File
@@ -5,7 +5,7 @@
```
tests/
├── unit/ # Module unit tests (Mocha)
│ ├── glmt/ # GLMT transformer tests
│ ├── glmt/ # Legacy GLMT transformer/internal compatibility tests
│ └── delegation/ # Delegation module tests
├── npm/ # npm package tests (Mocha)
├── native/ # Native installation tests (bash/PowerShell)
@@ -32,7 +32,7 @@ bun run test:native # Native Unix tests (bash)
### Unit Tests (`unit/`)
Module-level tests using Mocha framework:
- `unit/glmt/` - GLMT transformer, SSE parser, delta accumulator
- `unit/glmt/` - Legacy transformer internals kept for Cursor translation compatibility
- `unit/delegation/` - Permission mode, session manager, result formatter
### npm Tests (`npm/`)
@@ -53,7 +53,7 @@ Integration and smoke coverage for scenarios that exercise multiple layers:
- Shell and standalone probe scripts remain on-demand for targeted debugging
- `cursor-daemon-lifecycle.test.ts` - local daemon process + HTTP smoke coverage
- `image-analyzer-hook.test.ts` - hook integration coverage
- `glmt-integration-test.sh` - GLMT integration probe
- `glmt-integration-test.sh` - legacy GLMT compatibility smoke probe
- `symlink-chain-test.sh` - Symlink chain handling
- `ux-integration-test.sh` - CLI UX integration
+53 -139
View File
@@ -1,170 +1,84 @@
#!/usr/bin/env bash
#
# GLMT Integration Test Suite
# Tests proxy startup, configuration, and basic functionality
# Legacy GLMT compatibility smoke probe
# Verifies that user-facing GLMT marketing is gone while internal transformer
# modules still exist for compatibility and Cursor translation.
#
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
REPO_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
PASSED=0
FAILED=0
test_pass() {
echo -e "${GREEN}${NC} $1"
((PASSED++))
echo "[OK] $1"
PASSED=$((PASSED + 1))
}
test_fail() {
echo -e "${RED}${NC} $1"
((FAILED++))
echo "[X] $1"
FAILED=$((FAILED + 1))
}
test_info() {
echo -e "${YELLOW}${NC} $1"
echo "[i] $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
echo "=== Legacy GLMT Compatibility Smoke Test ==="
echo
# 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
echo "Test 1: Internal transformer sources remain available"
for file in \
"$REPO_DIR/src/glmt/glmt-transformer.ts" \
"$REPO_DIR/src/glmt/delta-accumulator.ts" \
"$REPO_DIR/src/glmt/sse-parser.ts" \
"$REPO_DIR/src/cursor/cursor-anthropic-response.ts"; do
if [ -f "$file" ]; then
test_pass "$(basename "$file") exists"
else
test_fail "Missing internal compatibility file: $file"
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
echo
echo "Test 2: Root help no longer advertises ccs glmt"
if command -v ccs >/dev/null 2>&1; then
if ccs --help | grep -q "ccs glmt"; then
test_fail "Root help still advertises ccs glmt"
else
test_pass "Root help hides ccs glmt"
fi
else
test_fail "Unit test file NOT found"
test_info "ccs binary not found in PATH; skipping live help probe"
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"
echo
echo "Test 3: Legacy glmt settings file inspection"
if [ -f "$HOME/.ccs/glmt.settings.json" ]; then
test_pass "Legacy glmt.settings.json detected"
if grep -q "api/coding/paas/v4/chat/completions" "$HOME/.ccs/glmt.settings.json"; then
test_info "Legacy proxy endpoint still present in settings; CCS should normalize it at runtime"
else
test_info "Settings already point at a direct endpoint or custom override"
fi
else
test_fail "GLMT NOT in help text"
test_info "No legacy glmt.settings.json found; nothing to migrate locally"
fi
# Summary
echo ""
echo "=== Test Summary ==="
echo -e "Passed: ${GREEN}$PASSED${NC}"
echo -e "Failed: ${RED}$FAILED${NC}"
echo ""
echo
echo "=== Summary ==="
echo "Passed: $PASSED"
echo "Failed: $FAILED"
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
if [ "$FAILED" -eq 0 ]; then
echo "[OK] Legacy compatibility surface looks consistent."
echo "[i] Preferred profiles: ccs glm for Z.AI, ccs km for reasoning-first Kimi."
exit 0
fi
echo "[X] Review the failures above."
exit 1
+3 -2
View File
@@ -77,8 +77,9 @@ describe('npm CLI', () => {
});
describe('Profile handling', () => {
// Note: GLM/GLMT/Kimi profiles are no longer auto-created (v6.0)
// Users create these via UI presets or CLI: ccs api create --preset glm
// Note: GLM/Kimi profiles are no longer auto-created (v6.0).
// Legacy GLMT files may still exist, but new supported API profiles are created
// via UI presets or CLI: ccs api create --preset glm
it('shows helpful error for non-existent profile', function() {
try {
+4 -3
View File
@@ -46,8 +46,9 @@ describe('npm postinstall', () => {
env: { ...process.env, CCS_HOME: testEnv.testHome }
});
// GLM/GLMT/Kimi profiles are NO LONGER auto-created during install
// Users create these via UI presets or CLI: ccs api create --preset glm
// GLM/Kimi profiles are NO LONGER auto-created during install.
// Legacy glmt.settings.json files may still exist from older setups.
// Users create supported API profiles via UI presets or CLI: ccs api create --preset glm
assert(!testEnv.fileExists('glm.settings.json'), 'glm.settings.json should NOT be auto-created');
assert(!testEnv.fileExists('glmt.settings.json'), 'glmt.settings.json should NOT be auto-created');
assert(!testEnv.fileExists('kimi.settings.json'), 'kimi.settings.json should NOT be auto-created');
@@ -110,7 +111,7 @@ describe('npm postinstall', () => {
// Verify existing file still exists and new files are created
assert(testEnv.fileExists('existing.txt'), 'Existing files should be preserved');
assert(testEnv.fileExists('config.yaml'), 'config.yaml should be created');
// GLM/GLMT/Kimi are no longer auto-created
// GLM/Kimi are no longer auto-created. Legacy GLMT files remain untouched if present.
assert(!testEnv.fileExists('glm.settings.json'), 'glm.settings.json should NOT be auto-created');
});
+11
View File
@@ -42,6 +42,12 @@ describe('provider-presets', () => {
expect(preset?.id).toBe('km');
});
it('resolves legacy glmt preset alias to glm', () => {
const preset = getPresetById('glmt');
expect(preset?.id).toBe('glm');
expect(preset?.baseUrl).toBe('https://api.z.ai/api/anthropic');
});
it('resolves preset id with extra whitespace', () => {
const preset = getPresetById(' km ');
expect(preset?.id).toBe('km');
@@ -56,6 +62,11 @@ describe('provider-presets', () => {
expect(isValidPresetId('kimi')).toBe(true);
});
it('keeps glmt out of the canonical preset catalog while preserving alias compatibility', () => {
expect(PROVIDER_PRESETS.some((preset) => preset.id === 'glmt')).toBe(false);
expect(isValidPresetId('glmt')).toBe(true);
});
it('uses non-reserved default profile name for qwen API preset', () => {
const preset = getPresetById('qwen');
expect(preset?.defaultProfileName).toBe('qwen-api');
@@ -41,6 +41,19 @@ describe('help command parity', () => {
expect(rendered.includes('http://127.0.0.1:8080')).toBe(true);
});
test('root help no longer markets glmt as a supported profile', async () => {
const lines: string[] = [];
console.log = (...args: unknown[]) => {
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs glmt')).toBe(false);
expect(rendered.includes('ccs glm')).toBe(true);
});
test('root help documents Claude IDE extension setup surfaces', async () => {
const lines: string[] = [];
console.log = (...args: unknown[]) => {
+9 -206
View File
@@ -1,214 +1,17 @@
#!/usr/bin/env node
'use strict';
/**
* Test Script: Multi-message thinking block behavior
*
* Simulates 3 consecutive messages to test if thinking blocks
* appear in all messages or only the first one.
*
* Usage: CCS_DEBUG=1 node test-thinking-multi-message.js
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const ccsPath = path.join(__dirname, 'bin', 'ccs.js');
const logDir = path.join(require('os').homedir(), '.ccs', 'logs');
// Ensure logs directory exists
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
console.log('='.repeat(60));
console.log('GLMT Multi-Message Thinking Block Test');
console.log('Legacy GLMT Multi-Message Probe');
console.log('='.repeat(60));
console.log('');
console.log('Test scenario: 3 consecutive messages with thinking enabled');
console.log('Expected: Thinking blocks appear in ALL 3 messages');
console.log('Actual: User reports thinking only in first message');
console.log('This manual script targeted the retired `ccs glmt` runtime path.');
console.log('CCS now routes supported Z.AI usage through `ccs glm`, and thinking is');
console.log('handled natively by current upstream models instead of the old GLMT proxy.');
console.log('');
console.log('Log directory:', logDir);
console.log('Use one of these instead:');
console.log(' 1. tests/integration/glmt-integration-test.sh');
console.log(' 2. ccs glm "<prompt>"');
console.log(' 3. internal transformer tests under tests/unit/glmt/');
console.log('');
// Test messages
const messages = [
'Message 1: Calculate 15! (factorial)',
'Message 2: What is the square root of 2 to 10 decimal places?',
'Message 3: Explain the Pythagorean theorem'
];
// Track results
const results = {
message1: { thinking: false, error: null },
message2: { thinking: false, error: null },
message3: { thinking: false, error: null }
};
async function runMessage(messageIndex) {
const message = messages[messageIndex];
const messageKey = `message${messageIndex + 1}`;
console.log('-'.repeat(60));
console.log(`Testing Message ${messageIndex + 1}/${messages.length}`);
console.log(`Prompt: "${message}"`);
console.log('-'.repeat(60));
return new Promise((resolve, reject) => {
const startTime = Date.now();
// Clear old logs for this test
const beforeFiles = fs.readdirSync(logDir).filter(f => f.endsWith('.json'));
// Use process.execPath for Windows compatibility (CVE-2024-27980)
const child = spawn(process.execPath, [ccsPath, 'glmt', '--verbose', message], {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
CCS_DEBUG: '1'
}
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
const text = data.toString();
stdout += text;
// Check for thinking indicator
if (text.includes('∴ Thinking') || text.includes('Thinking…')) {
results[messageKey].thinking = true;
console.log('[✓] Thinking block detected in stdout');
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
console.log('');
console.log(`Process exited with code ${code} after ${duration}ms`);
// Check logs
const afterFiles = fs.readdirSync(logDir).filter(f => f.endsWith('.json'));
const newFiles = afterFiles.filter(f => !beforeFiles.includes(f));
console.log(`New log files: ${newFiles.length}`);
// Check for reasoning_content in response logs
const responseFiles = newFiles.filter(f => f.includes('response-openai'));
console.log(`Response log files: ${responseFiles.length}`);
if (responseFiles.length > 0) {
const latestResponse = responseFiles.sort().pop();
const responsePath = path.join(logDir, latestResponse);
console.log(`Latest response log: ${latestResponse}`);
try {
const responseData = JSON.parse(fs.readFileSync(responsePath, 'utf8'));
const reasoningContent = responseData.choices?.[0]?.message?.reasoning_content;
if (reasoningContent) {
const length = reasoningContent.length;
const lines = reasoningContent.split('\n').length;
console.log(`[✓] reasoning_content found: ${length} chars, ${lines} lines`);
results[messageKey].thinking = true;
} else {
console.log('[X] No reasoning_content in response');
results[messageKey].thinking = false;
}
} catch (e) {
console.log(`[!] Error reading response log: ${e.message}`);
results[messageKey].error = e.message;
}
} else {
console.log('[X] No response logs found');
results[messageKey].error = 'No response logs';
}
console.log('');
if (code === 0) {
resolve();
} else {
results[messageKey].error = `Exit code ${code}`;
reject(new Error(`Process exited with code ${code}`));
}
});
child.on('error', (error) => {
console.error(`[X] Process error: ${error.message}`);
results[messageKey].error = error.message;
reject(error);
});
});
}
async function main() {
try {
// Run messages sequentially
for (let i = 0; i < messages.length; i++) {
await runMessage(i);
// Wait a bit between messages
if (i < messages.length - 1) {
console.log('Waiting 2s before next message...');
console.log('');
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
// Final summary
console.log('='.repeat(60));
console.log('TEST RESULTS');
console.log('='.repeat(60));
console.log('');
for (let i = 1; i <= 3; i++) {
const key = `message${i}`;
const result = results[key];
const status = result.thinking ? '[✓ PASS]' : '[X FAIL]';
console.log(`${status} Message ${i}: Thinking = ${result.thinking}`);
if (result.error) {
console.log(` Error: ${result.error}`);
}
}
console.log('');
const passCount = Object.values(results).filter(r => r.thinking).length;
const failCount = 3 - passCount;
console.log(`Summary: ${passCount}/3 messages showed thinking blocks`);
console.log('');
if (failCount > 0) {
console.log('[!] ISSUE CONFIRMED: Some messages missing thinking blocks');
console.log('');
console.log('Next steps:');
console.log(' 1. Analyze request logs to verify reasoning params');
console.log(' 2. Check if transformer is being called correctly');
console.log(' 3. Verify state management (accumulator/parser)');
console.log('');
process.exit(1);
} else {
console.log('[✓] ALL TESTS PASSED: Thinking blocks appear in all messages');
console.log('');
process.exit(0);
}
} catch (error) {
console.error('');
console.error('[X] Test failed:', error.message);
console.error('');
process.exit(1);
}
}
main();
process.exit(0);
+57
View File
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'bun:test';
import {
buildGlmtCompatibilityWarnings,
isDeprecatedGlmtProfileName,
isLegacyGlmtBaseUrl,
normalizeDeprecatedGlmtEnv,
} from '../../../src/utils/glmt-deprecation';
describe('glmt deprecation helpers', () => {
it('detects the deprecated glmt profile name case-insensitively', () => {
expect(isDeprecatedGlmtProfileName('glmt')).toBe(true);
expect(isDeprecatedGlmtProfileName('GLMT')).toBe(true);
expect(isDeprecatedGlmtProfileName('glm')).toBe(false);
});
it('detects legacy GLMT proxy base URLs', () => {
expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/coding/paas/v4/chat/completions')).toBe(
true
);
expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/coding/paas/v4/chat/completions/')).toBe(
true
);
expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/anthropic')).toBe(false);
});
it('normalizes legacy GLMT proxy settings to the direct GLM endpoint', () => {
const result = normalizeDeprecatedGlmtEnv({
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
ANTHROPIC_AUTH_TOKEN: 'ghp_test',
ANTHROPIC_MODEL: 'glm-5',
ENABLE_STREAMING: 'true',
MAX_THINKING_TOKENS: '32768',
API_TIMEOUT_MS: '3000000',
});
expect(result.migrated).toBe(true);
expect(result.env['ANTHROPIC_BASE_URL']).toBe('https://api.z.ai/api/anthropic');
expect(result.env['ENABLE_STREAMING']).toBeUndefined();
expect(result.env['MAX_THINKING_TOKENS']).toBeUndefined();
expect(result.env['API_TIMEOUT_MS']).toBeUndefined();
expect(result.warnings).toContain(
'CCS normalized legacy GLMT proxy settings to the direct GLM endpoint for this run.'
);
});
it('keeps already-direct GLM settings intact apart from deprecation messaging', () => {
const result = normalizeDeprecatedGlmtEnv({
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/anthropic',
ANTHROPIC_AUTH_TOKEN: 'ghp_test',
ANTHROPIC_MODEL: 'glm-5',
});
expect(result.migrated).toBe(false);
expect(result.env['ANTHROPIC_BASE_URL']).toBe('https://api.z.ai/api/anthropic');
expect(result.warnings).toEqual(buildGlmtCompatibilityWarnings(false));
});
});
@@ -237,7 +237,7 @@ export function ProfileCreateDialog({
};
// Check for common URL mistakes - only for truly custom URLs
// Presets (OpenRouter, GLM, GLMT, Kimi) have vetted URLs that may require full paths
// Presets (OpenRouter, GLM, Kimi) have vetted URLs that may require full paths
useEffect(() => {
// Only warn for custom URLs, not preset-selected ones
const isCustomUrl = selectedPreset === CUSTOM_PRESET_ID;
@@ -28,6 +28,10 @@ describe('resolvePresetApiKeyValue', () => {
});
describe('provider preset metadata', () => {
it('maps legacy glmt preset requests to glm', () => {
expect(getPresetById('glmt')?.id).toBe('glm');
});
it('keeps Anthropic direct last in the recommended order', () => {
const recommendedPresetIds = getPresetsByCategory('recommended').map((preset) => preset.id);
expect(recommendedPresetIds.at(-1)).toBe('anthropic');