mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
feat: deprecate GLMT user-facing surfaces
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)'
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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('# ----------------------------------------------------------------------------');
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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
@@ -32,7 +32,7 @@ export type {
|
||||
DelegationEvent,
|
||||
} from './delegation';
|
||||
|
||||
// GLMT types
|
||||
// Legacy GLMT transformer types
|
||||
export type {
|
||||
AnthropicMessage,
|
||||
ContentBlock,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user