Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CLI and ModelTrace integration tests
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python: ['3.10', '3.12', '3.14']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
path: switcher
- uses: actions/checkout@v4
with:
repository: FastR-D/ModelTrace
path: fingerprint-library
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: python -m pip install ./fingerprint-library ./switcher build
- run: python -m unittest discover -s tests -v
working-directory: switcher
env:
PYTHONUTF8: '0'
- run: python -m build
working-directory: switcher
- run: python -m build fingerprint-library
- run: python fingerprint-library/tests/verify_installation.py --wheels fingerprint-library/dist/modeltrace-0.1.0-py3-none-any.whl switcher/dist/cc_switch_simple-0.2.1-py3-none-any.whl
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
my_conf.yaml
my_conf.yaml
.venv/
__pycache__/
*.egg-info/
build/
dist/
confs/
output/
79 changes: 34 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,47 @@
### 最简cc-switch
目前cc-switch-cli只能基于tui配置,操作比较麻烦。
实现了一个简单版,一键导入配置,增加antropic和openai中转。
# cc-switch-simple

读取本地 YAML,切换 Codex 或 Claude Code 配置,用 `check` 调用 ModelTrace 检测模型指纹。

## 安装

```bash
chmod +x cc-simple.py
# 可选:创建软链接到 PATH
ln -s $(pwd)/cc-simple.py /usr/local/bin/cc-simple
需要 Python 3.10+。在项目目录执行:

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install .
```

## 使用
`check` 还需要安装 ModelTrace,将下面路径替换为其源码目录:

### 1. 添加配置
```bash
$ python3 cc-simple.py add conf.yaml

# OpenAI 兼容 API 配置示例
$ cat conf.yaml
name=xingshu
base_url=https://submodelhub.ilaien.com/v1
type=openai
api_key=sk-xxxx
model_name=gpt-4

# Anthropic API 配置示例
$ cat anthropic.yaml
name=anthropic-official
base_url=https://api.anthropic.com
type=anthropic
api_key=sk-ant-api03-xxxx
model_name=claude-opus-5-5
```sh
python -m pip install /path/to/ModelTrace
```

配置字段说明:
- `name` - 配置名称(必填)
- `base_url` - API 端点 URL(必填)
- `type` - 提供商类型:`openai` 或 `anthropic`(必填)
- `api_key` - API 密钥(必填)
- `model_name` - 模型名称(可选,默认:openai=gpt-4, anthropic=claude-opus-5-5)
## 配置

保存为 `provider.yaml`:

### 2. 查看所有配置
```bash
$ python3 cc-simple.py list
1 sbbbbbb https://fast.sbbbbbbbbb.xyz/v1
2 anthropic-proxy https://api.anthropic.com
```yaml
name: my-provider
base_url: https://api.example.com/v1
type: openai
api_key: YOUR_API_KEY
model_name: YOUR_MODEL_ID
```

### 3. 切换配置
```bash
$ python3 cc-simple.py use 1
Backed up existing config to /root/.codex/config.toml.bak
Switched to configuration: sbbbbbb (ID: 1)
Provider type: openai
Base URL: https://fast.sbbbbbbbbb.xyz/v1
`type` 为 `openai`(Codex)或 `anthropic`(Claude Code)。Codex 接口需支持 Responses。两种配置模板在 [conf_tmpl/](conf_tmpl/)。

## 使用

```sh
cc-simple add provider.yaml
cc-simple list
cc-simple use 1 --dry-run
cc-simple use 1
cc-simple check 1 --json
```

`use` 和 `check` 也可直接传 YAML 路径。切换前自动备份,保留已有无关配置。`check` 不修改客户端配置,会调用模型接口并消耗接口额度。

Windows 安装、环境变量和高级选项见 [使用说明](docs/USAGE.md)。
240 changes: 4 additions & 236 deletions cc-simple.py
Original file line number Diff line number Diff line change
@@ -1,238 +1,6 @@
#!/usr/bin/env python3
"""
cc-simple.py - Simple Codex configuration switcher
Manages provider configurations for Codex from YAML files
"""
"""Compatibility entry point; install this project for the cc-simple command."""
from cc_simple import main

import os
import sys
import argparse
from pathlib import Path
import shutil


class ConfigManager:
def __init__(self):
self.home = Path.home()
self.codex_config_dir = self.home / ".codex"
self.codex_config_file = self.codex_config_dir / "config.toml"
self.project_dir = Path(__file__).parent
self.confs_dir = self.project_dir / "confs"

# Ensure directories exist
self.confs_dir.mkdir(exist_ok=True)

def parse_yaml_config(self, yaml_file):
"""Parse simple key=value YAML format"""
config = {}
with open(yaml_file, 'r') as f:
for line in f:
line = line.strip()
if line and '=' in line:
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
return config

def add_config(self, yaml_file):
"""Add a configuration from YAML file"""
yaml_path = Path(yaml_file)
if not yaml_path.exists():
print(f"Error: {yaml_file} not found")
return False

config = self.parse_yaml_config(yaml_path)

# Validate required fields
required = ['name', 'base_url', 'type']
for field in required:
if field not in config:
print(f"Error: missing required field '{field}'")
return False

# Copy to confs directory with index
existing_confs = list(self.confs_dir.glob("*.yaml"))
index = len(existing_confs) + 1
dest_file = self.confs_dir / f"{index}_{config['name']}.yaml"

shutil.copy(yaml_path, dest_file)
print(f"Added configuration: {config['name']} (ID: {index})")
return True

def list_configs(self):
"""List all stored configurations"""
confs = sorted(self.confs_dir.glob("*.yaml"))

if not confs:
print("No configurations found")
return

for conf_file in confs:
# Extract index from filename (format: 1_name.yaml)
parts = conf_file.stem.split('_', 1)
if len(parts) == 2 and parts[0].isdigit():
index, name = parts
config = self.parse_yaml_config(conf_file)
base_url = config.get('base_url', 'N/A')
print(f"{index} {name} {base_url}")
# Skip files without proper ID_name format

def generate_toml_content(self, config):
"""Generate TOML content for the provider"""
provider_type = config.get('type', 'openai')

if provider_type == 'openai':
return self._generate_openai_toml(config)
elif provider_type == 'anthropic':
return self._generate_anthropic_toml(config)
else:
print(f"Warning: unknown type '{provider_type}', using OpenAI format")
return self._generate_openai_toml(config)

def _generate_openai_toml(self, config):
"""Generate OpenAI provider TOML"""
provider_name = config.get('name', 'unnamed')
api_key = config.get('api_key', config.get('model', 'sk-xxx'))
base_url = config.get('base_url', '')
model = config.get('model_name', 'gpt-4')

toml = f"""# Generated by cc-simple.py
# Provider: {provider_name}

model_provider = "{provider_name}"
model = "{model}"

[model_providers.{provider_name}]
name = "{provider_name}"
base_url = "{base_url}"
wire_api = "chat"

[model_providers.{provider_name}.auth]
header = "Authorization"
value = "Bearer {api_key}"

[tui]
screen_reader_detection_done = true
"""
return toml

def _generate_anthropic_toml(self, config):
"""Generate Anthropic provider TOML"""
provider_name = config.get('name', 'unnamed')
api_key = config.get('api_key', config.get('model', 'sk-ant-xxx'))
base_url = config.get('base_url', 'https://api.anthropic.com')
model = config.get('model_name', 'claude-opus-5-5')

toml = f"""# Generated by cc-simple.py
# Provider: {provider_name}

model_provider = "{provider_name}"
model = "{model}"

[model_providers.{provider_name}]
name = "{provider_name}"
base_url = "{base_url}"
wire_api = "responses"

[model_providers.{provider_name}.auth]
header = "x-api-key"
value = "{api_key}"

[tui]
screen_reader_detection_done = true
"""
return toml

def use_config(self, config_id):
"""Switch to a specific configuration"""
# Find config file by ID
target_file = None
for conf_file in self.confs_dir.glob("*.yaml"):
parts = conf_file.stem.split('_', 1)
if len(parts) == 2 and parts[0] == str(config_id):
target_file = conf_file
break

if not target_file:
print(f"Error: configuration ID {config_id} not found")
return False

# Parse config
config = self.parse_yaml_config(target_file)

# Generate TOML content
toml_content = self.generate_toml_content(config)

# Backup existing config if it exists
if self.codex_config_file.exists():
backup_file = self.codex_config_file.with_suffix('.toml.bak')
shutil.copy(self.codex_config_file, backup_file)
print(f"Backed up existing config to {backup_file}")

# Ensure codex config directory exists
self.codex_config_dir.mkdir(parents=True, exist_ok=True)

# Write new config
with open(self.codex_config_file, 'w') as f:
f.write(toml_content)

print(f"Switched to configuration: {config.get('name', 'unnamed')} (ID: {config_id})")
print(f"Provider type: {config.get('type', 'openai')}")
print(f"Base URL: {config.get('base_url', 'N/A')}")
return True


def main():
parser = argparse.ArgumentParser(
description='Simple Codex configuration switcher',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Add a configuration
cc-simple.py add conf.yaml

# List all configurations
cc-simple.py list

# Switch to configuration ID 1
cc-simple.py use 1
"""
)

subparsers = parser.add_subparsers(dest='command', help='Command to execute')

# Add command
add_parser = subparsers.add_parser('add', help='Add a configuration from YAML file')
add_parser.add_argument('yaml_file', help='Path to YAML configuration file')

# List command
subparsers.add_parser('list', help='List all configurations')

# Use command
use_parser = subparsers.add_parser('use', help='Switch to a configuration')
use_parser.add_argument('config_id', type=int, help='Configuration ID to use')

args = parser.parse_args()

if not args.command:
parser.print_help()
return 1

manager = ConfigManager()

if args.command == 'add':
success = manager.add_config(args.yaml_file)
return 0 if success else 1

elif args.command == 'list':
manager.list_configs()
return 0

elif args.command == 'use':
success = manager.use_config(args.config_id)
return 0 if success else 1

return 0


if __name__ == '__main__':
sys.exit(main())
if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading