From 9706e251e0067370a48778013c8c112ab9e765b9 Mon Sep 17 00:00:00 2001 From: Ronald Reis Date: Thu, 2 Jul 2026 10:55:07 +0100 Subject: [PATCH 1/2] fix: limit desktop model pickers to explicit providers --- apps/desktop/src/app/chat/index.tsx | 5 +- .../src/app/settings/model-settings.test.tsx | 48 ++++--------------- .../src/app/shell/model-menu-panel.tsx | 6 ++- apps/desktop/src/components/model-picker.tsx | 3 +- .../components/model-visibility-dialog.tsx | 5 +- .../src/components/onboarding/flow.tsx | 2 +- .../src/components/onboarding/index.tsx | 2 +- apps/desktop/src/hermes.test.ts | 22 ++++++++- apps/desktop/src/hermes.ts | 22 ++++++++- apps/desktop/src/store/onboarding.ts | 2 +- hermes_cli/auth.py | 20 ++++++++ hermes_cli/inventory.py | 39 +++++++++++++++ hermes_cli/web_server.py | 22 +++++---- tests/hermes_cli/test_inventory.py | 38 +++++++++++++++ .../test_web_server_profile_unification.py | 30 ++++++++++++ tests/test_tui_gateway_server.py | 46 ++++++++++++++++++ tui_gateway/server.py | 11 +++-- 17 files changed, 260 insertions(+), 63 deletions(-) diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index a5216210a799..74eb8df86613 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -358,7 +358,10 @@ export function ChatView({ throw new Error('Hermes gateway unavailable') } - return gateway.request('model.options', { session_id: activeSessionId }) + return gateway.request('model.options', { + session_id: activeSessionId, + explicit_only: true + }) }, enabled: gatewayOpen }) diff --git a/apps/desktop/src/app/settings/model-settings.test.tsx b/apps/desktop/src/app/settings/model-settings.test.tsx index 405fb49cd727..135e9e268f2f 100644 --- a/apps/desktop/src/app/settings/model-settings.test.tsx +++ b/apps/desktop/src/app/settings/model-settings.test.tsx @@ -13,8 +13,10 @@ beforeAll(() => { const getGlobalModelInfo = vi.fn() const getGlobalModelOptions = vi.fn() const getAuxiliaryModels = vi.fn() +const getMoaModels = vi.fn() const setModelAssignment = vi.fn() const getRecommendedDefaultModel = vi.fn() +const saveMoaModels = vi.fn() const setEnvVar = vi.fn() const getHermesConfigRecord = vi.fn() const saveHermesConfig = vi.fn() @@ -24,8 +26,10 @@ vi.mock('@/hermes', () => ({ getGlobalModelInfo: () => getGlobalModelInfo(), getGlobalModelOptions: () => getGlobalModelOptions(), getAuxiliaryModels: () => getAuxiliaryModels(), + getMoaModels: () => getMoaModels(), setModelAssignment: (body: unknown) => setModelAssignment(body), getRecommendedDefaultModel: (slug: string) => getRecommendedDefaultModel(slug), + saveMoaModels: (body: unknown) => saveMoaModels(body), setEnvVar: (key: string, value: string) => setEnvVar(key, value), getHermesConfigRecord: () => getHermesConfigRecord(), saveHermesConfig: (config: unknown) => saveHermesConfig(config) @@ -45,15 +49,6 @@ beforeEach(() => { models: ['hermes-4', 'hermes-4-mini'], authenticated: true, capabilities: { 'hermes-4': { reasoning: true, fast: true } } - }, - // An unconfigured api_key provider — surfaced by the full-universe payload. - { - name: 'DeepSeek', - slug: 'deepseek', - models: [], - authenticated: false, - auth_type: 'api_key', - key_env: 'DEEPSEEK_API_KEY' } ] }) @@ -61,8 +56,9 @@ beforeEach(() => { main: { provider: 'nous', model: 'hermes-4' }, tasks: [{ task: 'vision', provider: 'auto', model: '', base_url: '' }] }) + getMoaModels.mockResolvedValue(null) setModelAssignment.mockResolvedValue({ provider: 'nous', model: 'hermes-4', gateway_tools: [] }) - getRecommendedDefaultModel.mockResolvedValue({ provider: 'deepseek', model: 'deepseek-chat', free_tier: null }) + getRecommendedDefaultModel.mockResolvedValue({ provider: 'nous', model: 'hermes-4', free_tier: null }) setEnvVar.mockResolvedValue({ ok: true }) getHermesConfigRecord.mockResolvedValue({ agent: { reasoning_effort: 'medium', service_tier: 'normal' } }) saveHermesConfig.mockResolvedValue({ ok: true }) @@ -80,43 +76,19 @@ async function renderModelSettings() { } describe('ModelSettings', () => { - it('loads the current main model and lists the full provider universe', async () => { + it('loads the current main model and lists configured providers only', async () => { await renderModelSettings() await waitFor(() => expect(getGlobalModelInfo).toHaveBeenCalled()) await waitFor(() => expect(getGlobalModelOptions).toHaveBeenCalled()) - // Open the provider Select — every provider from the full payload should be - // listed, including the unconfigured one with its "set up" hint. + // Open the provider Select — only configured providers should be listed. const triggers = await screen.findAllByRole('combobox') fireEvent.click(triggers[0]) - // "Nous" shows in both the trigger and the open list; the unconfigured - // provider + its setup hint are the unique signal of the full universe. + // "Nous" shows in both the trigger and the open list. expect((await screen.findAllByText('Nous')).length).toBeGreaterThan(0) - expect(await screen.findByText(/DeepSeek/)).toBeTruthy() - expect(await screen.findByText(/set up/)).toBeTruthy() - }) - - it('activates an unconfigured api_key provider inline by saving its key', async () => { - await renderModelSettings() - - await waitFor(() => expect(getGlobalModelOptions).toHaveBeenCalled()) - - // Open the provider Select and pick the unconfigured provider. - const triggers = screen.getAllByRole('combobox') - fireEvent.click(triggers[0]) - const deepseekOption = await screen.findByText(/DeepSeek/) - fireEvent.click(deepseekOption) - - // The inline key input appears for an api_key provider that needs setup. - const keyInput = await screen.findByPlaceholderText(/Paste DEEPSEEK_API_KEY/) - fireEvent.change(keyInput, { target: { value: 'sk-test-123' } }) - - const activate = await screen.findByRole('button', { name: /Activate/ }) - fireEvent.click(activate) - - await waitFor(() => expect(setEnvVar).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-test-123')) + expect(screen.queryByText(/DeepSeek/)).toBeNull() }) it('writes the profile default speed (service_tier) when the fast switch is toggled', async () => { diff --git a/apps/desktop/src/app/shell/model-menu-panel.tsx b/apps/desktop/src/app/shell/model-menu-panel.tsx index 1d1d620a067e..2b1c0c94311c 100644 --- a/apps/desktop/src/app/shell/model-menu-panel.tsx +++ b/apps/desktop/src/app/shell/model-menu-panel.tsx @@ -84,7 +84,10 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model queryKey: ['model-options', activeSessionId || 'global'], queryFn: (): Promise => { if (gateway && activeSessionId) { - return gateway.request('model.options', { session_id: activeSessionId }) + return gateway.request('model.options', { + session_id: activeSessionId, + explicit_only: true + }) } return getGlobalModelOptions() @@ -140,6 +143,7 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model gateway && activeSessionId ? await gateway.request('model.options', { session_id: activeSessionId, + explicit_only: true, refresh: true }) : await getGlobalModelOptions({ refresh: true }) diff --git a/apps/desktop/src/components/model-picker.tsx b/apps/desktop/src/components/model-picker.tsx index a4e77a6d9ed5..30a7e2cdccc2 100644 --- a/apps/desktop/src/components/model-picker.tsx +++ b/apps/desktop/src/components/model-picker.tsx @@ -57,7 +57,8 @@ export function ModelPickerDialog({ queryFn: () => { if (gw && sessionId) { return gw.request('model.options', { - session_id: sessionId + session_id: sessionId, + explicit_only: true }) } diff --git a/apps/desktop/src/components/model-visibility-dialog.tsx b/apps/desktop/src/components/model-visibility-dialog.tsx index 05a5e92cb3ae..4cc4aae66e42 100644 --- a/apps/desktop/src/components/model-visibility-dialog.tsx +++ b/apps/desktop/src/components/model-visibility-dialog.tsx @@ -44,7 +44,10 @@ export function ModelVisibilityDialog({ queryKey: ['model-options', sessionId || 'global'], queryFn: (): Promise => { if (gw && sessionId) { - return gw.request('model.options', { session_id: sessionId }) + return gw.request('model.options', { + session_id: sessionId, + explicit_only: true + }) } return getGlobalModelOptions() diff --git a/apps/desktop/src/components/onboarding/flow.tsx b/apps/desktop/src/components/onboarding/flow.tsx index 11cb3073a179..f3377be1af0b 100644 --- a/apps/desktop/src/components/onboarding/flow.tsx +++ b/apps/desktop/src/components/onboarding/flow.tsx @@ -252,7 +252,7 @@ function ConfirmingModelPanel({ // shows the same $/Mtok + Free/Pro info the picker and CLI do. const options = useQuery({ queryKey: ['onboarding-model-options', flow.providerSlug], - queryFn: () => getGlobalModelOptions() + queryFn: () => getGlobalModelOptions({ includeUnconfigured: true, explicitOnly: false }) }) const providerRow = options.data?.providers?.find( diff --git a/apps/desktop/src/components/onboarding/index.tsx b/apps/desktop/src/components/onboarding/index.tsx index fb61cb83eeb3..bfbc6cbf2920 100644 --- a/apps/desktop/src/components/onboarding/index.tsx +++ b/apps/desktop/src/components/onboarding/index.tsx @@ -99,7 +99,7 @@ function useApiKeyCatalog(): ApiKeyOption[] { // Promise.resolve().then so a synchronous throw (e.g. no desktop bridge in // tests) is funneled into the same .catch instead of escaping. void Promise.resolve() - .then(() => getGlobalModelOptions()) + .then(() => getGlobalModelOptions({ includeUnconfigured: true, explicitOnly: false })) .then(res => { if (!cancelled) { setRows(res.providers ?? []) diff --git a/apps/desktop/src/hermes.test.ts b/apps/desktop/src/hermes.test.ts index 290f6aac96da..513dddd6bf7a 100644 --- a/apps/desktop/src/hermes.test.ts +++ b/apps/desktop/src/hermes.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { getSessionMessages, listAllProfileSessions, listSessions } from './hermes' +import { getGlobalModelOptions, getSessionMessages, listAllProfileSessions, listSessions } from './hermes' const emptySessionsResponse = { limit: 0, @@ -57,4 +57,24 @@ describe('Hermes REST session helpers', () => { profile: 'xiaoxuxu' }) }) + + it('defaults model options to configured providers only', async () => { + await getGlobalModelOptions() + + expect(api).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/model/options?explicit_only=1' + }) + ) + }) + + it('can opt into unconfigured providers for onboarding flows', async () => { + await getGlobalModelOptions({ includeUnconfigured: true, refresh: true, explicitOnly: false }) + + expect(api).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/model/options?refresh=1&include_unconfigured=1' + }) + ) + }) }) diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index 5006ce417edc..4c962a2ece28 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -756,10 +756,28 @@ export function getUsageAnalytics(days = 30): Promise { }) } -export function getGlobalModelOptions(opts?: { refresh?: boolean }): Promise { +export function getGlobalModelOptions(opts?: { + refresh?: boolean + includeUnconfigured?: boolean + explicitOnly?: boolean +}): Promise { + const params = new URLSearchParams() + + if (opts?.refresh) { + params.set('refresh', '1') + } + + if (opts?.includeUnconfigured) { + params.set('include_unconfigured', '1') + } + + if (opts?.explicitOnly !== false) { + params.set('explicit_only', '1') + } + return window.hermesDesktop.api({ ...profileScoped(), - path: opts?.refresh ? '/api/model/options?refresh=1' : '/api/model/options' + path: params.size > 0 ? `/api/model/options?${params.toString()}` : '/api/model/options' }) } diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts index 9ef3754be7be..bf4ed6dc74e8 100644 --- a/apps/desktop/src/store/onboarding.ts +++ b/apps/desktop/src/store/onboarding.ts @@ -244,7 +244,7 @@ async function fetchProviderDefaultModel( let options try { - options = await getGlobalModelOptions() + options = await getGlobalModelOptions({ includeUnconfigured: true, explicitOnly: false }) } catch { return null } diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index fdd099bfa466..44885aea7cde 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -1493,6 +1493,26 @@ def is_provider_explicitly_configured(provider_id: str) -> bool: if has_usable_secret(os.getenv(env_var, "")): return True + # 4. Check persisted credential-pool entries that came from EXPLICIT flows + # the user initiated inside Hermes (manual add / device-code / PKCE), plus + # env-backed pool entries. This intentionally excludes ambient borrowed + # sources like gh_cli / claude_code / qwen-cli. + try: + for entry in read_credential_pool(normalized): + if not isinstance(entry, dict): + continue + source = str(entry.get("source") or "").strip().lower() + if not source: + continue + if ( + source.startswith("env:") + or source in {"device_code", "loopback_pkce", "hermes_pkce", "manual"} + or source.startswith("manual:") + ): + return True + except Exception: + pass + return False diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index 0914cfc0377b..5a5d82ed4753 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -111,6 +111,7 @@ def load_picker_context() -> ConfigContext: def build_models_payload( ctx: ConfigContext, *, + explicit_only: bool = False, include_unconfigured: bool = False, picker_hints: bool = False, canonical_order: bool = False, @@ -124,6 +125,10 @@ def build_models_payload( needs from a single substrate call. Flags: + - ``explicit_only``: keep only providers the user explicitly configured + (current provider, providers from config, or providers backed by + provider-specific env vars). This hides ambient / auto-seeded + credentials from desktop chat pickers. - ``include_unconfigured``: append ``CANONICAL_PROVIDERS`` rows that ``list_authenticated_providers`` didn't emit (TUI uses this to show the full provider universe in the picker). @@ -167,6 +172,9 @@ def build_models_payload( if moa_row is not None: rows = [moa_row] + [r for r in rows if str(r.get("slug", "")).lower() != "moa"] + if explicit_only: + rows = _filter_explicit_provider_rows(rows, ctx) + # --- Deduplicate: remove models from aggregators that overlap with # user-defined providers. When a local proxy (e.g. litellm-proxy) # serves a model whose name also appears in an aggregator's curated @@ -295,6 +303,37 @@ def _append_unconfigured_rows(rows: list[dict], ctx: ConfigContext) -> list[dict return extras +def _filter_explicit_provider_rows(rows: list[dict], ctx: ConfigContext) -> list[dict]: + """Keep only rows backed by explicit user configuration. + + ``list_authenticated_providers`` intentionally discovers ambient / auto- + seeded credentials (for example GitHub CLI -> Copilot). Desktop chat model + pickers want the narrower subset the user explicitly configured for Hermes. + """ + from hermes_cli.auth import is_provider_explicitly_configured + + current_slug = str(ctx.current_provider or "").strip().lower() + kept: list[dict] = [] + for row in rows: + slug = str(row.get("slug", "")).strip().lower() + if not slug: + continue + if row.get("is_user_defined"): + kept.append(row) + continue + if current_slug and slug == current_slug: + kept.append(row) + continue + if slug == "moa": + # MoA is a virtual routing mode, not an independently configured + # provider. Hide it from explicit-only pickers unless it is the + # current provider (handled above). + continue + if is_provider_explicitly_configured(slug): + kept.append(row) + return kept + + def _apply_picker_hints(rows: list[dict]) -> None: """Add ``authenticated``/``auth_type``/``key_env``/``warning`` per row. diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index ae53511d41b8..ed41bce85272 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -4162,7 +4162,12 @@ def get_model_info(profile: Optional[str] = None): @app.get("/api/model/options") -def get_model_options(profile: Optional[str] = None, refresh: bool = False): +def get_model_options( + profile: Optional[str] = None, + refresh: bool = False, + include_unconfigured: bool = False, + explicit_only: bool = False, +): """Return authenticated providers + their curated model lists. REST equivalent of the ``model.options`` JSON-RPC on tui_gateway, so the @@ -4181,18 +4186,15 @@ def get_model_options(profile: Optional[str] = None, refresh: bool = False): try: from hermes_cli.inventory import build_models_payload, load_picker_context - # include_unconfigured + picker_hints + canonical_order mirror the - # tui_gateway `model.options` JSON-RPC handler exactly, so every GUI - # surface fed by this endpoint (Settings → Model, the first-run - # onboarding picker) sees the SAME full provider universe `hermes model` - # exposes — not just the authenticated subset. Unconfigured providers - # come back as skeleton rows carrying `authenticated=False` + - # `auth_type`/`key_env`/`warning` so the GUI can render a setup - # affordance instead of hiding the provider entirely. + # Most desktop surfaces should only list providers the user has already + # configured. Onboarding opts into the full provider universe via + # include_unconfigured=1 so it can still render setup affordances for + # providers that are not yet authenticated. with _profile_scope(profile): return build_models_payload( load_picker_context(), - include_unconfigured=True, + explicit_only=bool(explicit_only), + include_unconfigured=bool(include_unconfigured), picker_hints=True, canonical_order=True, pricing=True, diff --git a/tests/hermes_cli/test_inventory.py b/tests/hermes_cli/test_inventory.py index d33c7ff651f8..21a22b6be3be 100644 --- a/tests/hermes_cli/test_inventory.py +++ b/tests/hermes_cli/test_inventory.py @@ -321,6 +321,44 @@ def test_include_unconfigured_skips_already_present_slugs(): assert or_rows[0]["models"] == ["m1"] # the authenticated row, not skeleton +def test_explicit_only_filters_ambient_credentials_but_keeps_current_and_custom_rows(): + rows = [ + {"slug": "openai-codex", "name": "OpenAI Codex", "models": ["gpt-5.4"], + "total_models": 1, "is_current": True, "is_user_defined": False, + "source": "hermes"}, + {"slug": "gemini", "name": "Gemini", "models": ["gemini-2.5-pro"], + "total_models": 1, "is_current": False, "is_user_defined": False, + "source": "built-in"}, + {"slug": "copilot", "name": "Copilot", "models": ["gpt-5.4"], + "total_models": 1, "is_current": False, "is_user_defined": False, + "source": "hermes"}, + {"slug": "nous", "name": "Nous", "models": ["anthropic/claude-sonnet-5"], + "total_models": 1, "is_current": False, "is_user_defined": False, + "source": "hermes"}, + {"slug": "custom:lab", "name": "Lab", "models": ["lab-1"], + "total_models": 1, "is_current": False, "is_user_defined": True, + "source": "user-config"}, + {"slug": "moa", "name": "MoA", "models": ["default"], + "total_models": 1, "is_current": False, "is_user_defined": False, + "source": "virtual"}, + ] + ctx = _empty_ctx(provider="openai-codex", model="gpt-5.4") + with ( + _list_auth_returning(rows), + patch( + "hermes_cli.auth.is_provider_explicitly_configured", + side_effect=lambda slug: slug == "gemini", + ), + ): + payload = build_models_payload(ctx, explicit_only=True) + + assert [row["slug"] for row in payload["providers"]] == [ + "openai-codex", + "gemini", + "custom:lab", + ] + + # ─── picker_hints ────────────────────────────────────────────────────── diff --git a/tests/hermes_cli/test_web_server_profile_unification.py b/tests/hermes_cli/test_web_server_profile_unification.py index 40dde33aed4b..9ca100864ef4 100644 --- a/tests/hermes_cli/test_web_server_profile_unification.py +++ b/tests/hermes_cli/test_web_server_profile_unification.py @@ -283,6 +283,36 @@ def test_model_options_unknown_profile_404(self, client, isolated_profiles): resp = client.get("/api/model/options", params={"profile": "ghost"}) assert resp.status_code == 404 + def test_model_options_hides_unconfigured_providers_by_default(self, client, monkeypatch): + calls = [] + + monkeypatch.setattr( + "hermes_cli.inventory.load_picker_context", + lambda: object(), + ) + + def _fake_build_models_payload(_ctx, **kwargs): + calls.append(kwargs) + return {"providers": [], "model": "", "provider": ""} + + monkeypatch.setattr( + "hermes_cli.inventory.build_models_payload", + _fake_build_models_payload, + ) + + resp = client.get("/api/model/options") + assert resp.status_code == 200 + assert calls[-1]["explicit_only"] is False + assert calls[-1]["include_unconfigured"] is False + + resp = client.get("/api/model/options", params={"explicit_only": "1"}) + assert resp.status_code == 200 + assert calls[-1]["explicit_only"] is True + + resp = client.get("/api/model/options", params={"include_unconfigured": "1"}) + assert resp.status_code == 200 + assert calls[-1]["include_unconfigured"] is True + def test_model_info_unknown_profile_404(self, client, isolated_profiles): """Regression: the broad except used to convert the 404 into a 200 with empty model info ("no model set" — silently wrong).""" diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 6d39a252cfe9..299998a44d91 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -5865,6 +5865,52 @@ def test_model_options_propagates_list_exception(monkeypatch): assert "catalog blew up" in resp["error"]["message"] +def test_model_options_hides_unconfigured_providers_by_default(monkeypatch): + from hermes_cli.inventory import ConfigContext + + calls = [] + + monkeypatch.setattr(server, "_resolve_model", lambda: "") + monkeypatch.setattr( + "hermes_cli.inventory.load_picker_context", + lambda: ConfigContext( + current_provider="", + current_model="", + current_base_url="", + user_providers={}, + custom_providers=[], + ), + ) + + def _fake_build_models_payload(_ctx, **kwargs): + calls.append(kwargs) + return {"providers": [], "model": "", "provider": ""} + + monkeypatch.setattr( + "hermes_cli.inventory.build_models_payload", + _fake_build_models_payload, + ) + + resp = server._methods["model.options"](99, {"session_id": ""}) + assert "result" in resp, resp + assert calls[-1]["explicit_only"] is False + assert calls[-1]["include_unconfigured"] is False + + resp = server._methods["model.options"]( + 100, + {"session_id": "", "explicit_only": True}, + ) + assert "result" in resp, resp + assert calls[-1]["explicit_only"] is True + + resp = server._methods["model.options"]( + 101, + {"session_id": "", "include_unconfigured": True}, + ) + assert "result" in resp, resp + assert calls[-1]["include_unconfigured"] is True + + # --------------------------------------------------------------------------- # prompt.submit — auto-title # --------------------------------------------------------------------------- diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 6bd1ed13ace4..0740dbf724be 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -12262,17 +12262,18 @@ def _(rid, params: dict) -> dict: ), current_base_url=getattr(agent, "base_url", "") if agent else "", ) - # picker_hints + canonical_order produce the TUI's required shape: + # picker_hints + canonical_order produce the TUI/desktop picker shape: # `authenticated`/`auth_type`/`key_env`/`warning` per row, in - # CANONICAL_PROVIDERS declaration order. include_unconfigured=True - # so the picker can show the full provider universe (with the - # setup-hint warning attached) instead of only authed rows. + # CANONICAL_PROVIDERS declaration order. Desktop pickers default to the + # configured subset; callers that need setup affordances can pass + # include_unconfigured=true explicitly. # Curated model lists are preserved — list_authenticated_providers # populates `models` from the curated catalog, not provider_model_ids # (which would pull non-agentic models like TTS/embeddings/etc.). payload = build_models_payload( ctx, - include_unconfigured=True, + explicit_only=bool(params.get("explicit_only")), + include_unconfigured=bool(params.get("include_unconfigured")), picker_hints=True, canonical_order=True, pricing=True, From d19cae88168675a334bda0eaebf41131429bf9f8 Mon Sep 17 00:00:00 2001 From: Ronald Reis Date: Thu, 2 Jul 2026 14:55:43 +0100 Subject: [PATCH 2/2] chore: map Ronald contributor email --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 17ab714e65db..f99ea3cf5d37 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -45,6 +45,7 @@ # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { + "ronaldrj@gmail.com": "rarf", # PR #56966 (desktop chat model picker: hide implicitly discovered providers unless explicitly configured) "jvsantos.cunha@gmail.com": "plcunha", # PR #55300 salvage (gateway: record child gateway peer metadata after a compression session-id rotation and repoint stale sessions.json compression-parent entries to the recovered live child; consolidated in the compression-routing-integrity salvage) "jakepresent1@gmail.com": "jakepresent", # PR #55721 salvage (gateway: identity-guard stale in-flight compression splits — a late run may publish its compressed child only if its run generation is still current and the session key still points at the run's original parent, so an old run can't overwrite a newer /new or moved binding) "zhangml@tech.icbc.com.cn": "zmlgit", # PR #54872 salvage (multiplex-profile kanban: route task notifications via the owning profile's adapter + wake the creator agent with a synthetic internal MessageEvent on terminal events)