From f4628375360ca460a5bbcf581160824626c255fe Mon Sep 17 00:00:00 2001 From: decolua Date: Sat, 26 Sep 2026 11:14:04 +0700 Subject: [PATCH] fix(cli): filter model selector by active connections and noAuth providers Co-Authored-By: Claude Code --- CLAUDE.md | 1 + cli/src/cli/utils/modelSelector.js | 83 ++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d7c21345..8cbdaf9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,4 +88,5 @@ Pre-translate hooks that compress `tool_result` content in-place to cut tokens. - `custom-server.js` wraps the Next standalone server to derive client IP from the TCP socket and strip attacker-controlled `X-Forwarded-For` — trusting forwarding headers only from a loopback reverse proxy. Preserve this when touching request/IP/rate-limit code. - Security-sensitive env: `JWT_SECRET` (session cookie), `INITIAL_PASSWORD` (default `123456` — must override), `API_KEY_SECRET`, `MACHINE_ID_SALT`. Full env contract in `.env.example` and ARCHITECTURE.md's env matrix. - Binary/protobuf upstreams (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — they're handled inside their own executor, not the translator. +- **Security-first on PRs**: Security is the top priority when reviewing or creating PRs. Audit authentication, credential/token storage & leaks, header manipulation (`X-Forwarded-For`), and SSRF risks before functional logic. Always include explicit security warnings/notes when reporting PR reviews or changes to the user. - Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`). diff --git a/cli/src/cli/utils/modelSelector.js b/cli/src/cli/utils/modelSelector.js index a2bd3fdc..cec99deb 100644 --- a/cli/src/cli/utils/modelSelector.js +++ b/cli/src/cli/utils/modelSelector.js @@ -2,22 +2,24 @@ const api = require("../api/client"); const { prompt } = require("./input"); const { clearScreen } = require("./display"); -// Provider alias order: OAuth first, then API Key (matches ModelSelectModal) +// Provider alias order: OAuth first, then Free, then API Key const PROVIDER_ALIAS_ORDER = [ - "cc", "ag", "cx", "if", "qw", "gc", "gh", "kr", + "cc", "ag", "cx", "if", "qw", "gc", "gh", "kr", "oc", "openrouter", "glm", "kimi", "minimax", "openai", "anthropic", "gemini" ]; // Alias to display name mapping const PROVIDER_ALIAS_NAMES = { cc: "Claude Code", - ag: "Antigravity", + ag: "Antigravity", cx: "OpenAI Codex", if: "iFlow AI", qw: "Qwen Code", gc: "Gemini CLI", gh: "GitHub Copilot", kr: "Kiro AI", + oc: "OpenCode Free", + opencode: "OpenCode Free", openrouter: "OpenRouter", glm: "GLM Coding", kimi: "Kimi Coding", @@ -27,30 +29,78 @@ const PROVIDER_ALIAS_NAMES = { gemini: "Gemini" }; +const PROVIDER_ID_TO_ALIAS = { + claude: "cc", + codex: "cx", + "gemini-cli": "gc", + github: "gh", + antigravity: "ag", + iflow: "if", + qwen: "qw", + kiro: "kr", + cursor: "cu", + cline: "cline", + clinepass: "clinepass", + qoder: "qd", + "qoder-cn": "qd", + gitlab: "gitlab", + "codebuddy-cn": "cb", + "codebuddy-intl": "cbai", + kimchi: "kimchi", + "grok-cli": "grok-cli", + trae: "trae", + windsurf: "windsurf", + zed: "zed", + opencode: "oc", + "opencode-go": "ocg", + "opencode-zen": "ocz", +}; + +// Providers usable without stored credentials +const NO_AUTH_PROVIDERS = new Set(["opencode", "oc"]); + /** - * Get all available models grouped by provider + combos + * Get all available models grouped by provider + combos (filtered by active connections) * @returns {Promise<{combos: Array, groups: Object}>} */ async function getAvailableModelsGrouped() { - const result = await api.getAvailableModels(); - if (!result.success) return { combos: [], groups: {} }; - - const models = result.data?.data || []; + const [modelsResult, providersResult] = await Promise.all([ + api.getAvailableModels(), + api.getProviders() + ]); + + if (!modelsResult.success) return { combos: [], groups: {} }; + + const connections = providersResult.success ? (providersResult.data?.connections || []) : []; + const activeAliases = new Set(NO_AUTH_PROVIDERS); + + connections.forEach(conn => { + if (conn.isActive === false) return; + const p = conn.provider; + if (!p) return; + activeAliases.add(p); + const alias = conn.providerSpecificData?.prefix || PROVIDER_ID_TO_ALIAS[p] || p; + activeAliases.add(alias); + }); + + const models = modelsResult.data?.data || []; const combos = []; const groups = {}; - + models.forEach(m => { if (m.owned_by === "combo") { combos.push(m.id); } else { const provider = m.owned_by; + // Only keep connected providers or noAuth providers + if (!activeAliases.has(provider)) return; if (!groups[provider]) { groups[provider] = []; } groups[provider].push(m.id); } }); - + return { combos, groups }; } @@ -68,6 +118,19 @@ async function selectModelFromList(title, currentValue = "", options = {}) { const totalModels = combos.length + Object.values(groups).flat().length; if (totalModels === 0) { + clearScreen(); + console.log(`\nšŸŽÆ ${title}`); + console.log("=".repeat(50)); + console.log("\n No connected providers found."); + console.log(" Please connect a provider in Providers menu first.\n"); + console.log(" m. āœļø Enter custom model ID"); + console.log(" 0. Cancel\n"); + const act = await prompt("Select option (m/0): "); + const trimmed = act.trim(); + if (trimmed.toLowerCase() === "m") { + const custom = await prompt("Enter custom model ID: "); + return custom.trim() || null; + } return null; }