From f31ea403ea4c31b96fba3ebbb7e7ddd7a7d16043 Mon Sep 17 00:00:00 2001 From: Auto Date: Mon, 12 Jan 2026 12:25:13 +0200 Subject: [PATCH] feat: add GLM/alternative API support via environment variables Add support for using alternative API endpoints (like Zhipu AI's GLM models) without affecting the user's global Claude Code settings. Configuration is done via AutoCoder's .env file. Changes: - Add API_ENV_VARS constant and pass through ClaudeAgentOptions.env parameter in client.py and all server service files (spec, expand, assistant sessions) - Add glm_mode to settings API response to indicate when GLM is configured - Add purple "GLM" badge in UI header when GLM mode is active - Update setup status to accept GLM credentials as valid authentication - Update .env.example with GLM configuration documentation - Update README.md with AutoCoder-scoped GLM setup instructions Supported environment variables: - 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 This approach routes API requests through the alternative endpoint while keeping all Claude Code features (MCP servers, hooks, permissions) intact. Co-Authored-By: Claude Opus 4.5 --- .env.example | 12 ++++++++++ README.md | 18 +++++++-------- client.py | 28 +++++++++++++++++++++++ server/main.py | 7 +++++- server/routers/settings.py | 8 +++++++ server/schemas.py | 1 + server/services/assistant_chat_session.py | 14 ++++++++++++ server/services/expand_chat_session.py | 15 ++++++++++++ server/services/spec_chat_session.py | 16 +++++++++++++ ui/src/App.tsx | 13 ++++++++++- ui/src/hooks/useProjects.ts | 1 + ui/src/lib/types.ts | 1 + 12 files changed, 123 insertions(+), 11 deletions(-) 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 {