From 754cddd4ad61788eae147ed74ce33cdec9ef118c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 30 Dec 2025 15:23:44 +0800 Subject: [PATCH] fix(python): improve Python detection and pip command reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared python-utils.ts module for consistent Python detection - Use `python -m pip` instead of direct pip command (fixes "pip not found") - Support CCW_PYTHON env var for custom Python path - Use Windows py launcher to find compatible versions (3.9-3.12) - Warn users when Python version may not be compatible with onnxruntime Fixes issues where users couldn't install ccw-litellm due to: - pip not in PATH - Only pip3 available (not pip) - Python 3.13+ without onnxruntime support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- ccw/src/core/routes/litellm-api-routes.ts | 13 ++- ccw/src/tools/codex-lens.ts | 21 +--- ccw/src/utils/python-utils.ts | 121 ++++++++++++++++++++++ 3 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 ccw/src/utils/python-utils.ts diff --git a/ccw/src/core/routes/litellm-api-routes.ts b/ccw/src/core/routes/litellm-api-routes.ts index 3343e21b..98042767 100644 --- a/ccw/src/core/routes/litellm-api-routes.ts +++ b/ccw/src/core/routes/litellm-api-routes.ts @@ -6,6 +6,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { fileURLToPath } from 'url'; import { dirname, join as pathJoin } from 'path'; +import { getSystemPython } from '../../utils/python-utils.js'; // Get current module path for package-relative lookups const __filename = fileURLToPath(import.meta.url); @@ -834,10 +835,13 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise { - const proc = spawn('pip', ['install', 'ccw-litellm'], { shell: true, timeout: 300000 }); + const proc = spawn(pythonCmd, ['-m', 'pip', 'install', 'ccw-litellm'], { shell: true, timeout: 300000 }); let output = ''; let error = ''; proc.stdout?.on('data', (data) => { output += data.toString(); }); @@ -857,7 +861,7 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise { - const proc = spawn('pip', ['install', '-e', packagePath], { shell: true, timeout: 300000 }); + const proc = spawn(pythonCmd, ['-m', 'pip', 'install', '-e', packagePath], { shell: true, timeout: 300000 }); let output = ''; let error = ''; proc.stdout?.on('data', (data) => { output += data.toString(); }); @@ -892,8 +896,11 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise { - const proc = spawn('pip', ['uninstall', '-y', 'ccw-litellm'], { shell: true, timeout: 120000 }); + const proc = spawn(pythonCmd, ['-m', 'pip', 'uninstall', '-y', 'ccw-litellm'], { shell: true, timeout: 120000 }); let output = ''; let error = ''; proc.stdout?.on('data', (data) => { output += data.toString(); }); diff --git a/ccw/src/tools/codex-lens.ts b/ccw/src/tools/codex-lens.ts index 19302542..9c984817 100644 --- a/ccw/src/tools/codex-lens.ts +++ b/ccw/src/tools/codex-lens.ts @@ -16,6 +16,7 @@ import { existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; +import { getSystemPython } from '../utils/python-utils.js'; // Get directory of this module const __filename = fileURLToPath(import.meta.url); @@ -131,25 +132,7 @@ function clearVenvStatusCache(): void { venvStatusCache = null; } -/** - * Detect available Python 3 executable - * @returns Python executable command - */ -function getSystemPython(): string { - const commands = process.platform === 'win32' ? ['python', 'py', 'python3'] : ['python3', 'python']; - - for (const cmd of commands) { - try { - const version = execSync(`${cmd} --version 2>&1`, { encoding: 'utf8' }); - if (version.includes('Python 3')) { - return cmd; - } - } catch { - // Try next command - } - } - throw new Error('Python 3 not found. Please install Python 3 and ensure it is in PATH.'); -} +// Python detection functions imported from ../utils/python-utils.js /** * Check if CodexLens venv exists and has required packages diff --git a/ccw/src/utils/python-utils.ts b/ccw/src/utils/python-utils.ts new file mode 100644 index 00000000..73ec0732 --- /dev/null +++ b/ccw/src/utils/python-utils.ts @@ -0,0 +1,121 @@ +/** + * Python detection and version compatibility utilities + * Shared module for consistent Python discovery across the application + */ + +import { execSync } from 'child_process'; + +/** + * Parse Python version string to major.minor numbers + * @param versionStr - Version string like "Python 3.11.5" + * @returns Object with major and minor version numbers, or null if parsing fails + */ +export function parsePythonVersion(versionStr: string): { major: number; minor: number } | null { + const match = versionStr.match(/Python\s+(\d+)\.(\d+)/); + if (match) { + return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) }; + } + return null; +} + +/** + * Check if Python version is compatible with onnxruntime (3.9-3.12) + * @param major - Major version number + * @param minor - Minor version number + * @returns true if compatible + */ +export function isPythonVersionCompatible(major: number, minor: number): boolean { + // onnxruntime currently supports Python 3.9-3.12 + return major === 3 && minor >= 9 && minor <= 12; +} + +/** + * Detect available Python 3 executable + * Supports CCW_PYTHON environment variable for custom Python path + * On Windows, uses py launcher to find compatible versions + * @returns Python executable command + */ +export function getSystemPython(): string { + // Check for user-specified Python via environment variable + const customPython = process.env.CCW_PYTHON; + if (customPython) { + try { + const version = execSync(`"${customPython}" --version 2>&1`, { encoding: 'utf8' }); + if (version.includes('Python 3')) { + const parsed = parsePythonVersion(version); + if (parsed && !isPythonVersionCompatible(parsed.major, parsed.minor)) { + console.warn(`[Python] Warning: CCW_PYTHON points to Python ${parsed.major}.${parsed.minor}, which may not be compatible with onnxruntime (requires 3.9-3.12)`); + } + return `"${customPython}"`; + } + } catch { + console.warn(`[Python] Warning: CCW_PYTHON="${customPython}" is not a valid Python executable, falling back to system Python`); + } + } + + // On Windows, try py launcher with specific versions first (3.12, 3.11, 3.10, 3.9) + if (process.platform === 'win32') { + const compatibleVersions = ['3.12', '3.11', '3.10', '3.9']; + for (const ver of compatibleVersions) { + try { + const version = execSync(`py -${ver} --version 2>&1`, { encoding: 'utf8' }); + if (version.includes(`Python ${ver}`)) { + console.log(`[Python] Found compatible Python ${ver} via py launcher`); + return `py -${ver}`; + } + } catch { + // Version not installed, try next + } + } + } + + const commands = process.platform === 'win32' ? ['python', 'py', 'python3'] : ['python3', 'python']; + let fallbackCmd: string | null = null; + let fallbackVersion: { major: number; minor: number } | null = null; + + for (const cmd of commands) { + try { + const version = execSync(`${cmd} --version 2>&1`, { encoding: 'utf8' }); + if (version.includes('Python 3')) { + const parsed = parsePythonVersion(version); + if (parsed) { + // Prefer compatible version (3.9-3.12) + if (isPythonVersionCompatible(parsed.major, parsed.minor)) { + return cmd; + } + // Keep track of first Python 3 found as fallback + if (!fallbackCmd) { + fallbackCmd = cmd; + fallbackVersion = parsed; + } + } + } + } catch { + // Try next command + } + } + + // If no compatible version found, use fallback with warning + if (fallbackCmd && fallbackVersion) { + console.warn(`[Python] Warning: Only Python ${fallbackVersion.major}.${fallbackVersion.minor} found, which may not be compatible with onnxruntime (requires 3.9-3.12).`); + console.warn('[Python] To use a specific Python version, set CCW_PYTHON environment variable:'); + console.warn(' Windows: set CCW_PYTHON=C:\\path\\to\\python.exe'); + console.warn(' Unix: export CCW_PYTHON=/path/to/python3.11'); + console.warn('[Python] Alternatively, use LiteLLM embedding backend which has no Python version restrictions.'); + return fallbackCmd; + } + + throw new Error('Python 3 not found. Please install Python 3.9-3.12 and ensure it is in PATH, or set CCW_PYTHON environment variable.'); +} + +/** + * Get the Python command for pip operations (uses -m pip for reliability) + * @returns Array of command arguments for spawn + */ +export function getPipCommand(): { pythonCmd: string; pipArgs: string[] } { + const pythonCmd = getSystemPython(); + return { + pythonCmd, + pipArgs: ['-m', 'pip'] + }; +}