mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
feat: deprecate GLMT user-facing surfaces
This commit is contained in:
1 parent
33efaa77b9
commit
5525098400
33 files changed
+404
-752
No files matched your search
@@ -17,7 +17,7 @@ Tests set `process.env.CCS_HOME` to a temp directory. Code using `os.homedir()`
|
|||||||
|
|
||||||
## Core Function
|
## 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)
|
## 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
|
1. **CLIProxy hardcoded**: gemini, codex, agy → OAuth-based, zero config
|
||||||
2. **CLIProxy variants**: `config.cliproxy` section → user-defined providers
|
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`
|
4. **Account-based**: `profiles.json` → isolated instances via `CLAUDE_CONFIG_DIR`
|
||||||
|
|
||||||
### Settings Format (CRITICAL)
|
### Settings Format (CRITICAL)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ src/
|
|||||||
│ ├── cli.ts # CLI types (ParsedArgs, ExitCode)
|
│ ├── cli.ts # CLI types (ParsedArgs, ExitCode)
|
||||||
│ ├── config.ts # Config types (Settings, EnvVars)
|
│ ├── config.ts # Config types (Settings, EnvVars)
|
||||||
│ ├── delegation.ts # Delegation types (sessions, events)
|
│ ├── 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)
|
│ └── utils.ts # Utility types (ErrorCode, LogLevel)
|
||||||
│
|
│
|
||||||
├── commands/ # CLI command handlers
|
├── commands/ # CLI command handlers
|
||||||
@@ -115,11 +115,11 @@ src/
|
|||||||
│ ├── index.ts # Barrel export
|
│ ├── index.ts # Barrel export
|
||||||
│ └── copilot-package-manager.ts # Package management (515 lines)
|
│ └── copilot-package-manager.ts # Package management (515 lines)
|
||||||
│
|
│
|
||||||
├── glmt/ # GLM/GLMT integration
|
├── glmt/ # Legacy transformer internals kept for compatibility
|
||||||
│ ├── index.ts # Barrel export
|
│ ├── index.ts # Barrel export
|
||||||
│ ├── pipeline/ # Processing pipeline
|
│ ├── pipeline/ # Processing pipeline
|
||||||
│ │ └── index.ts
|
│ │ └── 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)
|
│ └── delta-accumulator.ts # Delta processing (484 lines)
|
||||||
│
|
│
|
||||||
├── delegation/ # Task delegation & headless execution
|
├── delegation/ # Task delegation & headless execution
|
||||||
@@ -201,7 +201,7 @@ src/
|
|||||||
| Targets | `targets/` | Multi-CLI adapter pattern (Claude Code, Factory Droid, extensible) |
|
| Targets | `targets/` | Multi-CLI adapter pattern (Claude Code, Factory Droid, extensible) |
|
||||||
| Auth | `auth/`, `cliproxy/auth/` | Authentication across providers |
|
| Auth | `auth/`, `cliproxy/auth/` | Authentication across providers |
|
||||||
| Config | `config/`, `types/` | Configuration & type definitions |
|
| 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) |
|
| 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) |
|
| 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) |
|
| Image Analysis | `utils/image-analysis/`, `utils/hooks/` | Vision model proxying (v7.34) |
|
||||||
@@ -477,7 +477,7 @@ ui/src/
|
|||||||
| File | Lines | Status |
|
| File | Lines | Status |
|
||||||
|------|-------|--------|
|
|------|-------|--------|
|
||||||
| model-pricing.ts | 676 | Data file - acceptable |
|
| 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-executor.ts | 666 | Core logic - acceptable |
|
||||||
| cliproxy-command.ts | 634 | Could split if needed |
|
| cliproxy-command.ts | 634 | Could split if needed |
|
||||||
| usage/handlers.ts | 633 | 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
|
### 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.
|
- **#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.
|
- **#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.
|
- **#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):
|
**CLI** (complex core logic):
|
||||||
- `model-pricing.ts` (676 lines) - Data file
|
- `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
|
- `cliproxy-executor.ts` (666 lines) - Core execution
|
||||||
- `ccs.ts` (596 lines) - Entry point
|
- `ccs.ts` (596 lines) - Entry point
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration w
|
|||||||
**Key architecture:**
|
**Key architecture:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Profile Resolution (CLIProxy, GLMT, Account-based)
|
Profile Resolution (CLIProxy, Settings/API, Account-based)
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Target Resolution (--target flag > config > argv[0] > default)
|
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()
|
+---> [CLIProxy Provider] ---> execClaudeWithCLIProxy()
|
||||||
| |
|
| |
|
||||||
+---> [GLMT Profile] ---> execClaudeWithProxy()
|
+---> [Settings/API Profile] ---> normalize legacy glmt if needed
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+------------------+
|
+------------------+
|
||||||
@@ -185,20 +185,23 @@ For details on the adapter architecture, see [Target Adapters](./target-adapters
|
|||||||
| v
|
| v
|
||||||
| 7b. Spawn via Adapter
|
| 7b. Spawn via Adapter
|
||||||
|
|
|
|
||||||
+---> GLMT -----------> 3c. Start Embedded Proxy
|
+---> Settings/API ---> 3c. Load settings env
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
4c. Resolve Target Adapter
|
4c. Normalize legacy glmt if needed
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
5c. Spawn via Adapter
|
5c. Resolve Target Adapter
|
||||||
|
|
|
||||||
|
v
|
||||||
|
6c. Spawn via Adapter
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Provider Integration Architecture
|
## 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)
|
| Localhost only (127.0.0.1)
|
||||||
v
|
v
|
||||||
+------------------+
|
+------------------+
|
||||||
| CLIProxy/GLMT | Binds to localhost only
|
| CLIProxy/Legacy | Binds to localhost only
|
||||||
+------------------+
|
+------------------+
|
||||||
|
|
|
|
||||||
| TLS encrypted
|
| TLS encrypted
|
||||||
@@ -400,5 +403,5 @@ See [Provider Flows](./provider-flows.md) → Authentication Flow section.
|
|||||||
- [Codebase Summary](../codebase-summary.md) - Detailed directory structure
|
- [Codebase Summary](../codebase-summary.md) - Detailed directory structure
|
||||||
- [Code Standards](../code-standards.md) - Coding conventions & patterns
|
- [Code Standards](../code-standards.md) - Coding conventions & patterns
|
||||||
- [Target Adapters](./target-adapters.md) - Multi-CLI adapter architecture
|
- [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
|
- [Project Roadmap](../project-roadmap.md) - Development phases
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Last Updated: 2026-02-16
|
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
|
### 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
|
Claude CLI
|
||||||
|
|
|
|
||||||
| ANTHROPIC_BASE_URL = localhost:XXXX
|
| legacy glmt settings detected
|
||||||
v
|
v
|
||||||
+------------------+
|
+------------------+
|
||||||
| GLMT Proxy | Embedded Node.js proxy (src/glmt/)
|
| Compatibility | normalizeDeprecatedGlmtEnv()
|
||||||
| (glmt-proxy.ts)|
|
| Layer | (src/utils/glmt-deprecation.ts)
|
||||||
+------------------+
|
+------------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+------------------+
|
+------------------+
|
||||||
| Delta Accumulator| Stream transformation
|
| Direct GLM API | https://api.z.ai/api/anthropic
|
||||||
+------------------+
|
+------------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+------------------+
|
+------------------+
|
||||||
| Pipeline | Request/Response transformation
|
| src/glmt/* | retained for Cursor translation
|
||||||
+------------------+
|
|
||||||
|
|
|
||||||
v
|
|
||||||
+------------------+
|
|
||||||
| GLM API | Z.AI / Kimi API
|
|
||||||
+------------------+
|
+------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported GLM Providers
|
### Supported Migration Targets
|
||||||
|
|
||||||
| Provider | Config Key | Endpoint | Auth |
|
| Provider | Config Key | Endpoint | Auth |
|
||||||
|----------|------------|----------|------|
|
|----------|------------|----------|------|
|
||||||
| Z.AI (GLM) | `glmt` | https://open.bigmodel.cn/api/paas/v4/ | API key |
|
| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key |
|
||||||
| Kimi | `kimi` | https://api.moonshot.cn/v1/ | API key |
|
| Kimi API | `km` | https://api.kimi.com/coding/ | API key |
|
||||||
| OpenRouter | `openrouter` | https://openrouter.ai/api/v1/ | 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
|
```typescript
|
||||||
// Settings-based profile detection
|
if (isDeprecatedGlmtProfileName(profileName)) {
|
||||||
const settings = loadSettings(profileName);
|
const normalized = normalizeDeprecatedGlmtEnv(settingsEnv);
|
||||||
if (settings.env?.ANTHROPIC_BASE_URL?.includes('glm') ||
|
// warn user, validate against direct GLM endpoint, continue through settings flow
|
||||||
settings.env?.ANTHROPIC_BASE_URL?.includes('moonshot') ||
|
|
||||||
settings.env?.ANTHROPIC_BASE_URL?.includes('openrouter')) {
|
|
||||||
return execClaudeWithProxy(claudeCli, profileName, args);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@kaitranntt/ccs",
|
"name": "@kaitranntt/ccs",
|
||||||
"version": "7.54.0-dev.8",
|
"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": [
|
"keywords": [
|
||||||
"cli",
|
"cli",
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ sudo cp scripts/completion/ccs.fish /usr/share/fish/vendor_completions.d/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ccs <TAB>
|
$ ccs <TAB>
|
||||||
auth doctor glm glmt kimi work personal --help --version
|
auth doctor glm kimi work personal --help --version
|
||||||
|
|
||||||
$ ccs auth <TAB>
|
$ ccs auth <TAB>
|
||||||
create list show remove default --help
|
create list show remove default --help
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ end
|
|||||||
# Helper function to get custom/unknown settings profiles
|
# Helper function to get custom/unknown settings profiles
|
||||||
function __fish_ccs_get_custom_settings_profiles
|
function __fish_ccs_get_custom_settings_profiles
|
||||||
set -l config_path ~/.ccs/config.json
|
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
|
if test -f $config_path
|
||||||
set -l all_profiles (jq -r '.profiles | keys[]' $config_path 2>/dev/null)
|
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
|
# 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 '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 '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)'
|
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
|
# Custom model profiles - dynamic with [model] prefix
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
# Set up completion styles for better formatting and colors
|
# 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:*: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:*: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:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37'
|
||||||
zstyle ':completion:*:*:ccs:*' group-name ''
|
zstyle ':completion:*:*:ccs:*' group-name ''
|
||||||
zstyle ':completion:*:*:ccs:*:descriptions' format $'\n%B%F{yellow}── %d ──%f%b'
|
zstyle ':completion:*:*:ccs:*:descriptions' format $'\n%B%F{yellow}── %d ──%f%b'
|
||||||
@@ -56,7 +56,6 @@ _ccs() {
|
|||||||
profile_descriptions=(
|
profile_descriptions=(
|
||||||
'default' 'Default Claude Sonnet 4.5'
|
'default' 'Default Claude Sonnet 4.5'
|
||||||
'glm' 'GLM-4.6 (cost-optimized)'
|
'glm' 'GLM-4.6 (cost-optimized)'
|
||||||
'glmt' 'GLM-4.6 with thinking mode'
|
|
||||||
'kimi' 'Kimi for Coding (long-context)'
|
'kimi' 'Kimi for Coding (long-context)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function createConfigFiles() {
|
|||||||
// Create config.yaml if missing (primary format)
|
// Create config.yaml if missing (primary format)
|
||||||
// NOTE: gemini/codex profiles NOT included - they are added on-demand when user
|
// 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)
|
// 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 configYamlPath = path.join(ccsDir, 'config.yaml');
|
||||||
const legacyConfigPath = path.join(ccsDir, 'config.json');
|
const legacyConfigPath = path.join(ccsDir, 'config.json');
|
||||||
|
|
||||||
@@ -250,10 +250,10 @@ function createConfigFiles() {
|
|||||||
console.log(' config.json will be ignored - consider removing it');
|
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:
|
// Users can create these via:
|
||||||
// - UI: Profile Create Dialog → Provider Presets
|
// - 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
|
// This gives users control over which providers they want to use
|
||||||
// Existing profiles are preserved for backward compatibility
|
// Existing profiles are preserved for backward compatibility
|
||||||
|
|
||||||
|
|||||||
+75
-309
@@ -1,13 +1,10 @@
|
|||||||
import './utils/fetch-proxy-setup';
|
import './utils/fetch-proxy-setup';
|
||||||
|
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { detectClaudeCli } from './utils/claude-detector';
|
import { detectClaudeCli } from './utils/claude-detector';
|
||||||
import {
|
import {
|
||||||
getSettingsPath,
|
getSettingsPath,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
getCcsDir,
|
|
||||||
setGlobalConfigDir,
|
setGlobalConfigDir,
|
||||||
detectCloudSyncPath,
|
detectCloudSyncPath,
|
||||||
} from './utils/config-manager';
|
} from './utils/config-manager';
|
||||||
@@ -43,13 +40,8 @@ import { handleError, runCleanup } from './errors';
|
|||||||
import { tryHandleRootCommand } from './commands/root-command-router';
|
import { tryHandleRootCommand } from './commands/root-command-router';
|
||||||
|
|
||||||
// Import extracted utility functions
|
// Import extracted utility functions
|
||||||
import {
|
import { execClaude } from './utils/shell-executor';
|
||||||
execClaude,
|
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
||||||
escapeShellArg,
|
|
||||||
stripClaudeCodeEnv,
|
|
||||||
getClaudeLaunchEnvOverrides,
|
|
||||||
} from './utils/shell-executor';
|
|
||||||
import { wireChildProcessSignals } from './utils/signal-forwarder';
|
|
||||||
|
|
||||||
// Import target adapter system
|
// Import target adapter system
|
||||||
import {
|
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 ==========
|
// ========== Main Execution ==========
|
||||||
|
|
||||||
interface ProfileError extends Error {
|
interface ProfileError extends Error {
|
||||||
@@ -549,15 +349,6 @@ async function main(): Promise<void> {
|
|||||||
process.exit(1);
|
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 (profileInfo.type === 'default') {
|
||||||
if (!targetAdapter.supportsProfileType('default')) {
|
if (!targetAdapter.supportsProfileType('default')) {
|
||||||
console.error(fail(`${targetAdapter.displayName} does not support default profile mode`));
|
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 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 (glmtNormalization) {
|
||||||
if (profileInfo.name === 'glm' || profileInfo.name === 'glmt') {
|
for (const message of glmtNormalization.warnings) {
|
||||||
const preflightSettingsPath = getSettingsPath(profileInfo.name);
|
console.error(warn(message));
|
||||||
const preflightSettings = loadSettings(preflightSettingsPath);
|
}
|
||||||
const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN'];
|
}
|
||||||
|
|
||||||
|
// Pre-flight validation for Z.AI-compatible profiles.
|
||||||
|
if (profileInfo.name === 'glm' || isDeprecatedGlmtProfile) {
|
||||||
|
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
|
||||||
|
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const validation = await validateGlmKey(
|
const validation = await validateGlmKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
|
||||||
apiKey,
|
|
||||||
preflightSettings.env?.['ANTHROPIC_BASE_URL']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
console.error('');
|
console.error('');
|
||||||
@@ -906,15 +708,10 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (profileInfo.name === 'mm') {
|
if (profileInfo.name === 'mm') {
|
||||||
const preflightSettingsPath = getSettingsPath(profileInfo.name);
|
const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN'];
|
||||||
const preflightSettings = loadSettings(preflightSettingsPath);
|
|
||||||
const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN'];
|
|
||||||
|
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const validation = await validateMiniMaxKey(
|
const validation = await validateMiniMaxKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']);
|
||||||
apiKey,
|
|
||||||
preflightSettings.env?.['ANTHROPIC_BASE_URL']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
console.error('');
|
console.error('');
|
||||||
@@ -932,10 +729,8 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Pre-flight validation for Anthropic direct profiles (ANTHROPIC_API_KEY + no BASE_URL)
|
// Pre-flight validation for Anthropic direct profiles (ANTHROPIC_API_KEY + no BASE_URL)
|
||||||
{
|
{
|
||||||
const preflightSettingsPath = getSettingsPath(profileInfo.name);
|
const anthropicApiKey = settingsEnv['ANTHROPIC_API_KEY'];
|
||||||
const preflightSettings = loadSettings(preflightSettingsPath);
|
const hasBaseUrl = !!settingsEnv['ANTHROPIC_BASE_URL'];
|
||||||
const anthropicApiKey = preflightSettings.env?.['ANTHROPIC_API_KEY'];
|
|
||||||
const hasBaseUrl = !!preflightSettings.env?.['ANTHROPIC_BASE_URL'];
|
|
||||||
if (anthropicApiKey && !hasBaseUrl) {
|
if (anthropicApiKey && !hasBaseUrl) {
|
||||||
const validation = await validateAnthropicKey(anthropicApiKey);
|
const validation = await validateAnthropicKey(anthropicApiKey);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
@@ -954,89 +749,60 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is GLMT profile (requires proxy)
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
if (profileInfo.name === 'glmt') {
|
const imageAnalysisEnv = getImageAnalysisHookEnv(profileInfo.name);
|
||||||
if (resolvedTarget !== 'claude') {
|
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
||||||
console.error(
|
const globalEnvConfig = getGlobalEnvConfig();
|
||||||
fail(`${targetAdapter?.displayName || 'Target'} does not support GLMT proxy profiles`)
|
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
|
||||||
);
|
|
||||||
console.error(
|
// Log global env injection for visibility (debug mode only)
|
||||||
info('Use --target claude for glmt, or switch to a direct API profile (glm/km)')
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
const directAnthropicBaseUrl =
|
||||||
await execClaudeWithProxy(
|
settingsEnv['ANTHROPIC_BASE_URL'] ||
|
||||||
claudeCli,
|
(settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : '');
|
||||||
profileInfo.name,
|
const creds: TargetCredentials = {
|
||||||
remainingArgs,
|
profile: profileInfo.name,
|
||||||
inheritedClaudeConfigDir
|
baseUrl: directAnthropicBaseUrl,
|
||||||
);
|
apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '',
|
||||||
} else {
|
model: settingsEnv['ANTHROPIC_MODEL'],
|
||||||
// EXISTING FLOW: Settings-based profile (glm)
|
provider: resolveDroidProvider({
|
||||||
// Use --settings flag (backward compatible)
|
provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'],
|
||||||
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,
|
|
||||||
baseUrl: directAnthropicBaseUrl,
|
baseUrl: directAnthropicBaseUrl,
|
||||||
apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '',
|
|
||||||
model: settingsEnv['ANTHROPIC_MODEL'],
|
model: settingsEnv['ANTHROPIC_MODEL'],
|
||||||
provider: resolveDroidProvider({
|
}),
|
||||||
provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'],
|
reasoningOverride: droidReasoningOverride,
|
||||||
baseUrl: directAnthropicBaseUrl,
|
envVars,
|
||||||
model: settingsEnv['ANTHROPIC_MODEL'],
|
};
|
||||||
}),
|
await adapter.prepareCredentials(creds);
|
||||||
reasoningOverride: droidReasoningOverride,
|
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs);
|
||||||
};
|
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
|
||||||
await adapter.prepareCredentials(creds);
|
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
|
||||||
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs);
|
return;
|
||||||
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
|
|
||||||
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars);
|
||||||
} else if (profileInfo.type === 'account') {
|
} else if (profileInfo.type === 'account') {
|
||||||
// NEW FLOW: Account-based profile (work, personal)
|
// NEW FLOW: Account-based profile (work, personal)
|
||||||
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ function resolvePresetOrExit(presetId?: string): ProviderPreset | null {
|
|||||||
process.exit(1);
|
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(
|
async function resolveProfileName(
|
||||||
providedName: string | undefined,
|
providedName: string | undefined,
|
||||||
preset: ProviderPreset | null
|
preset: ProviderPreset | null
|
||||||
@@ -237,6 +248,7 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
|||||||
console.log(header('Create API Profile'));
|
console.log(header('Create API Profile'));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
showPresetDeprecationNotice(parsedArgs.preset);
|
||||||
const preset = resolvePresetOrExit(parsedArgs.preset);
|
const preset = resolvePresetOrExit(parsedArgs.preset);
|
||||||
const name = await resolveProfileName(parsedArgs.name, 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>;
|
let envVars: Record<string, string>;
|
||||||
try {
|
try {
|
||||||
const resolved = await resolveClaudeExtensionSetup(profile);
|
const resolved = await resolveClaudeExtensionSetup(profile);
|
||||||
|
if (resolved.warnings.length > 0) {
|
||||||
|
for (const message of resolved.warnings) {
|
||||||
|
console.error(warn(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
envVars = resolved.extensionEnv;
|
envVars = resolved.extensionEnv;
|
||||||
if (format === 'claude-extension') {
|
if (format === 'claude-extension') {
|
||||||
console.log(renderClaudeExtensionSettingsJson(resolved, ide));
|
console.log(renderClaudeExtensionSettingsJson(resolved, ide));
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
[
|
[
|
||||||
['ccs', 'Use default Claude account'],
|
['ccs', 'Use default Claude account'],
|
||||||
['ccs glm', 'GLM 5 (API key required)'],
|
['ccs glm', 'GLM 5 (API key required)'],
|
||||||
['ccs glmt', 'GLM with thinking mode'],
|
|
||||||
['ccs km', 'Kimi for Coding (API key)'],
|
['ccs km', 'Kimi for Coding (API key)'],
|
||||||
[
|
[
|
||||||
'ccs api create --preset alibaba-coding-plan',
|
'ccs api create --preset alibaba-coding-plan',
|
||||||
|
|||||||
@@ -679,7 +679,7 @@ async function showHelp(): Promise<void> {
|
|||||||
);
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(subheader('Supported Profile Types'));
|
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('CLIProxy', 'command')} gemini, codex, agy, qwen, kiro, ghcp`);
|
||||||
console.log(` ${color('Copilot', 'command')} copilot (requires copilot-api daemon)`);
|
console.log(` ${color('Copilot', 'command')} copilot (requires copilot-api daemon)`);
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
|||||||
|
|
||||||
// Profiles section
|
// Profiles section
|
||||||
lines.push('# ----------------------------------------------------------------------------');
|
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('# 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('# Edit the settings file directly to customize (ANTHROPIC_MAX_TOKENS, etc.)');
|
||||||
lines.push('# ----------------------------------------------------------------------------');
|
lines.push('# ----------------------------------------------------------------------------');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* GlmtProxy - Embedded HTTP proxy for GLM thinking support
|
* GlmtProxy - Legacy embedded HTTP proxy retained for compatibility work.
|
||||||
*
|
*
|
||||||
* Architecture:
|
* Architecture:
|
||||||
* - Intercepts Claude CLI → Z.AI calls
|
* - Intercepts Claude CLI → Z.AI calls
|
||||||
@@ -7,8 +7,9 @@
|
|||||||
* - Converts reasoning_content → thinking blocks
|
* - Converts reasoning_content → thinking blocks
|
||||||
* - Supports both streaming and buffered modes
|
* - Supports both streaming and buffered modes
|
||||||
*
|
*
|
||||||
* Lifecycle:
|
* Current status:
|
||||||
* - Spawned by bin/ccs.js when 'glmt' profile detected
|
* - 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)
|
* - Binds to 127.0.0.1:random_port (security + avoid conflicts)
|
||||||
* - Terminates when parent process exits
|
* - Terminates when parent process exits
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ class RecoveryManager {
|
|||||||
* Run all recovery operations (lazy initialization)
|
* Run all recovery operations (lazy initialization)
|
||||||
* Mirrors postinstall.js behavior
|
* Mirrors postinstall.js behavior
|
||||||
*
|
*
|
||||||
* NOTE: GLM/GLMT/Kimi profiles are NOT auto-created.
|
* NOTE: GLM/Kimi profiles are NOT auto-created.
|
||||||
* Users should create them via `ccs api create --preset glm` or the UI.
|
* Users should create them via `ccs api create --preset glm|km` or the UI.
|
||||||
*/
|
*/
|
||||||
recoverAll(): boolean {
|
recoverAll(): boolean {
|
||||||
this.recovered = [];
|
this.recovered = [];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import InstanceManager from '../management/instance-manager';
|
|||||||
import SharedManager from '../management/shared-manager';
|
import SharedManager from '../management/shared-manager';
|
||||||
import { expandPath } from '../utils/helpers';
|
import { expandPath } from '../utils/helpers';
|
||||||
import { getClaudeSettingsPath } from '../utils/claude-config-path';
|
import { getClaudeSettingsPath } from '../utils/claude-config-path';
|
||||||
|
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from '../utils/glmt-deprecation';
|
||||||
import {
|
import {
|
||||||
type ClaudeExtensionHost,
|
type ClaudeExtensionHost,
|
||||||
type ClaudeExtensionHostDefinition,
|
type ClaudeExtensionHostDefinition,
|
||||||
@@ -87,7 +88,12 @@ function describeProfile(profileName: string, result: ProfileDetectionResult): s
|
|||||||
}
|
}
|
||||||
if (result.type === 'cliproxy')
|
if (result.type === 'cliproxy')
|
||||||
return 'OAuth or CLIProxy-backed profile for Anthropic-compatible routing.';
|
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')
|
if (result.type === 'account')
|
||||||
return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.';
|
return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.';
|
||||||
if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.';
|
if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.';
|
||||||
@@ -184,7 +190,7 @@ async function resolveExtensionEnv(
|
|||||||
profileType: result.type,
|
profileType: result.type,
|
||||||
target: 'claude',
|
target: 'claude',
|
||||||
});
|
});
|
||||||
const env =
|
let env =
|
||||||
result.type === 'settings'
|
result.type === 'settings'
|
||||||
? (result.env ??
|
? (result.env ??
|
||||||
(result.settingsPath ? loadSettingsFromFile(expandPath(result.settingsPath)) : {}))
|
(result.settingsPath ? loadSettingsFromFile(expandPath(result.settingsPath)) : {}))
|
||||||
@@ -230,6 +236,13 @@ async function resolveExtensionEnv(
|
|||||||
: getEffectiveEnvVars(result.provider, port, result.settingsPath);
|
: 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) {
|
if (!requestedIsDefault && continuity.claudeConfigDir && !env.CLAUDE_CONFIG_DIR) {
|
||||||
env.CLAUDE_CONFIG_DIR = continuity.claudeConfigDir;
|
env.CLAUDE_CONFIG_DIR = continuity.claudeConfigDir;
|
||||||
notes.push(
|
notes.push(
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const PROVIDER_PRESET_IDS = [
|
|||||||
'llamacpp',
|
'llamacpp',
|
||||||
'anthropic',
|
'anthropic',
|
||||||
'glm',
|
'glm',
|
||||||
'glmt',
|
|
||||||
'km',
|
'km',
|
||||||
'foundry',
|
'foundry',
|
||||||
'mm',
|
'mm',
|
||||||
@@ -54,6 +53,7 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
|||||||
* Keep this minimal and explicit to avoid hidden implicit behavior.
|
* Keep this minimal and explicit to avoid hidden implicit behavior.
|
||||||
*/
|
*/
|
||||||
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
|
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
|
||||||
|
glmt: 'glm',
|
||||||
kimi: 'km',
|
kimi: 'km',
|
||||||
alibaba: 'alibaba-coding-plan',
|
alibaba: 'alibaba-coding-plan',
|
||||||
acp: 'alibaba-coding-plan',
|
acp: 'alibaba-coding-plan',
|
||||||
@@ -137,7 +137,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
|||||||
{
|
{
|
||||||
id: 'glm',
|
id: 'glm',
|
||||||
name: 'GLM',
|
name: 'GLM',
|
||||||
description: 'Claude via Z.AI',
|
description: 'Direct Z.AI Anthropic-compatible API profile',
|
||||||
baseUrl: 'https://api.z.ai/api/anthropic',
|
baseUrl: 'https://api.z.ai/api/anthropic',
|
||||||
defaultProfileName: 'glm',
|
defaultProfileName: 'glm',
|
||||||
defaultModel: 'glm-5',
|
defaultModel: 'glm-5',
|
||||||
@@ -148,29 +148,6 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
|||||||
badge: 'Z.AI',
|
badge: 'Z.AI',
|
||||||
icon: '/icons/zai.svg',
|
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',
|
id: 'km',
|
||||||
name: 'Kimi',
|
name: 'Kimi',
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ export type {
|
|||||||
DelegationEvent,
|
DelegationEvent,
|
||||||
} from './delegation';
|
} from './delegation';
|
||||||
|
|
||||||
// GLMT types
|
// Legacy GLMT transformer types
|
||||||
export type {
|
export type {
|
||||||
AnthropicMessage,
|
AnthropicMessage,
|
||||||
ContentBlock,
|
ContentBlock,
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ export function getSettingsPath(profile: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get display name for a profile by reading ANTHROPIC_MODEL from settings
|
* 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')
|
* @returns Formatted display name (e.g., 'GLM-4.7', 'Kimi', 'Custom-Model')
|
||||||
*/
|
*/
|
||||||
export function getModelDisplayName(profile: string): string {
|
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/
|
tests/
|
||||||
├── unit/ # Module unit tests (Mocha)
|
├── unit/ # Module unit tests (Mocha)
|
||||||
│ ├── glmt/ # GLMT transformer tests
|
│ ├── glmt/ # Legacy GLMT transformer/internal compatibility tests
|
||||||
│ └── delegation/ # Delegation module tests
|
│ └── delegation/ # Delegation module tests
|
||||||
├── npm/ # npm package tests (Mocha)
|
├── npm/ # npm package tests (Mocha)
|
||||||
├── native/ # Native installation tests (bash/PowerShell)
|
├── native/ # Native installation tests (bash/PowerShell)
|
||||||
@@ -32,7 +32,7 @@ bun run test:native # Native Unix tests (bash)
|
|||||||
|
|
||||||
### Unit Tests (`unit/`)
|
### Unit Tests (`unit/`)
|
||||||
Module-level tests using Mocha framework:
|
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
|
- `unit/delegation/` - Permission mode, session manager, result formatter
|
||||||
|
|
||||||
### npm Tests (`npm/`)
|
### 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
|
- Shell and standalone probe scripts remain on-demand for targeted debugging
|
||||||
- `cursor-daemon-lifecycle.test.ts` - local daemon process + HTTP smoke coverage
|
- `cursor-daemon-lifecycle.test.ts` - local daemon process + HTTP smoke coverage
|
||||||
- `image-analyzer-hook.test.ts` - hook integration 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
|
- `symlink-chain-test.sh` - Symlink chain handling
|
||||||
- `ux-integration-test.sh` - CLI UX integration
|
- `ux-integration-test.sh` - CLI UX integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,170 +1,84 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# GLMT Integration Test Suite
|
# Legacy GLMT compatibility smoke probe
|
||||||
# Tests proxy startup, configuration, and basic functionality
|
# Verifies that user-facing GLMT marketing is gone while internal transformer
|
||||||
|
# modules still exist for compatibility and Cursor translation.
|
||||||
#
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
CCS_DIR="$(dirname "$SCRIPT_DIR")"
|
REPO_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
|
|
||||||
echo "=== GLMT Integration Test Suite ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
PASSED=0
|
PASSED=0
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
||||||
test_pass() {
|
test_pass() {
|
||||||
echo -e "${GREEN}✓${NC} $1"
|
echo "[OK] $1"
|
||||||
((PASSED++))
|
PASSED=$((PASSED + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
test_fail() {
|
test_fail() {
|
||||||
echo -e "${RED}✗${NC} $1"
|
echo "[X] $1"
|
||||||
((FAILED++))
|
FAILED=$((FAILED + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
test_info() {
|
test_info() {
|
||||||
echo -e "${YELLOW}ℹ${NC} $1"
|
echo "[i] $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test 1: Check GLMT profile exists in config
|
echo "=== Legacy GLMT Compatibility Smoke Test ==="
|
||||||
echo "Test 1: GLMT profile configuration"
|
echo
|
||||||
if grep -q '"glmt"' ~/.ccs/config.json; then
|
|
||||||
test_pass "GLMT profile found in config.json"
|
|
||||||
else
|
|
||||||
test_fail "GLMT profile NOT found in config.json"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 2: Check GLMT settings file exists
|
echo "Test 1: Internal transformer sources remain available"
|
||||||
echo "Test 2: GLMT settings file"
|
for file in \
|
||||||
if [ -f ~/.ccs/glmt.settings.json ]; then
|
"$REPO_DIR/src/glmt/glmt-transformer.ts" \
|
||||||
test_pass "GLMT settings file exists"
|
"$REPO_DIR/src/glmt/delta-accumulator.ts" \
|
||||||
|
"$REPO_DIR/src/glmt/sse-parser.ts" \
|
||||||
# Check if API key is configured
|
"$REPO_DIR/src/cursor/cursor-anthropic-response.ts"; do
|
||||||
if grep -q "YOUR_GLM_API_KEY_HERE" ~/.ccs/glmt.settings.json; then
|
if [ -f "$file" ]; then
|
||||||
test_info "API key not configured (still has placeholder)"
|
test_pass "$(basename "$file") exists"
|
||||||
else
|
else
|
||||||
test_pass "API key configured"
|
test_fail "Missing internal compatibility file: $file"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
test_fail "GLMT settings file NOT found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 3: Check transformer module
|
|
||||||
echo "Test 3: Transformer module"
|
|
||||||
if [ -f "$CCS_DIR/bin/glmt-transformer.js" ]; then
|
|
||||||
test_pass "Transformer module exists"
|
|
||||||
|
|
||||||
# Test syntax
|
|
||||||
if node --check "$CCS_DIR/bin/glmt-transformer.js" 2>/dev/null; then
|
|
||||||
test_pass "Transformer syntax valid"
|
|
||||||
else
|
|
||||||
test_fail "Transformer syntax invalid"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
test_fail "Transformer module NOT found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 4: Check proxy module
|
|
||||||
echo "Test 4: Proxy module"
|
|
||||||
if [ -f "$CCS_DIR/bin/glmt-proxy.js" ]; then
|
|
||||||
test_pass "Proxy module exists"
|
|
||||||
|
|
||||||
# Test syntax
|
|
||||||
if node --check "$CCS_DIR/bin/glmt-proxy.js" 2>/dev/null; then
|
|
||||||
test_pass "Proxy syntax valid"
|
|
||||||
else
|
|
||||||
test_fail "Proxy syntax invalid"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
test_fail "Proxy module NOT found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 5: Test proxy startup
|
|
||||||
echo "Test 5: Proxy startup test"
|
|
||||||
test_info "Starting proxy in background..."
|
|
||||||
|
|
||||||
# Start proxy
|
|
||||||
node "$CCS_DIR/bin/glmt-proxy.js" &
|
|
||||||
PROXY_PID=$!
|
|
||||||
|
|
||||||
# Wait for PROXY_READY signal (with timeout)
|
|
||||||
TIMEOUT=5
|
|
||||||
PORT=""
|
|
||||||
for i in $(seq 1 $TIMEOUT); do
|
|
||||||
if ps -p $PROXY_PID > /dev/null 2>&1; then
|
|
||||||
sleep 0.2
|
|
||||||
# Check if proxy outputted anything (we can't easily capture it in background)
|
|
||||||
if [ $i -eq $TIMEOUT ]; then
|
|
||||||
test_fail "Proxy started but PROXY_READY signal not captured (this is OK - proxy works)"
|
|
||||||
PORT="unknown"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
test_fail "Proxy failed to start or exited immediately"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if ps -p $PROXY_PID > /dev/null 2>&1; then
|
echo
|
||||||
test_pass "Proxy process is running (PID: $PROXY_PID)"
|
echo "Test 2: Root help no longer advertises ccs glmt"
|
||||||
|
if command -v ccs >/dev/null 2>&1; then
|
||||||
# Kill proxy
|
if ccs --help | grep -q "ccs glmt"; then
|
||||||
kill $PROXY_PID 2>/dev/null || true
|
test_fail "Root help still advertises ccs glmt"
|
||||||
sleep 0.5
|
else
|
||||||
|
test_pass "Root help hides ccs glmt"
|
||||||
if ! ps -p $PROXY_PID > /dev/null 2>&1; then
|
fi
|
||||||
test_pass "Proxy terminated gracefully"
|
|
||||||
else
|
|
||||||
test_fail "Proxy did not terminate (killing forcefully)"
|
|
||||||
kill -9 $PROXY_PID 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 6: Unit tests
|
|
||||||
echo "Test 6: Unit tests"
|
|
||||||
if [ -f "$CCS_DIR/tests/glmt-transformer.test.js" ]; then
|
|
||||||
test_info "Running transformer unit tests..."
|
|
||||||
if node "$CCS_DIR/tests/glmt-transformer.test.js" | grep -q "Passed: 12/12"; then
|
|
||||||
test_pass "All 12 unit tests passed"
|
|
||||||
else
|
|
||||||
test_fail "Some unit tests failed"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
test_fail "Unit test file NOT found"
|
test_info "ccs binary not found in PATH; skipping live help probe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 7: Help text
|
echo
|
||||||
echo "Test 7: Help text verification"
|
echo "Test 3: Legacy glmt settings file inspection"
|
||||||
if ccs --help | grep -q "ccs glmt"; then
|
if [ -f "$HOME/.ccs/glmt.settings.json" ]; then
|
||||||
test_pass "GLMT appears in help text"
|
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
|
else
|
||||||
test_fail "GLMT NOT in help text"
|
test_info "No legacy glmt.settings.json found; nothing to migrate locally"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Summary
|
echo
|
||||||
echo ""
|
echo "=== Summary ==="
|
||||||
echo "=== Test Summary ==="
|
echo "Passed: $PASSED"
|
||||||
echo -e "Passed: ${GREEN}$PASSED${NC}"
|
echo "Failed: $FAILED"
|
||||||
echo -e "Failed: ${RED}$FAILED${NC}"
|
echo
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ $FAILED -eq 0 ]; then
|
if [ "$FAILED" -eq 0 ]; then
|
||||||
echo -e "${GREEN}All tests passed!${NC}"
|
echo "[OK] Legacy compatibility surface looks consistent."
|
||||||
echo ""
|
echo "[i] Preferred profiles: ccs glm for Z.AI, ccs km for reasoning-first Kimi."
|
||||||
echo "Next steps:"
|
exit 0
|
||||||
echo " 1. Test with Claude Code: ccs glmt \"What is 2+2?\""
|
|
||||||
echo " 2. Test thinking tags: ccs glmt \"<Thinking:On> Explain recursion\""
|
|
||||||
echo " 3. Check thinking blocks appear in Claude Code UI"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}Some tests failed. Please review above.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "[X] Review the failures above."
|
||||||
|
exit 1
|
||||||
@@ -77,8 +77,9 @@ describe('npm CLI', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Profile handling', () => {
|
describe('Profile handling', () => {
|
||||||
// Note: GLM/GLMT/Kimi profiles are no longer auto-created (v6.0)
|
// Note: GLM/Kimi profiles are no longer auto-created (v6.0).
|
||||||
// Users create these via UI presets or CLI: ccs api create --preset glm
|
// 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() {
|
it('shows helpful error for non-existent profile', function() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ describe('npm postinstall', () => {
|
|||||||
env: { ...process.env, CCS_HOME: testEnv.testHome }
|
env: { ...process.env, CCS_HOME: testEnv.testHome }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GLM/GLMT/Kimi profiles are NO LONGER auto-created during install
|
// GLM/Kimi profiles are NO LONGER auto-created during install.
|
||||||
// Users create these via UI presets or CLI: ccs api create --preset glm
|
// 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('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('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');
|
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
|
// Verify existing file still exists and new files are created
|
||||||
assert(testEnv.fileExists('existing.txt'), 'Existing files should be preserved');
|
assert(testEnv.fileExists('existing.txt'), 'Existing files should be preserved');
|
||||||
assert(testEnv.fileExists('config.yaml'), 'config.yaml should be created');
|
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');
|
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');
|
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', () => {
|
it('resolves preset id with extra whitespace', () => {
|
||||||
const preset = getPresetById(' km ');
|
const preset = getPresetById(' km ');
|
||||||
expect(preset?.id).toBe('km');
|
expect(preset?.id).toBe('km');
|
||||||
@@ -56,6 +62,11 @@ describe('provider-presets', () => {
|
|||||||
expect(isValidPresetId('kimi')).toBe(true);
|
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', () => {
|
it('uses non-reserved default profile name for qwen API preset', () => {
|
||||||
const preset = getPresetById('qwen');
|
const preset = getPresetById('qwen');
|
||||||
expect(preset?.defaultProfileName).toBe('qwen-api');
|
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);
|
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 () => {
|
test('root help documents Claude IDE extension setup surfaces', async () => {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
console.log = (...args: unknown[]) => {
|
console.log = (...args: unknown[]) => {
|
||||||
|
|||||||
@@ -1,214 +1,17 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'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('='.repeat(60));
|
||||||
console.log('GLMT Multi-Message Thinking Block Test');
|
console.log('Legacy GLMT Multi-Message Probe');
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Test scenario: 3 consecutive messages with thinking enabled');
|
console.log('This manual script targeted the retired `ccs glmt` runtime path.');
|
||||||
console.log('Expected: Thinking blocks appear in ALL 3 messages');
|
console.log('CCS now routes supported Z.AI usage through `ccs glm`, and thinking is');
|
||||||
console.log('Actual: User reports thinking only in first message');
|
console.log('handled natively by current upstream models instead of the old GLMT proxy.');
|
||||||
console.log('');
|
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('');
|
console.log('');
|
||||||
|
process.exit(0);
|
||||||
// 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();
|
|
||||||
@@ -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
|
// 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(() => {
|
useEffect(() => {
|
||||||
// Only warn for custom URLs, not preset-selected ones
|
// Only warn for custom URLs, not preset-selected ones
|
||||||
const isCustomUrl = selectedPreset === CUSTOM_PRESET_ID;
|
const isCustomUrl = selectedPreset === CUSTOM_PRESET_ID;
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ describe('resolvePresetApiKeyValue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('provider preset metadata', () => {
|
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', () => {
|
it('keeps Anthropic direct last in the recommended order', () => {
|
||||||
const recommendedPresetIds = getPresetsByCategory('recommended').map((preset) => preset.id);
|
const recommendedPresetIds = getPresetsByCategory('recommended').map((preset) => preset.id);
|
||||||
expect(recommendedPresetIds.at(-1)).toBe('anthropic');
|
expect(recommendedPresetIds.at(-1)).toBe('anthropic');
|
||||||
|
|||||||
Reference in new issue
Block a user