diff --git a/core/framework/config.py b/core/framework/config.py index e3c8d682..275bd2d9 100644 --- a/core/framework/config.py +++ b/core/framework/config.py @@ -90,6 +90,17 @@ def get_api_key() -> str | None: except ImportError: pass + # Kimi Code subscription: read API key from ~/.kimi/config.toml + if llm.get("use_kimi_code_subscription"): + try: + from framework.runner.runner import get_kimi_code_token + + token = get_kimi_code_token() + if token: + return token + except ImportError: + pass + # Standard env-var path (covers ZAI Code and all API-key providers) api_key_env_var = llm.get("api_key_env_var") if api_key_env_var: @@ -108,6 +119,9 @@ def get_api_base() -> str | None: if llm.get("use_codex_subscription"): # Codex subscription routes through the ChatGPT backend, not api.openai.com. return "https://chatgpt.com/backend-api/codex" + if llm.get("use_kimi_code_subscription"): + # Kimi Code uses an Anthropic-compatible endpoint (no /v1 suffix). + return "https://api.kimi.com/coding" return llm.get("api_base") diff --git a/core/framework/llm/litellm.py b/core/framework/llm/litellm.py index 893618dc..2037bd58 100644 --- a/core/framework/llm/litellm.py +++ b/core/framework/llm/litellm.py @@ -118,6 +118,10 @@ RATE_LIMIT_MAX_RETRIES = 10 RATE_LIMIT_BACKOFF_BASE = 2 # seconds RATE_LIMIT_MAX_DELAY = 120 # seconds - cap to prevent absurd waits MINIMAX_API_BASE = "https://api.minimax.io/v1" +# Kimi For Coding uses an Anthropic-compatible endpoint (no /v1 suffix). +# Claude Code integration uses this format; the /v1 OpenAI-compatible endpoint +# enforces a coding-agent whitelist that blocks unknown User-Agents. +KIMI_API_BASE = "https://api.kimi.com/coding" # Empty-stream retries use a short fixed delay, not the rate-limit backoff. # Conversation-structure issues are deterministic — long waits don't help. @@ -323,9 +327,21 @@ class LiteLLMProvider(LLMProvider): api_base: Custom API base URL (for proxies or local deployments) **kwargs: Additional arguments passed to litellm.completion() """ + # Kimi For Coding exposes an Anthropic-compatible endpoint at + # https://api.kimi.com/coding (the same format Claude Code uses natively). + # Translate kimi/ prefix to anthropic/ so litellm uses the Anthropic + # Messages API handler and routes to that endpoint — no special headers needed. + _original_model = model + if model.lower().startswith("kimi/"): + model = "anthropic/" + model[len("kimi/"):] + # Normalise api_base: litellm's Anthropic handler appends /v1/messages, + # so the base must be https://api.kimi.com/coding (no /v1 suffix). + # Strip a trailing /v1 in case the user's saved config has the old value. + if api_base and api_base.rstrip("/").endswith("/v1"): + api_base = api_base.rstrip("/")[:-3] self.model = model self.api_key = api_key - self.api_base = api_base or self._default_api_base_for_model(model) + self.api_base = api_base or self._default_api_base_for_model(_original_model) self.extra_kwargs = kwargs # The Codex ChatGPT backend (chatgpt.com/backend-api/codex) rejects # several standard OpenAI params: max_output_tokens, stream_options. @@ -350,6 +366,8 @@ class LiteLLMProvider(LLMProvider): model_lower = model.lower() if model_lower.startswith("minimax/") or model_lower.startswith("minimax-"): return MINIMAX_API_BASE + if model_lower.startswith("kimi/"): + return KIMI_API_BASE return None def _completion_with_rate_limit_retry( diff --git a/core/framework/runner/runner.py b/core/framework/runner/runner.py index cf101a13..afa2578e 100644 --- a/core/framework/runner/runner.py +++ b/core/framework/runner/runner.py @@ -517,6 +517,41 @@ def get_codex_account_id() -> str | None: return None +# --------------------------------------------------------------------------- +# Kimi Code subscription token helpers +# --------------------------------------------------------------------------- + + +def get_kimi_code_token() -> str | None: + """Get the API key from a Kimi Code CLI installation. + + Reads the API key from ``~/.kimi/config.toml``, which is created when + the user runs ``kimi /login`` in the Kimi Code CLI. + + Returns: + The API key if available, None otherwise. + """ + import tomllib + + config_path = Path.home() / ".kimi" / "config.toml" + if not config_path.exists(): + return None + + try: + with open(config_path, "rb") as f: + config = tomllib.load(f) + providers = config.get("providers", {}) + # kimi-cli stores credentials under providers.kimi-for-coding + for provider_cfg in providers.values(): + if isinstance(provider_cfg, dict): + key = provider_cfg.get("api_key") + if key: + return key + except Exception: + pass + return None + + @dataclass class AgentInfo: """Information about an exported agent.""" @@ -1104,6 +1139,7 @@ class AgentRunner: llm_config = config.get("llm", {}) use_claude_code = llm_config.get("use_claude_code_subscription", False) use_codex = llm_config.get("use_codex_subscription", False) + use_kimi_code = llm_config.get("use_kimi_code_subscription", False) api_base = llm_config.get("api_base") api_key = None @@ -1119,6 +1155,12 @@ class AgentRunner: if not api_key: print("Warning: Codex subscription configured but no token found.") print("Run 'codex' to authenticate, then try again.") + elif use_kimi_code: + # Get API key from Kimi Code CLI config (~/.kimi/config.toml) + api_key = get_kimi_code_token() + if not api_key: + print("Warning: Kimi Code subscription configured but no key found.") + print("Run 'kimi /login' to authenticate, then try again.") if api_key and use_claude_code: # Use litellm's built-in Anthropic OAuth support. @@ -1149,6 +1191,14 @@ class AgentRunner: store=False, allowed_openai_params=["store"], ) + elif api_key and use_kimi_code: + # Kimi Code subscription uses the Kimi coding API (OpenAI-compatible). + # The api_base is set automatically by LiteLLMProvider for kimi/ models. + self._llm = LiteLLMProvider( + model=self.model, + api_key=api_key, + api_base=api_base, + ) else: # Local models (e.g. Ollama) don't need an API key if self._is_local_model(self.model): @@ -1314,6 +1364,8 @@ class AgentRunner: return "TOGETHER_API_KEY" elif model_lower.startswith("minimax/") or model_lower.startswith("minimax-"): return "MINIMAX_API_KEY" + elif model_lower.startswith("kimi/"): + return "KIMI_API_KEY" else: # Default: assume OpenAI-compatible return "OPENAI_API_KEY" @@ -1334,6 +1386,8 @@ class AgentRunner: cred_id = "anthropic" elif model_lower.startswith("minimax/") or model_lower.startswith("minimax-"): cred_id = "minimax" + elif model_lower.startswith("kimi/"): + cred_id = "kimi" # Add more mappings as providers are added to LLM_CREDENTIALS if cred_id is None: diff --git a/quickstart.ps1 b/quickstart.ps1 index b60e11be..96c25851 100644 --- a/quickstart.ps1 +++ b/quickstart.ps1 @@ -911,6 +911,13 @@ $zaiKey = [System.Environment]::GetEnvironmentVariable("ZAI_API_KEY", "User") if (-not $zaiKey) { $zaiKey = $env:ZAI_API_KEY } if ($zaiKey) { $ZaiCredDetected = $true } +$KimiCredDetected = $false +$kimiConfigPath = Join-Path $env:USERPROFILE ".kimi\config.toml" +if (Test-Path $kimiConfigPath) { $KimiCredDetected = $true } +$kimiKey = [System.Environment]::GetEnvironmentVariable("KIMI_API_KEY", "User") +if (-not $kimiKey) { $kimiKey = $env:KIMI_API_KEY } +if ($kimiKey) { $KimiCredDetected = $true } + # Detect API key providers $ProviderMenuEnvVars = @("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY", "CEREBRAS_API_KEY") $ProviderMenuNames = @("Anthropic (Claude) - Recommended", "OpenAI (GPT)", "Google Gemini - Free tier available", "Groq - Fast, free tier", "Cerebras - Fast, free tier") @@ -938,7 +945,9 @@ if (Test-Path $HiveConfigFile) { $PrevEnvVar = if ($prevLlm.api_key_env_var) { $prevLlm.api_key_env_var } else { "" } if ($prevLlm.use_claude_code_subscription) { $PrevSubMode = "claude_code" } elseif ($prevLlm.use_codex_subscription) { $PrevSubMode = "codex" } + elseif ($prevLlm.use_kimi_code_subscription) { $PrevSubMode = "kimi_code" } elseif ($prevLlm.api_base -and $prevLlm.api_base -like "*api.z.ai*") { $PrevSubMode = "zai_code" } + elseif ($prevLlm.api_base -and $prevLlm.api_base -like "*api.kimi.com*") { $PrevSubMode = "kimi_code" } } } catch { } } @@ -951,6 +960,7 @@ if ($PrevSubMode -or $PrevProvider) { "claude_code" { if ($ClaudeCredDetected) { $prevCredValid = $true } } "zai_code" { if ($ZaiCredDetected) { $prevCredValid = $true } } "codex" { if ($CodexCredDetected) { $prevCredValid = $true } } + "kimi_code" { if ($KimiCredDetected) { $prevCredValid = $true } } default { if ($PrevEnvVar) { $envVal = [System.Environment]::GetEnvironmentVariable($PrevEnvVar, "Process") @@ -964,14 +974,16 @@ if ($PrevSubMode -or $PrevProvider) { "claude_code" { $DefaultChoice = "1" } "zai_code" { $DefaultChoice = "2" } "codex" { $DefaultChoice = "3" } + "kimi_code" { $DefaultChoice = "4" } } if (-not $DefaultChoice) { switch ($PrevProvider) { - "anthropic" { $DefaultChoice = "4" } - "openai" { $DefaultChoice = "5" } - "gemini" { $DefaultChoice = "6" } - "groq" { $DefaultChoice = "7" } - "cerebras" { $DefaultChoice = "8" } + "anthropic" { $DefaultChoice = "5" } + "openai" { $DefaultChoice = "6" } + "gemini" { $DefaultChoice = "7" } + "groq" { $DefaultChoice = "8" } + "cerebras" { $DefaultChoice = "9" } + "kimi" { $DefaultChoice = "4" } } } } @@ -1003,12 +1015,19 @@ Write-Host ") OpenAI Codex Subscription " -NoNewline Write-Color -Text "(use your Codex/ChatGPT Plus plan)" -Color DarkGray -NoNewline if ($CodexCredDetected) { Write-Color -Text " (credential detected)" -Color Green } else { Write-Host "" } +# 4) Kimi Code +Write-Host " " -NoNewline +Write-Color -Text "4" -Color Cyan -NoNewline +Write-Host ") Kimi Code Subscription " -NoNewline +Write-Color -Text "(use your Kimi Code plan)" -Color DarkGray -NoNewline +if ($KimiCredDetected) { Write-Color -Text " (credential detected)" -Color Green } else { Write-Host "" } + Write-Host "" Write-Color -Text " API key providers:" -Color Cyan -# 4-8) API key providers +# 5-9) API key providers for ($idx = 0; $idx -lt $ProviderMenuEnvVars.Count; $idx++) { - $num = $idx + 4 + $num = $idx + 5 $envVal = [System.Environment]::GetEnvironmentVariable($ProviderMenuEnvVars[$idx], "Process") if (-not $envVal) { $envVal = [System.Environment]::GetEnvironmentVariable($ProviderMenuEnvVars[$idx], "User") } Write-Host " " -NoNewline @@ -1018,7 +1037,7 @@ for ($idx = 0; $idx -lt $ProviderMenuEnvVars.Count; $idx++) { } Write-Host " " -NoNewline -Write-Color -Text "9" -Color Cyan -NoNewline +Write-Color -Text "10" -Color Cyan -NoNewline Write-Host ") Skip for now" Write-Host "" @@ -1029,16 +1048,16 @@ if ($DefaultChoice) { while ($true) { if ($DefaultChoice) { - $raw = Read-Host "Enter choice (1-9) [$DefaultChoice]" + $raw = Read-Host "Enter choice (1-10) [$DefaultChoice]" if ([string]::IsNullOrWhiteSpace($raw)) { $raw = $DefaultChoice } } else { - $raw = Read-Host "Enter choice (1-9)" + $raw = Read-Host "Enter choice (1-10)" } if ($raw -match '^\d+$') { $num = [int]$raw - if ($num -ge 1 -and $num -le 9) { break } + if ($num -ge 1 -and $num -le 10) { break } } - Write-Color -Text "Invalid choice. Please enter 1-9" -Color Red + Write-Color -Text "Invalid choice. Please enter 1-10" -Color Red } switch ($num) { @@ -1102,9 +1121,20 @@ switch ($num) { Write-Ok "Using OpenAI Codex subscription" } } - { $_ -ge 4 -and $_ -le 8 } { + 4 { + # Kimi Code Subscription + $SubscriptionMode = "kimi_code" + $SelectedProviderId = "kimi" + $SelectedEnvVar = "KIMI_API_KEY" + $SelectedModel = "kimi-k2.5" + $SelectedMaxTokens = 32768 + Write-Host "" + Write-Ok "Using Kimi Code subscription" + Write-Color -Text " Model: kimi-k2.5 | API: api.kimi.com/coding" -Color DarkGray + } + { $_ -ge 5 -and $_ -le 9 } { # API key providers - $provIdx = $num - 4 + $provIdx = $num - 5 $SelectedEnvVar = $ProviderMenuEnvVars[$provIdx] $SelectedProviderId = $ProviderMenuIds[$provIdx] $providerName = $ProviderMenuNames[$provIdx] -replace ' - .*', '' # strip description @@ -1175,7 +1205,7 @@ switch ($num) { } } } - 9 { + 10 { Write-Host "" Write-Warn "Skipped. An LLM API key is required to test and use worker agents." Write-Host " Add your API key later by running:" @@ -1252,6 +1282,70 @@ if ($SubscriptionMode -eq "zai_code") { } } +# For Kimi Code subscription: prompt for API key with verification + retry +if ($SubscriptionMode -eq "kimi_code") { + while ($true) { + $existingKimi = [System.Environment]::GetEnvironmentVariable("KIMI_API_KEY", "User") + if (-not $existingKimi) { $existingKimi = $env:KIMI_API_KEY } + + if ($existingKimi) { + $masked = $existingKimi.Substring(0, [Math]::Min(4, $existingKimi.Length)) + "..." + $existingKimi.Substring([Math]::Max(0, $existingKimi.Length - 4)) + Write-Host "" + Write-Color -Text " $([char]0x2B22) Current Kimi key: $masked" -Color Green + $apiKey = Read-Host " Press Enter to keep, or paste a new key to replace" + } else { + Write-Host "" + Write-Host "Get your API key from: " -NoNewline + Write-Color -Text "https://www.kimi.com/code" -Color Cyan + Write-Host "" + $apiKey = Read-Host "Paste your Kimi API key (or press Enter to skip)" + } + + if ($apiKey) { + [System.Environment]::SetEnvironmentVariable("KIMI_API_KEY", $apiKey, "User") + $env:KIMI_API_KEY = $apiKey + Write-Host "" + Write-Ok "Kimi API key saved as User environment variable" + + # Health check the new key + Write-Host " Verifying Kimi API key... " -NoNewline + try { + $hcResult = & uv run python (Join-Path $ScriptDir "scripts/check_llm_key.py") "kimi" $apiKey "https://api.kimi.com/coding" 2>$null + $hcJson = $hcResult | ConvertFrom-Json + if ($hcJson.valid -eq $true) { + Write-Color -Text "ok" -Color Green + break + } elseif ($hcJson.valid -eq $false) { + Write-Color -Text "failed" -Color Red + Write-Warn $hcJson.message + [System.Environment]::SetEnvironmentVariable("KIMI_API_KEY", $null, "User") + Remove-Item -Path "Env:\KIMI_API_KEY" -ErrorAction SilentlyContinue + Write-Host "" + Read-Host " Press Enter to try again" + } else { + Write-Color -Text "--" -Color Yellow + Write-Color -Text " Could not verify key (network issue). The key has been saved." -Color DarkGray + break + } + } catch { + Write-Color -Text "--" -Color Yellow + Write-Color -Text " Could not verify key (network issue). The key has been saved." -Color DarkGray + break + } + } elseif (-not $existingKimi) { + Write-Host "" + Write-Warn "Skipped. Add your Kimi API key later:" + Write-Color -Text " [System.Environment]::SetEnvironmentVariable('KIMI_API_KEY', 'your-key', 'User')" -Color Cyan + $SelectedEnvVar = "" + $SelectedProviderId = "" + $SubscriptionMode = "" + break + } else { + break + } + } +} + # Prompt for model if not already selected (manual provider path) if ($SelectedProviderId -and -not $SelectedModel) { $modelSel = Get-ModelSelection $SelectedProviderId @@ -1287,6 +1381,9 @@ if ($SelectedProviderId) { } elseif ($SubscriptionMode -eq "zai_code") { $config.llm["api_base"] = "https://api.z.ai/api/coding/paas/v4" $config.llm["api_key_env_var"] = $SelectedEnvVar + } elseif ($SubscriptionMode -eq "kimi_code") { + $config.llm["api_base"] = "https://api.kimi.com/coding" + $config.llm["api_key_env_var"] = $SelectedEnvVar } else { $config.llm["api_key_env_var"] = $SelectedEnvVar } diff --git a/quickstart.sh b/quickstart.sh index f2f5bce0..0e158963 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -410,7 +410,7 @@ if [ "$USE_ASSOC_ARRAYS" = true ]; then declare -A DEFAULT_MODELS=( ["anthropic"]="claude-haiku-4-5-20251001" ["openai"]="gpt-5-mini" - ["minimax"]="MiniMax-M2.1" + ["minimax"]="MiniMax-M2.5" ["gemini"]="gemini-3-flash-preview" ["groq"]="moonshotai/kimi-k2-instruct-0905" ["cerebras"]="zai-glm-4.7" @@ -510,7 +510,7 @@ else # Default models by provider id (parallel arrays) MODEL_PROVIDER_IDS=(anthropic openai minimax gemini groq cerebras mistral together_ai deepseek) - MODEL_DEFAULTS=("claude-haiku-4-5-20251001" "gpt-5-mini" "MiniMax-M2.1" "gemini-3-flash-preview" "moonshotai/kimi-k2-instruct-0905" "zai-glm-4.7" "mistral-large-latest" "meta-llama/Llama-3.3-70B-Instruct-Turbo" "deepseek-chat") + MODEL_DEFAULTS=("claude-haiku-4-5-20251001" "gpt-5-mini" "MiniMax-M2.5" "gemini-3-flash-preview" "moonshotai/kimi-k2-instruct-0905" "zai-glm-4.7" "mistral-large-latest" "meta-llama/Llama-3.3-70B-Instruct-Turbo" "deepseek-chat") # Helper: get provider display name for an env var get_provider_name() { @@ -824,6 +824,13 @@ if [ -n "${MINIMAX_API_KEY:-}" ]; then MINIMAX_CRED_DETECTED=true fi +KIMI_CRED_DETECTED=false +if [ -f "$HOME/.kimi/config.toml" ]; then + KIMI_CRED_DETECTED=true +elif [ -n "${KIMI_API_KEY:-}" ]; then + KIMI_CRED_DETECTED=true +fi + # Detect API key providers if [ "$USE_ASSOC_ARRAYS" = true ]; then for env_var in "${!PROVIDER_NAMES[@]}"; do @@ -859,6 +866,7 @@ try: sub = '' if llm.get('use_claude_code_subscription'): sub = 'claude_code' elif llm.get('use_codex_subscription'): sub = 'codex' + elif llm.get('use_kimi_code_subscription'): sub = 'kimi_code' elif llm.get('provider', '') == 'minimax' or 'api.minimax.io' in llm.get('api_base', ''): sub = 'minimax_code' elif 'api.z.ai' in llm.get('api_base', ''): sub = 'zai_code' print(f'PREV_SUB_MODE={sub}') @@ -875,6 +883,7 @@ if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then claude_code) [ "$CLAUDE_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; zai_code) [ "$ZAI_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; codex) [ "$CODEX_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; + kimi_code) [ "$KIMI_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; *) # API key provider — check if the env var is set if [ -n "$PREV_ENV_VAR" ] && [ -n "${!PREV_ENV_VAR}" ]; then @@ -889,15 +898,17 @@ if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then zai_code) DEFAULT_CHOICE=2 ;; codex) DEFAULT_CHOICE=3 ;; minimax_code) DEFAULT_CHOICE=4 ;; + kimi_code) DEFAULT_CHOICE=5 ;; esac if [ -z "$DEFAULT_CHOICE" ]; then case "$PREV_PROVIDER" in - anthropic) DEFAULT_CHOICE=5 ;; - openai) DEFAULT_CHOICE=6 ;; - gemini) DEFAULT_CHOICE=7 ;; - groq) DEFAULT_CHOICE=8 ;; - cerebras) DEFAULT_CHOICE=9 ;; + anthropic) DEFAULT_CHOICE=6 ;; + openai) DEFAULT_CHOICE=7 ;; + gemini) DEFAULT_CHOICE=8 ;; + groq) DEFAULT_CHOICE=9 ;; + cerebras) DEFAULT_CHOICE=10 ;; minimax) DEFAULT_CHOICE=4 ;; + kimi) DEFAULT_CHOICE=5 ;; esac fi fi @@ -936,14 +947,21 @@ else echo -e " ${CYAN}4)${NC} MiniMax Coding Key ${DIM}(use your MiniMax coding key)${NC}" fi +# 5) Kimi Code +if [ "$KIMI_CRED_DETECTED" = true ]; then + echo -e " ${CYAN}5)${NC} Kimi Code Subscription ${DIM}(use your Kimi Code plan)${NC} ${GREEN}(credential detected)${NC}" +else + echo -e " ${CYAN}5)${NC} Kimi Code Subscription ${DIM}(use your Kimi Code plan)${NC}" +fi + echo "" echo -e " ${CYAN}${BOLD}API key providers:${NC}" -# 5-9) API key providers — show (credential detected) if key already set +# 6-10) API key providers — show (credential detected) if key already set PROVIDER_MENU_ENVS=(ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY GROQ_API_KEY CEREBRAS_API_KEY) PROVIDER_MENU_NAMES=("Anthropic (Claude) - Recommended" "OpenAI (GPT)" "Google Gemini - Free tier available" "Groq - Fast, free tier" "Cerebras - Fast, free tier") for idx in 0 1 2 3 4; do - num=$((idx + 5)) + num=$((idx + 6)) env_var="${PROVIDER_MENU_ENVS[$idx]}" if [ -n "${!env_var}" ]; then echo -e " ${CYAN}$num)${NC} ${PROVIDER_MENU_NAMES[$idx]} ${GREEN}(credential detected)${NC}" @@ -952,7 +970,7 @@ for idx in 0 1 2 3 4; do fi done -echo -e " ${CYAN}10)${NC} Skip for now" +echo -e " ${CYAN}11)${NC} Skip for now" echo "" if [ -n "$DEFAULT_CHOICE" ]; then @@ -962,15 +980,15 @@ fi while true; do if [ -n "$DEFAULT_CHOICE" ]; then - read -r -p "Enter choice (1-10) [$DEFAULT_CHOICE]: " choice || true + read -r -p "Enter choice (1-11) [$DEFAULT_CHOICE]: " choice || true choice="${choice:-$DEFAULT_CHOICE}" else - read -r -p "Enter choice (1-10): " choice || true + read -r -p "Enter choice (1-11): " choice || true fi - if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le 10 ]; then + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le 11 ]; then break fi - echo -e "${RED}Invalid choice. Please enter 1-10${NC}" + echo -e "${RED}Invalid choice. Please enter 1-11${NC}" done case $choice in @@ -1038,46 +1056,60 @@ case $choice in SUBSCRIPTION_MODE="minimax_code" SELECTED_ENV_VAR="MINIMAX_API_KEY" SELECTED_PROVIDER_ID="minimax" - SELECTED_MODEL="MiniMax-M2.1" - SELECTED_MAX_TOKENS=8192 + SELECTED_MODEL="MiniMax-M2.5" + SELECTED_MAX_TOKENS=32768 SELECTED_API_BASE="https://api.minimax.io/v1" PROVIDER_NAME="MiniMax" SIGNUP_URL="https://platform.minimax.io/user-center/basic-information/interface-key" echo "" echo -e "${GREEN}⬢${NC} Using MiniMax coding key" - echo -e " ${DIM}Model: MiniMax-M2.1 | API: api.minimax.io${NC}" + echo -e " ${DIM}Model: MiniMax-M2.5 | API: api.minimax.io${NC}" ;; 5) + # Kimi Code Subscription + SUBSCRIPTION_MODE="kimi_code" + SELECTED_PROVIDER_ID="kimi" + SELECTED_ENV_VAR="KIMI_API_KEY" + SELECTED_MODEL="kimi-k2.5" + SELECTED_MAX_TOKENS=32768 + SELECTED_API_BASE="https://api.kimi.com/coding" + PROVIDER_NAME="Kimi" + SIGNUP_URL="https://www.kimi.com/code" + echo "" + echo -e "${GREEN}⬢${NC} Using Kimi Code subscription" + echo -e " ${DIM}Model: kimi-k2.5 | API: api.kimi.com/coding${NC}" + ;; + 6) SELECTED_ENV_VAR="ANTHROPIC_API_KEY" SELECTED_PROVIDER_ID="anthropic" PROVIDER_NAME="Anthropic" SIGNUP_URL="https://console.anthropic.com/settings/keys" ;; - 6) + 7) SELECTED_ENV_VAR="OPENAI_API_KEY" SELECTED_PROVIDER_ID="openai" PROVIDER_NAME="OpenAI" SIGNUP_URL="https://platform.openai.com/api-keys" ;; - 7) + 8) SELECTED_ENV_VAR="GEMINI_API_KEY" SELECTED_PROVIDER_ID="gemini" PROVIDER_NAME="Google Gemini" SIGNUP_URL="https://aistudio.google.com/apikey" ;; - 8) + 9) SELECTED_ENV_VAR="GROQ_API_KEY" SELECTED_PROVIDER_ID="groq" PROVIDER_NAME="Groq" SIGNUP_URL="https://console.groq.com/keys" ;; - 9) + 10) SELECTED_ENV_VAR="CEREBRAS_API_KEY" SELECTED_PROVIDER_ID="cerebras" PROVIDER_NAME="Cerebras" SIGNUP_URL="https://cloud.cerebras.ai/" ;; - 10) + 11) echo "" echo -e "${YELLOW}Skipped.${NC} An LLM API key is required to test and use worker agents." echo -e "Add your API key later by running:" @@ -1090,7 +1122,7 @@ case $choice in esac # For API-key providers: prompt for key (allow replacement if already set) -if { [ -z "$SUBSCRIPTION_MODE" ] || [ "$SUBSCRIPTION_MODE" = "minimax_code" ]; } && [ -n "$SELECTED_ENV_VAR" ]; then +if { [ -z "$SUBSCRIPTION_MODE" ] || [ "$SUBSCRIPTION_MODE" = "minimax_code" ] || [ "$SUBSCRIPTION_MODE" = "kimi_code" ]; } && [ -n "$SELECTED_ENV_VAR" ]; then while true; do CURRENT_KEY="${!SELECTED_ENV_VAR}" if [ -n "$CURRENT_KEY" ]; then @@ -1118,7 +1150,7 @@ if { [ -z "$SUBSCRIPTION_MODE" ] || [ "$SUBSCRIPTION_MODE" = "minimax_code" ]; } echo -e "${GREEN}⬢${NC} API key saved to $SHELL_RC_FILE" # Health check the new key echo -n " Verifying API key... " - if [ "$SUBSCRIPTION_MODE" = "minimax_code" ] && [ -n "${SELECTED_API_BASE:-}" ]; then + if { [ "$SUBSCRIPTION_MODE" = "minimax_code" ] || [ "$SUBSCRIPTION_MODE" = "kimi_code" ]; } && [ -n "${SELECTED_API_BASE:-}" ]; then HC_RESULT=$(uv run python "$SCRIPT_DIR/scripts/check_llm_key.py" "$SELECTED_PROVIDER_ID" "$API_KEY" "$SELECTED_API_BASE" 2>/dev/null) || true else HC_RESULT=$(uv run python "$SCRIPT_DIR/scripts/check_llm_key.py" "$SELECTED_PROVIDER_ID" "$API_KEY" 2>/dev/null) || true @@ -1238,6 +1270,8 @@ if [ -n "$SELECTED_PROVIDER_ID" ]; then save_configuration "$SELECTED_PROVIDER_ID" "$SELECTED_ENV_VAR" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "" "https://api.z.ai/api/coding/paas/v4" > /dev/null elif [ "$SUBSCRIPTION_MODE" = "minimax_code" ]; then save_configuration "$SELECTED_PROVIDER_ID" "$SELECTED_ENV_VAR" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "" "$SELECTED_API_BASE" > /dev/null + elif [ "$SUBSCRIPTION_MODE" = "kimi_code" ]; then + save_configuration "$SELECTED_PROVIDER_ID" "$SELECTED_ENV_VAR" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "" "$SELECTED_API_BASE" > /dev/null else save_configuration "$SELECTED_PROVIDER_ID" "$SELECTED_ENV_VAR" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" > /dev/null fi diff --git a/scripts/check_llm_key.py b/scripts/check_llm_key.py index a9222aaf..e15b91a8 100644 --- a/scripts/check_llm_key.py +++ b/scripts/check_llm_key.py @@ -69,7 +69,7 @@ def check_minimax(api_key: str, api_base: str = "https://api.minimax.io/v1", **_ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, - json={"model": "MiniMax-M2.1", "messages": []}, + json={"model": "MiniMax-M2.5", "messages": []}, ) if r.status_code in (200, 400, 422, 429): return {"valid": True, "message": "MiniMax API key valid"} @@ -80,6 +80,27 @@ def check_minimax(api_key: str, api_base: str = "https://api.minimax.io/v1", **_ return {"valid": False, "message": f"MiniMax API returned status {r.status_code}"} +def check_anthropic_compatible(api_key: str, endpoint: str, name: str) -> dict: + """POST empty messages to an Anthropic-compatible endpoint to validate key.""" + with httpx.Client(timeout=TIMEOUT) as client: + r = client.post( + endpoint, + headers={ + "x-api-key": api_key, + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", + }, + json={"model": "kimi-k2.5", "max_tokens": 1, "messages": []}, + ) + if r.status_code in (200, 400, 429): + return {"valid": True, "message": f"{name} API key valid"} + if r.status_code == 401: + return {"valid": False, "message": f"Invalid {name} API key"} + if r.status_code == 403: + return {"valid": False, "message": f"{name} API key lacks permissions"} + return {"valid": False, "message": f"{name} API returned status {r.status_code}"} + + def check_gemini(api_key: str, **_: str) -> dict: """List models with query param auth.""" with httpx.Client(timeout=TIMEOUT) as client: @@ -107,6 +128,11 @@ PROVIDERS = { key, "https://api.cerebras.ai/v1/models", "Cerebras" ), "minimax": lambda key, **kw: check_minimax(key), + # Kimi For Coding uses an Anthropic-compatible endpoint; check via /v1/messages + # with empty messages (same as check_anthropic, triggers 400 not 401). + "kimi": lambda key, **kw: check_anthropic_compatible( + key, "https://api.kimi.com/coding/v1/messages", "Kimi" + ), } @@ -129,6 +155,11 @@ def main() -> None: try: if api_base and provider_id == "minimax": result = check_minimax(api_key, api_base) + elif api_base and provider_id == "kimi": + # Kimi uses an Anthropic-compatible endpoint; check via /v1/messages + result = check_anthropic_compatible( + api_key, api_base.rstrip("/") + "/v1/messages", "Kimi" + ) elif api_base: # Custom API base (ZAI or other OpenAI-compatible) endpoint = api_base.rstrip("/") + "/models"