diff --git a/.env.example b/.env.example index c986d7b..8458726 100644 --- a/.env.example +++ b/.env.example @@ -32,5 +32,25 @@ # =================== # Alternative API Providers (GLM, Ollama, Kimi, Custom) # =================== -# Configure alternative providers via the Settings UI (gear icon > API Provider). -# The Settings UI is the recommended way to switch providers and models. +# Configure via Settings UI (recommended) or set env vars below. +# When both are set, env vars take precedence. +# +# GLM (Zhipu AI): +# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic +# ANTHROPIC_AUTH_TOKEN=your-glm-api-key +# ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7 +# ANTHROPIC_DEFAULT_SONNET_MODEL=glm-4.7 +# ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.7 +# +# Ollama (Local): +# ANTHROPIC_BASE_URL=http://localhost:11434 +# ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder +# ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder +# ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder +# +# Kimi (Moonshot): +# ANTHROPIC_BASE_URL=https://api.kimi.com/coding/ +# ANTHROPIC_API_KEY=your-kimi-api-key +# ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.5 +# ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.5 +# ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.5 diff --git a/autonomous_agent_demo.py b/autonomous_agent_demo.py index 4880c49..918b2c1 100644 --- a/autonomous_agent_demo.py +++ b/autonomous_agent_demo.py @@ -44,8 +44,10 @@ from dotenv import load_dotenv # IMPORTANT: Must be called BEFORE importing other modules that read env vars at load time load_dotenv() +import os + from agent import run_autonomous_agent -from registry import DEFAULT_MODEL, get_project_path +from registry import DEFAULT_MODEL, get_effective_sdk_env, get_project_path def parse_args() -> argparse.Namespace: @@ -195,6 +197,14 @@ def main() -> None: # Note: Authentication is handled by start.bat/start.sh before this script runs. # The Claude SDK auto-detects credentials from ~/.claude/.credentials.json + # Apply UI-configured provider settings to this process's environment. + # This ensures CLI-launched agents respect Settings UI provider config (GLM, Ollama, etc.). + # Uses setdefault so explicit env vars / .env file take precedence. + sdk_overrides = get_effective_sdk_env() + for key, value in sdk_overrides.items(): + if value: # Only set non-empty values (empty values are used to clear conflicts) + os.environ.setdefault(key, value) + # Handle deprecated --parallel flag if args.parallel is not None: print("WARNING: --parallel is deprecated. Use --concurrency instead.", flush=True) diff --git a/client.py b/client.py index 2962f0b..a81a66d 100644 --- a/client.py +++ b/client.py @@ -16,7 +16,6 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient from claude_agent_sdk.types import HookContext, HookInput, HookMatcher, SyncHookJSONOutput from dotenv import load_dotenv -from env_constants import API_ENV_VARS from security import SENSITIVE_DIRECTORIES, bash_security_hook # Load environment variables from .env file if present @@ -450,14 +449,11 @@ def create_client( } # Build environment overrides for API endpoint configuration - # These override system env vars for the Claude CLI subprocess, - # allowing AutoForge 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 + # Uses get_effective_sdk_env() which reads provider settings from the database, + # ensuring UI-configured alternative providers (GLM, Ollama, Kimi, Custom) propagate + # correctly to the Claude CLI subprocess + from registry import get_effective_sdk_env + sdk_env = get_effective_sdk_env() # Detect alternative API mode (Ollama, GLM, or Vertex AI) base_url = sdk_env.get("ANTHROPIC_BASE_URL", "") diff --git a/registry.py b/registry.py index 159d2fd..3076519 100644 --- a/registry.py +++ b/registry.py @@ -731,7 +731,22 @@ def get_effective_sdk_env() -> dict[str, str]: sdk_env[var] = value return sdk_env - sdk_env = {} + sdk_env: dict[str, str] = {} + + # Explicitly clear credentials that could leak from the server process env. + # For providers using ANTHROPIC_AUTH_TOKEN (GLM, Custom), clear ANTHROPIC_API_KEY. + # For providers using ANTHROPIC_API_KEY (Kimi), clear ANTHROPIC_AUTH_TOKEN. + # This prevents the Claude CLI from using the wrong credentials. + auth_env_var = provider.get("auth_env_var", "ANTHROPIC_AUTH_TOKEN") + if auth_env_var == "ANTHROPIC_AUTH_TOKEN": + sdk_env["ANTHROPIC_API_KEY"] = "" + elif auth_env_var == "ANTHROPIC_API_KEY": + sdk_env["ANTHROPIC_AUTH_TOKEN"] = "" + + # Clear Vertex AI vars when using non-Vertex alternative providers + sdk_env["CLAUDE_CODE_USE_VERTEX"] = "" + sdk_env["CLOUD_ML_REGION"] = "" + sdk_env["ANTHROPIC_VERTEX_PROJECT_ID"] = "" # Base URL base_url = all_settings.get("api_base_url") or provider.get("base_url") @@ -741,7 +756,6 @@ def get_effective_sdk_env() -> dict[str, str]: # Auth token auth_token = all_settings.get("api_auth_token") if auth_token: - auth_env_var = provider.get("auth_env_var", "ANTHROPIC_AUTH_TOKEN") sdk_env[auth_env_var] = auth_token # Model - set all three tier overrides to the same model diff --git a/server/schemas.py b/server/schemas.py index d94d4a5..5f546e2 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -190,9 +190,12 @@ class AgentStartRequest(BaseModel): @field_validator('model') @classmethod def validate_model(cls, v: str | None) -> str | None: - """Validate model is in the allowed list.""" + """Validate model is in the allowed list (Claude) or allow any model for alternative providers.""" if v is not None and v not in VALID_MODELS: - raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}") + from registry import get_all_settings + settings = get_all_settings() + if settings.get("api_provider", "claude") == "claude": + raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}") return v @field_validator('max_concurrency') @@ -571,9 +574,12 @@ class ScheduleCreate(BaseModel): @field_validator('model') @classmethod def validate_model(cls, v: str | None) -> str | None: - """Validate model is in the allowed list.""" + """Validate model is in the allowed list (Claude) or allow any model for alternative providers.""" if v is not None and v not in VALID_MODELS: - raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}") + from registry import get_all_settings + settings = get_all_settings() + if settings.get("api_provider", "claude") == "claude": + raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}") return v @@ -593,9 +599,12 @@ class ScheduleUpdate(BaseModel): @field_validator('model') @classmethod def validate_model(cls, v: str | None) -> str | None: - """Validate model is in the allowed list.""" + """Validate model is in the allowed list (Claude) or allow any model for alternative providers.""" if v is not None and v not in VALID_MODELS: - raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}") + from registry import get_all_settings + settings = get_all_settings() + if settings.get("api_provider", "claude") == "claude": + raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}") return v diff --git a/start.py b/start.py index 0e303ef..a47da39 100644 --- a/start.py +++ b/start.py @@ -390,8 +390,11 @@ def run_agent(project_name: str, project_dir: Path) -> None: print(f"Location: {project_dir}") print("-" * 50) - # Build the command - pass absolute path - cmd = [sys.executable, "autonomous_agent_demo.py", "--project-dir", str(project_dir.resolve())] + # Build the command - pass absolute path and model from settings + from registry import DEFAULT_MODEL, get_all_settings + settings = get_all_settings() + model = settings.get("api_model") or settings.get("model", DEFAULT_MODEL) + cmd = [sys.executable, "autonomous_agent_demo.py", "--project-dir", str(project_dir.resolve()), "--model", model] # Run the agent with stderr capture to detect auth errors # stdout goes directly to terminal for real-time output