diff --git a/CHANGELOG.md b/CHANGELOG.md index 88efa156..2f537f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Fixed +- **The OpenRouter setup wizard now offers Z.AI models under OpenRouter's own namespace instead of IDs that 404.** `_FALLBACK_MODELS` is authored for the *direct* provider endpoints, so its nine Z.AI entries carry the provider-native `zai/` prefix — and the OpenRouter onboarding setup reused that list verbatim. OpenRouter serves the same models as `z-ai/…`, so anyone who picked a Z.AI model during OpenRouter onboarding was handed an ID the provider does not recognise and their first message failed. The IDs are now translated at the OpenRouter projection boundary only; the fallback catalog and the direct Z.AI setup keep their own namespace. Thanks @webtecnica. (#7518, closes #7514) + - **Recovered "thinking" cards no longer multiply on every turn after context compaction.** When a compacted history left the display transcript longer than the model-facing result, a recovered reasoning-only row could be re-inserted *after* the new reply each turn — doubling the recovered block geometrically (1, 2, 4, 8…). Settlement now fixes the active-turn boundary before restoring reasoning metadata and only restores a historical reasoning row directly in front of its own API-safe successor (requiring a unique stable-id or exact-identity match), failing closed to the current-turn boundary when turn ownership can't be proven. The synchronous chat path mirrors the streaming path, so live and reloaded transcripts stay consistent. Thanks @carlotestor. (#7443, refs #7388) - **Command-backed provider credentials no longer break the agent cache or discard conversation state between turns.** When a provider used `key_cmd`, the runtime supplied a lazy callable token source, but the cache signature tried to call `.encode()` on it; every turn therefore missed the cache and rebuilt the agent. Callable credentials now use a stable, secret-free cache marker while the existing runtime refresh path still receives the callable itself, so token resolution stays lazy and agent context is reused. Thanks @ArushKhasru. (#7398, closes #7396) diff --git a/api/onboarding.py b/api/onboarding.py index cfff9ea2..f09651c4 100644 --- a/api/onboarding.py +++ b/api/onboarding.py @@ -35,6 +35,34 @@ from api.workspace import get_last_workspace, load_workspaces logger = logging.getLogger(__name__) +# ── OpenRouter namespace translation (#7514) ──────────────────────────────── +# +# ``_FALLBACK_MODELS`` (api/config.py) is authored for the *direct* provider +# endpoints, so its Z.AI entries carry the provider's own ``zai/`` prefix. +# OpenRouter serves the same models under its canonical ``z-ai/`` slug, so +# projecting that list into the OpenRouter setup verbatim handed the wizard +# ids that 404 on openrouter.ai (``zai/glm-5.3`` instead of +# ``z-ai/glm-5.3``). Translate at this projection boundary only — the +# fallback catalog and the direct ``zai`` setup keep their own namespace. +# +# Every other namespace in the fallback list is already OpenRouter-canonical: +# openai/, anthropic/, google/, deepseek/, qwen/, x-ai/, mistralai/, minimax/, +# openrouter/, tencent/, nvidia/, arcee-ai/ — hence a single mapping entry. +_OPENROUTER_NAMESPACE_MAP = {"zai/": "z-ai/"} + + +def _to_openrouter_namespace(model_id: str) -> str: + """Return *model_id* with direct-provider prefixes mapped to OpenRouter's. + + Ids whose namespace already matches OpenRouter (or that carry no + namespace) are returned unchanged. + """ + for direct_prefix, openrouter_prefix in _OPENROUTER_NAMESPACE_MAP.items(): + if model_id.startswith(direct_prefix): + return openrouter_prefix + model_id[len(direct_prefix):] + return model_id + + _SUPPORTED_PROVIDER_SETUPS = { # ── Easy start ────────────────────────────────────────────────────── "openrouter": { @@ -43,7 +71,8 @@ _SUPPORTED_PROVIDER_SETUPS = { "default_model": "anthropic/claude-sonnet-4.6", "requires_base_url": False, "models": [ - {"id": model["id"], "label": model["label"]} for model in _FALLBACK_MODELS + {"id": _to_openrouter_namespace(model["id"]), "label": model["label"]} + for model in _FALLBACK_MODELS ], "category": "easy_start", "quick": True, diff --git a/static/panels.js b/static/panels.js index f7d9a8ea..b53a89c6 100644 --- a/static/panels.js +++ b/static/panels.js @@ -12315,12 +12315,23 @@ function _buildAuxProviderOptions(sel,providers,currentProvider){ autoOpt.value='auto';autoOpt.textContent='auto ('+t('settings_aux_provider_auto')+')'; if(currentProvider==='auto'||!currentProvider) autoOpt.selected=true; sel.appendChild(autoOpt); + let matched=currentProvider==='auto'||!currentProvider; for(const p of providers){ const opt=document.createElement('option'); opt.value=p.slug;opt.textContent=p.name; - if(p.slug===currentProvider) opt.selected=true; + if(p.slug===currentProvider){opt.selected=true;matched=true;} sel.appendChild(opt); } + // The configured provider can be absent from the /api/models catalog (e.g. its + // group exposes no models). Keep it selectable: with no matching option the + // select falls back to its first entry ('auto') and the next Apply would + // persist that, silently discarding the configured value. + if(!matched&¤tProvider){ + const configuredOpt=document.createElement('option'); + configuredOpt.value=currentProvider;configuredOpt.textContent=currentProvider+' (configured)'; + configuredOpt.selected=true; + sel.appendChild(configuredOpt); + } } function _buildAuxModelOptions(sel,provider,providers,currentModel){ diff --git a/tests/test_auxiliary_models_settings.py b/tests/test_auxiliary_models_settings.py index 98c73579..513377ee 100644 --- a/tests/test_auxiliary_models_settings.py +++ b/tests/test_auxiliary_models_settings.py @@ -312,6 +312,280 @@ console.log(JSON.stringify({ "Missing _buildAuxModelOptions() for model dropdown rebuild" ) + @pytest.mark.skipif(NODE is None, reason="node not on PATH") + def test_configured_aux_provider_absent_from_catalog_is_preserved(self): + """#7486: a configured provider missing from /api/models must survive Apply. + + The aux provider /