diff --git a/.env.example b/.env.example index c426100..e29bec3 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,15 @@ # - false: Browser opens a visible window (useful for debugging) # Defaults to 'false' if not specified # PLAYWRIGHT_HEADLESS=false + +# GLM/Alternative API Configuration (Optional) +# To use Zhipu AI's GLM models instead of Claude, uncomment and set these variables. +# This only affects AutoCoder - your global Claude Code settings remain unchanged. +# Get an API key at: https://z.ai/subscribe +# +# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic +# ANTHROPIC_AUTH_TOKEN=your-zhipu-api-key +# API_TIMEOUT_MS=3000000 +# ANTHROPIC_DEFAULT_SONNET_MODEL=glm-4.7 +# ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7 +# ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air diff --git a/README.md b/README.md index 9af8172..3ed7f15 100644 --- a/README.md +++ b/README.md @@ -290,18 +290,18 @@ When test progress increases, the agent sends: ### Using GLM Models (Alternative to Claude) -To use Zhipu AI's GLM models instead of Claude, create a settings file at `~/.claude/settings.json`: +To use Zhipu AI's GLM models instead of Claude, add these variables to your `.env` file in the AutoCoder directory: -```json -{ - "env": { - "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic", - "ANTHROPIC_AUTH_TOKEN": "your-zhipu-api-key" - } -} +```bash +ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic +ANTHROPIC_AUTH_TOKEN=your-zhipu-api-key +API_TIMEOUT_MS=3000000 +ANTHROPIC_DEFAULT_SONNET_MODEL=glm-4.7 +ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7 +ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air ``` -This routes Claude Code requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models while keeping all Claude Code features (MCP servers, hooks, permissions). +This routes AutoCoder's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoCoder** - your global Claude Code settings remain unchanged. Get an API key at: https://z.ai/subscribe diff --git a/client.py b/client.py index f232a0a..fdf6d54 100644 --- a/client.py +++ b/client.py @@ -25,6 +25,18 @@ load_dotenv() # When False, browser window is visible (default - useful for monitoring agent progress) DEFAULT_PLAYWRIGHT_HEADLESS = False +# Environment variables to pass through to Claude CLI for API configuration +# These allow using alternative API endpoints (e.g., GLM via z.ai) without +# affecting the user's global Claude Code settings +API_ENV_VARS = [ + "ANTHROPIC_BASE_URL", # Custom API endpoint (e.g., https://api.z.ai/api/anthropic) + "ANTHROPIC_AUTH_TOKEN", # API authentication token + "API_TIMEOUT_MS", # Request timeout in milliseconds + "ANTHROPIC_DEFAULT_SONNET_MODEL", # Model override for Sonnet + "ANTHROPIC_DEFAULT_OPUS_MODEL", # Model override for Opus + "ANTHROPIC_DEFAULT_HAIKU_MODEL", # Model override for Haiku +] + def get_playwright_headless() -> bool: """ @@ -205,6 +217,21 @@ def create_client(project_dir: Path, model: str, yolo_mode: bool = False): "args": playwright_args, } + # Build environment overrides for API endpoint configuration + # These override system env vars for the Claude CLI subprocess, + # allowing AutoCoder to use alternative APIs (e.g., GLM) without + # affecting the user's global Claude Code settings + sdk_env = {} + for var in API_ENV_VARS: + value = os.getenv(var) + if value: + sdk_env[var] = value + + if sdk_env: + print(f" - API overrides: {', '.join(sdk_env.keys())}") + if "ANTHROPIC_BASE_URL" in sdk_env: + print(f" - GLM Mode: Using {sdk_env['ANTHROPIC_BASE_URL']}") + return ClaudeSDKClient( options=ClaudeAgentOptions( model=model, @@ -222,5 +249,6 @@ def create_client(project_dir: Path, model: str, yolo_mode: bool = False): max_turns=1000, cwd=str(project_dir.resolve()), settings=str(settings_file.resolve()), # Use absolute path + env=sdk_env, # Pass API configuration overrides to CLI subprocess ) ) diff --git a/server/main.py b/server/main.py index 8be2a50..9340315 100644 --- a/server/main.py +++ b/server/main.py @@ -6,6 +6,7 @@ Main entry point for the Autonomous Coding UI server. Provides REST API, WebSocket, and static file serving. """ +import os import shutil from contextlib import asynccontextmanager from pathlib import Path @@ -148,7 +149,11 @@ async def setup_status(): # Note: CLI no longer stores credentials in ~/.claude/.credentials.json # The existence of ~/.claude indicates the CLI has been configured claude_dir = Path.home() / ".claude" - credentials = claude_dir.exists() and claude_dir.is_dir() + has_claude_config = claude_dir.exists() and claude_dir.is_dir() + + # If GLM mode is configured via .env, we have alternative credentials + glm_configured = bool(os.getenv("ANTHROPIC_BASE_URL") and os.getenv("ANTHROPIC_AUTH_TOKEN")) + credentials = has_claude_config or glm_configured # Check for Node.js and npm node = shutil.which("node") is not None diff --git a/server/routers/settings.py b/server/routers/settings.py index 18362ee..78d6ff8 100644 --- a/server/routers/settings.py +++ b/server/routers/settings.py @@ -6,6 +6,7 @@ API endpoints for global settings management. Settings are stored in the registry database and shared across all projects. """ +import os import sys from pathlib import Path @@ -33,6 +34,11 @@ def _parse_yolo_mode(value: str | None) -> bool: return (value or "false").lower() == "true" +def _is_glm_mode() -> bool: + """Check if GLM API is configured via environment variables.""" + return bool(os.getenv("ANTHROPIC_BASE_URL")) + + @router.get("/models", response_model=ModelsResponse) async def get_available_models(): """Get list of available models. @@ -54,6 +60,7 @@ async def get_settings(): return SettingsResponse( yolo_mode=_parse_yolo_mode(all_settings.get("yolo_mode")), model=all_settings.get("model", DEFAULT_MODEL), + glm_mode=_is_glm_mode(), ) @@ -71,4 +78,5 @@ async def update_settings(update: SettingsUpdate): return SettingsResponse( yolo_mode=_parse_yolo_mode(all_settings.get("yolo_mode")), model=all_settings.get("model", DEFAULT_MODEL), + glm_mode=_is_glm_mode(), ) diff --git a/server/schemas.py b/server/schemas.py index 72d6bf4..e9b9c31 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -289,6 +289,7 @@ class SettingsResponse(BaseModel): """Response schema for global settings.""" yolo_mode: bool = False model: str = DEFAULT_MODEL + glm_mode: bool = False # True if GLM API is configured via .env class ModelsResponse(BaseModel): diff --git a/server/services/assistant_chat_session.py b/server/services/assistant_chat_session.py index 9e067f1..cf18241 100755 --- a/server/services/assistant_chat_session.py +++ b/server/services/assistant_chat_session.py @@ -33,6 +33,16 @@ logger = logging.getLogger(__name__) # Root directory of the project ROOT_DIR = Path(__file__).parent.parent.parent +# Environment variables to pass through to Claude CLI for API configuration +API_ENV_VARS = [ + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "API_TIMEOUT_MS", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", +] + # Read-only feature MCP tools READONLY_FEATURE_MCP_TOOLS = [ "mcp__features__feature_get_stats", @@ -234,6 +244,9 @@ class AssistantChatSession: # Use system Claude CLI system_cli = shutil.which("claude") + # Build environment overrides for API configuration + sdk_env = {var: os.getenv(var) for var in API_ENV_VARS if os.getenv(var)} + try: self.client = ClaudeSDKClient( options=ClaudeAgentOptions( @@ -246,6 +259,7 @@ class AssistantChatSession: max_turns=100, cwd=str(self.project_dir.resolve()), settings=str(settings_file.resolve()), + env=sdk_env, ) ) await self.client.__aenter__() diff --git a/server/services/expand_chat_session.py b/server/services/expand_chat_session.py index b187804..71e56bb 100644 --- a/server/services/expand_chat_session.py +++ b/server/services/expand_chat_session.py @@ -9,6 +9,7 @@ Uses the expand-project.md skill to help users add features to existing projects import asyncio import json import logging +import os import re import shutil import threading @@ -27,6 +28,16 @@ load_dotenv() logger = logging.getLogger(__name__) +# Environment variables to pass through to Claude CLI for API configuration +API_ENV_VARS = [ + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "API_TIMEOUT_MS", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", +] + async def _make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]: """ @@ -153,6 +164,9 @@ class ExpandChatSession: project_path = str(self.project_dir.resolve()) system_prompt = skill_content.replace("$ARGUMENTS", project_path) + # Build environment overrides for API configuration + sdk_env = {var: os.getenv(var) for var in API_ENV_VARS if os.getenv(var)} + # Create Claude SDK client try: self.client = ClaudeSDKClient( @@ -168,6 +182,7 @@ class ExpandChatSession: max_turns=100, cwd=str(self.project_dir.resolve()), settings=str(settings_file.resolve()), + env=sdk_env, ) ) await self.client.__aenter__() diff --git a/server/services/spec_chat_session.py b/server/services/spec_chat_session.py index b3b4e1c..e073a4e 100644 --- a/server/services/spec_chat_session.py +++ b/server/services/spec_chat_session.py @@ -8,6 +8,7 @@ Uses the create-spec.md skill to guide users through app spec creation. import json import logging +import os import shutil import threading from datetime import datetime @@ -24,6 +25,16 @@ load_dotenv() logger = logging.getLogger(__name__) +# Environment variables to pass through to Claude CLI for API configuration +API_ENV_VARS = [ + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "API_TIMEOUT_MS", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", +] + async def _make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]: """ @@ -147,6 +158,10 @@ class SpecChatSession: # Use Opus for best quality spec generation # Use system Claude CLI to avoid bundled Bun runtime crash (exit code 3) on Windows system_cli = shutil.which("claude") + + # Build environment overrides for API configuration + sdk_env = {var: os.getenv(var) for var in API_ENV_VARS if os.getenv(var)} + try: self.client = ClaudeSDKClient( options=ClaudeAgentOptions( @@ -163,6 +178,7 @@ class SpecChatSession: max_turns=100, cwd=str(self.project_dir.resolve()), settings=str(settings_file.resolve()), + env=sdk_env, ) ) # Enter the async context and track it diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 4a33b9e..50b0297 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' import { useQueryClient } from '@tanstack/react-query' -import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects' +import { useProjects, useFeatures, useAgentStatus, useSettings } from './hooks/useProjects' import { useProjectWebSocket } from './hooks/useWebSocket' import { useFeatureSound } from './hooks/useFeatureSound' import { useCelebration } from './hooks/useCelebration' @@ -46,6 +46,7 @@ function App() { const queryClient = useQueryClient() const { data: projects, isLoading: projectsLoading } = useProjects() const { data: features } = useFeatures(selectedProject) + const { data: settings } = useSettings() useAgentStatus(selectedProject) // Keep polling for status updates const wsState = useProjectWebSocket(selectedProject) @@ -210,6 +211,16 @@ function App() { > + + {/* GLM Mode Badge */} + {settings?.glm_mode && ( + + GLM + + )} )} diff --git a/ui/src/hooks/useProjects.ts b/ui/src/hooks/useProjects.ts index 6a1098f..d6081a7 100644 --- a/ui/src/hooks/useProjects.ts +++ b/ui/src/hooks/useProjects.ts @@ -217,6 +217,7 @@ const DEFAULT_MODELS: ModelsResponse = { const DEFAULT_SETTINGS: Settings = { yolo_mode: false, model: 'claude-opus-4-5-20251101', + glm_mode: false, } export function useAvailableModels() { diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index cceb704..0851617 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -387,6 +387,7 @@ export interface ModelsResponse { export interface Settings { yolo_mode: boolean model: string + glm_mode: boolean } export interface SettingsUpdate {