From 02a1849ca456ffc09c47cdd97f7c165b020a01f0 Mon Sep 17 00:00:00 2001 From: happy5318 Date: Tue, 2 Jun 2026 14:04:37 +0800 Subject: [PATCH 1/5] fix: filter GITHUB_TOKEN env var from Copilot credential pool Copilot API does not support classic PATs (ghp_*), only OAuth tokens (gho_*) or fine-grained PATs (github_pat_*). When GITHUB_TOKEN env var contains a classic PAT, it should be filtered from the credential pool to prevent Copilot from appearing in the model picker dropdown. The _is_ambient_gh_cli_entry() function now also filters entries with source='env:GITHUB_TOKEN', treating them as ambient/auto-detected rather than user-explicitly-added credentials. Fixes issue where Copilot group appeared in WebUI model picker even when gh auth token was an unsupported classic PAT. --- api/config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/config.py b/api/config.py index b474a822..d306d89d 100644 --- a/api/config.py +++ b/api/config.py @@ -1255,9 +1255,16 @@ def _is_ambient_gh_cli_entry(source: str, label: str, key_source: str) -> bool: """True when a credential-pool entry is a seeded gh-cli token rather than one the user added explicitly. Filter these so Copilot doesn't appear in the dropdown just because `gh` is installed on the system. + + Also filters GITHUB_TOKEN env var entries, which are auto-detected from + the environment and should not cause Copilot to appear in the picker + when the token is a classic PAT (ghp_*) that Copilot API doesn't support. """ + source_lower = source.strip().lower() return ( - source.strip().lower() in _AMBIENT_GH_CLI_MARKERS + source_lower in _AMBIENT_GH_CLI_MARKERS + # Filter GITHUB_TOKEN env var entries (source="env:GITHUB_TOKEN") + or source_lower == "env:github_token" or label.strip().lower() == "gh auth token" or key_source.strip().lower() == "gh auth token" ) From 2892d3d6c08afa25c4ae2aab98504ccffaa0829b Mon Sep 17 00:00:00 2001 From: happy5318 Date: Tue, 2 Jun 2026 18:19:39 +0800 Subject: [PATCH 2/5] fix: also filter GH_TOKEN env var from Copilot credential pool Follow-up to PR #3382 review feedback. There are three GitHub-related env vars that can be auto-detected: - COPILOT_GITHUB_TOKEN: user-specific config, should NOT be filtered - GITHUB_TOKEN: classic PAT, should be filtered (already done) - GH_TOKEN: another env var, should also be filtered (this commit) Use a frozenset (_AMBIENT_GH_ENV_SOURCES) to clearly define which env var sources are ambient vs user-configured. --- api/config.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/api/config.py b/api/config.py index d306d89d..31a74249 100644 --- a/api/config.py +++ b/api/config.py @@ -1250,21 +1250,29 @@ _PROVIDER_MODELS = { _AMBIENT_GH_CLI_MARKERS = frozenset({"gh_cli", "gh auth token"}) +# Environment variable sources that are auto-detected and should be filtered +# when the token is a classic PAT (ghp_*) that Copilot API doesn't support. +# Note: COPILOT_GITHUB_TOKEN is NOT included here - it's user-specific config. +_AMBIENT_GH_ENV_SOURCES = frozenset({"env:github_token", "env:gh_token"}) + def _is_ambient_gh_cli_entry(source: str, label: str, key_source: str) -> bool: """True when a credential-pool entry is a seeded gh-cli token rather than one the user added explicitly. Filter these so Copilot doesn't appear in the dropdown just because `gh` is installed on the system. - Also filters GITHUB_TOKEN env var entries, which are auto-detected from - the environment and should not cause Copilot to appear in the picker - when the token is a classic PAT (ghp_*) that Copilot API doesn't support. + Also filters GITHUB_TOKEN and GH_TOKEN env var entries, which are + auto-detected from the environment and should not cause Copilot to appear + in the picker when the token is a classic PAT (ghp_*) that Copilot API + doesn't support. + + Note: COPILOT_GITHUB_TOKEN is NOT filtered - it's user-specific config + that should always be respected. """ source_lower = source.strip().lower() return ( source_lower in _AMBIENT_GH_CLI_MARKERS - # Filter GITHUB_TOKEN env var entries (source="env:GITHUB_TOKEN") - or source_lower == "env:github_token" + or source_lower in _AMBIENT_GH_ENV_SOURCES or label.strip().lower() == "gh auth token" or key_source.strip().lower() == "gh auth token" ) From 68716b3e4fbb64c0e12ad1a3a2abae7abecd7217 Mon Sep 17 00:00:00 2001 From: b3nw Date: Tue, 2 Jun 2026 04:34:40 +0000 Subject: [PATCH 3/5] fix: generalized reasoning capability heuristics for custom providers (#3377) --- CHANGELOG.md | 3 + api/config.py | 37 +++++++++-- ...st_custom_provider_bare_model_reasoning.py | 64 +++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 147e0cbb..e8d2d5c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## [Unreleased] +### Fixed +- Generalized reasoning-effort capability checks in `_candidate_supports_reasoning` to target whole model families (GPT-5+, Claude 4/3.7, Qwen-3, Kimi, Minimax, Mimo, GLM, Step, and DeepSeek) instead of anchoring on hardcoded version numbers or vendor formats. This prevents the thinking-level configuration selector from being hidden on custom providers, new model releases, or when names carry suffixes like `-free` or `:free` (common on integrations such as Kilo Code or OpenCode Zen). The GPT heuristic is now version-anchored (5+) to avoid falsely enabling reasoning_effort for gpt-4o/4.1/3.5 on aggregator providers (#3377). + ## [v0.51.210] — 2026-06-02 — Release GD (stage-batch1 — model-picker multi-slash fix + extensionless preview highlighting) ### Fixed diff --git a/api/config.py b/api/config.py index 31a74249..014c1e91 100644 --- a/api/config.py +++ b/api/config.py @@ -2151,15 +2151,44 @@ def _candidate_supports_reasoning(candidate: str) -> bool: if "thinking" in token_set or "reasoning" in token_set: return True + if "gpt" in token_set or normalized.startswith("gpt"): + # Restrict to GPT-5+ (exclude GPT-4o/4.1/3.5 — reasoning_effort unsupported) + m = re.search(r"gpt-(\d+)", normalized) + if m and int(m.group(1)) >= 5: + return True + return False if normalized in {"o1", "o3", "o4"} or normalized.startswith(("o1-", "o3-", "o4-")): return True - if normalized.startswith(("kimi-k2", "kimi-thinking", "claude-3", "claude-4")): + if "claude" in token_set or normalized.startswith("claude"): + # Restrict to Claude 4+ or Claude 3.7+ (exclude Claude 3.0/3.5) + match = re.search(r"claude.*?(\d+)(?:\D+(\d+))?", normalized) + if match: + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + if major >= 4 or (major == 3 and minor >= 7): + return True + return False + if "qwen" in token_set or normalized.startswith("qwen"): + # Restrict to Qwen 3+ (exclude Qwen 2/2.5) + match = re.search(r"qwen.*?(\d+)(?:\D+(\d+))?", normalized) + if match: + major = int(match.group(1)) + if major >= 3: + return True + return False + if "kimi" in token_set or normalized.startswith("kimi"): return True - if normalized.startswith("qwen3") or "qwen3" in token_set: + if "minimax" in token_set or normalized.startswith("minimax"): return True - if normalized.startswith(("deepseek-v3", "deepseek-v4", "deepseek-r1", "deepseek-r2")): + if "mimo" in token_set or normalized.startswith("mimo"): return True - if len(tokens) >= 2 and tokens[0] == "deepseek" and tokens[1] in {"v3", "v4", "r1", "r2"}: + if "glm" in token_set or normalized.startswith("glm"): + return True + if "step" in token_set or normalized.startswith("step"): + return True + if normalized.startswith(("deepseek-v", "deepseek-r")): + return True + if len(tokens) >= 2 and tokens[0] == "deepseek" and tokens[1].startswith(("v", "r")): return True return False diff --git a/tests/test_custom_provider_bare_model_reasoning.py b/tests/test_custom_provider_bare_model_reasoning.py index 46fbf3f1..6d064d75 100644 --- a/tests/test_custom_provider_bare_model_reasoning.py +++ b/tests/test_custom_provider_bare_model_reasoning.py @@ -143,3 +143,67 @@ def test_openrouter_slash_prefix_unaffected(): provider_id="openrouter", ) assert set(efforts) >= {"low", "medium", "high"} + + +def test_generalized_model_families_and_suffixed_ids(): + test_models = [ + # GPT + ("gpt-5.5", "custom:newapi"), + ("gpt-6-ultra", "custom:newapi"), + # Claude + ("claude-sonnet-4-6-free", "opencode-zen"), + ("claude-opus-4-7:free", "kilocode"), + ("claude-sonnet-3-7-free", "opencode-zen"), + # Qwen + ("qwen-3-coder-free", "opencode-zen"), + ("qwen-4-coder:free", "opencode-zen"), + # Minimax + ("minimax-m2.5-free", "opencode-zen"), + ("minimax-m3-pro", "custom:newapi"), + # Mimo + ("mimo-v2.5-free", "opencode-zen"), + ("mimo-v3-pro", "custom:newapi"), + # GLM + ("glm-5.1:free", "kilocode"), + ("glm-6-pro", "custom:newapi"), + # Step + ("step-1.5:free", "kilocode"), + ("step-2-pro", "custom:newapi"), + # DeepSeek + ("deepseek-v5-free", "custom:newapi"), + ("deepseek-r3:free", "kilocode"), + # Kimi + ("kimi-k2.6-free", "opencode-zen"), + ("kimi-k3-pro:free", "kilocode") + ] + + for model_id, provider_id in test_models: + efforts = cfg.resolve_model_reasoning_efforts(model_id, provider_id=provider_id) + assert set(efforts) >= {"low", "medium", "high"}, ( + f"Failed: {model_id} via {provider_id} should resolve reasoning support" + ) + + +def test_unsupported_model_families_and_versions(): + unsupported_models = [ + # GPT: only 5+ supports reasoning_effort — gpt-4o/4.1/3.5 must be excluded + ("gpt-4o", "opencode-zen"), + ("gpt-4o-mini", "opencode-zen"), + ("gpt-4.1", "kilocode"), + ("gpt-4-turbo", "custom:newapi"), + ("gpt-3.5-turbo", "opencode-zen"), + # Claude + ("claude-sonnet-3.5", "opencode-zen"), + ("claude-opus-3-5-free", "kilocode"), + # Qwen + ("qwen-2.5-coder-free", "opencode-zen"), + ("qwen-2-7b-instruct", "custom:newapi"), + ] + + for model_id, provider_id in unsupported_models: + efforts = cfg.resolve_model_reasoning_efforts(model_id, provider_id=provider_id) + assert efforts == [], ( + f"Failed: {model_id} via {provider_id} should NOT resolve reasoning support" + ) + + From 0ea5e4ef690429b820ec52636c76941d8addacc1 Mon Sep 17 00:00:00 2001 From: Vanshaj Pahwa Date: Tue, 2 Jun 2026 14:27:55 +0530 Subject: [PATCH 4/5] Fix model selection shadowing shorter model names (#3368) The /model slash command's fuzzy fallback used a substring match that took the first option containing the query, so "/model mimo-v2.5" selected "mimo-v2.5-pro" instead of "mimo-v2.5". Replace both fallback loops with a shared _bestModelMatch helper that prefers an exact id/label match and otherwise returns the shortest option containing the query, so a more specific query is no longer shadowed by a longer variant. The longer variant is still selectable by querying its full name. Closes #3368 --- static/commands.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/static/commands.js b/static/commands.js index d95529e0..e2d55249 100644 --- a/static/commands.js +++ b/static/commands.js @@ -322,6 +322,24 @@ function cmdClear(){ showToast(t('conversation_cleared')); } +// Find the best matching model