Merge pull request #3415 from nesquena/release/stage-batch1
Release GE — v0.51.211 (reasoning heuristics + /model shortest-match + Copilot env-token filter)
This commit is contained in:
4 files changed
+140
-15
No files matched your search
@@ -3,6 +3,13 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.211] — 2026-06-02 — Release GE (stage-batch1 — reasoning heuristics + /model shortest-match + Copilot env-token filter)
|
||||
|
||||
### 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 (#3379, @b3nw, closes #3377).
|
||||
- The `/model` slash command no longer selects a longer model variant when a shorter name is a prefix of it (e.g. `/model mimo-v2.5` selecting `mimo-v2.5-pro`). The fuzzy fallback now prefers an exact id/label match and otherwise the shortest matching option, applied to both the main and bare-name (`provider/...`) fallbacks (#3394, @vanshaj-pahwa, closes #3368).
|
||||
- `GITHUB_TOKEN` and `GH_TOKEN` environment variables are now filtered from the Copilot credential pool alongside the seeded `gh`-CLI token, so a classic PAT (`ghp_*`) auto-detected from the environment no longer makes Copilot appear in the model picker when the Copilot API can't use it. User-specific `COPILOT_GITHUB_TOKEN` is still respected (#3382, @happy5318).
|
||||
|
||||
## [v0.51.210] — 2026-06-02 — Release GD (stage-batch1 — model-picker multi-slash fix + extensionless preview highlighting)
|
||||
|
||||
### Fixed
|
||||
|
||||
+49
-5
@@ -1250,14 +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 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.strip().lower() in _AMBIENT_GH_CLI_MARKERS
|
||||
source_lower in _AMBIENT_GH_CLI_MARKERS
|
||||
or source_lower in _AMBIENT_GH_ENV_SOURCES
|
||||
or label.strip().lower() == "gh auth token"
|
||||
or key_source.strip().lower() == "gh auth token"
|
||||
)
|
||||
@@ -2136,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
|
||||
|
||||
|
||||
+20
-10
@@ -322,6 +322,24 @@ function cmdClear(){
|
||||
showToast(t('conversation_cleared'));
|
||||
}
|
||||
|
||||
// Find the best matching model <option> for a slash-command query.
|
||||
// Returns an exact id/label match if present, otherwise the shortest option
|
||||
// whose value or label contains the query. Preferring the shortest match keeps
|
||||
// a specific query like "mimo-v2.5" from being shadowed by a longer variant
|
||||
// such as "mimo-v2.5-pro". See issue #3368.
|
||||
function _bestModelMatch(options,query){
|
||||
let best=null;
|
||||
for(const opt of options){
|
||||
const value=opt.value.toLowerCase();
|
||||
const text=opt.textContent.toLowerCase();
|
||||
if(value===query||text===query) return opt.value;
|
||||
if(value.includes(query)||text.includes(query)){
|
||||
if(best===null||opt.value.length<best.length) best=opt.value;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
async function cmdModel(args){
|
||||
if(!args){showToast(t('model_usage'));return;}
|
||||
const sel=$('modelSelect');
|
||||
@@ -348,21 +366,13 @@ async function cmdModel(args){
|
||||
let match=(typeof _findModelInDropdown==='function')?_findModelInDropdown(q,sel,preferred):null;
|
||||
// Fallback: fuzzy match across all options
|
||||
if(!match){
|
||||
for(const opt of sel.options){
|
||||
if(opt.value.toLowerCase().includes(q)||opt.textContent.toLowerCase().includes(q)){
|
||||
match=opt.value;break;
|
||||
}
|
||||
}
|
||||
match=_bestModelMatch(sel.options,q);
|
||||
}
|
||||
// Fallback: if q has provider/ prefix (e.g. "deepseek/deepseek-v4-flash"),
|
||||
// try the bare model name (which is how options appear for the active provider)
|
||||
if(!match && q.includes('/')){
|
||||
const bare=q.slice(q.lastIndexOf('/')+1);
|
||||
for(const opt of sel.options){
|
||||
if(opt.value.toLowerCase().includes(bare)||opt.textContent.toLowerCase().includes(bare)){
|
||||
match=opt.value;break;
|
||||
}
|
||||
}
|
||||
match=_bestModelMatch(sel.options,bare);
|
||||
// Cross-provider fallback: if still no match, the model is from a
|
||||
// different provider not in the dropdown. Call /api/session/update directly.
|
||||
if(!match && S&&S.session&&S.session.session_id){
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user