From a4c5a85befea7c3da617efcdc01c51653aed6d90 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 7 Jun 2026 23:25:26 +0100 Subject: [PATCH] Keep Assistant route labels provider-explicit --- .../v6/internal/subsystems/ai-runtime.md | 13 +++++++---- .../AI/Chat/__tests__/AIChat.test.tsx | 23 +++++++++++++++++++ .../__tests__/aiProviderPresentation.test.ts | 4 ++++ .../src/utils/aiProviderPresentation.ts | 16 ++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 5c18cf3ec..34bf652db 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -1471,11 +1471,14 @@ timers when the stream already has a newer workflow/tool state. route. Assistant transcript rows must carry and render the effective model route that produced each live or restored response so route recovery and mixed-provider sessions remain auditable. Direct provider models must not - gain a gateway label. Gateway route strings must still render as readable - provider/model labels before catalog hydration; this mirrors OpenCode's - source pattern of retaining structured `providerID`/`modelID` metadata and - presenting catalog names with ID fallbacks rather than leaking raw route - storage IDs into primary chat chrome. Assistant model selector actions must + gain a gateway label, but explicit direct route strings such as + `deepseek:deepseek-v4-pro` must still render with provider identity rather + than collapsing to a bare-looking model payload. Gateway route strings must + still render as readable provider/model labels before catalog hydration; this + mirrors OpenCode's source pattern of retaining structured + `providerID`/`modelID` metadata and presenting catalog names with ID + fallbacks rather than leaking raw route storage IDs into primary chat chrome. + Assistant model selector actions must remain route-distinct: if the configured chat override resolves to the same route as the effective default or the already selected session model, the drawer must not render a duplicate override action. diff --git a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx index d433e56d1..38cbe53a6 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx @@ -4921,6 +4921,29 @@ describe('AIChat', () => { expect(mockChat.setModel).toHaveBeenCalledWith('openrouter:qwen/qwen3.7-plus'); }); + it('labels direct provider recent routes with provider identity', () => { + localStorage.setItem( + 'pulse:ai_chat_recent_models', + JSON.stringify(['deepseek:deepseek-v4-pro']), + ); + mockChat.model.mockReturnValue('openrouter:qwen/qwen3.7-plus'); + + renderChat(); + + fireEvent.click( + screen.getByRole('button', { + name: /Cycle recent Assistant model: DeepSeek: DeepSeek V4 Pro/, + }), + ); + + expect( + screen.queryByRole('button', { + name: 'Cycle recent Assistant model: deepseek-v4-pro', + }), + ).not.toBeInTheDocument(); + expect(mockChat.setModel).toHaveBeenCalledWith('deepseek:deepseek-v4-pro'); + }); + it('disables recent model cycling when the active route is the only recent route', () => { localStorage.setItem( 'pulse:ai_chat_recent_models', diff --git a/frontend-modern/src/utils/__tests__/aiProviderPresentation.test.ts b/frontend-modern/src/utils/__tests__/aiProviderPresentation.test.ts index 0626a5a47..6cbfe32f4 100644 --- a/frontend-modern/src/utils/__tests__/aiProviderPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/aiProviderPresentation.test.ts @@ -42,6 +42,10 @@ describe('aiProviderPresentation', () => { provider: 'deepseek', }), ).toBe('DeepSeek: DeepSeek V4 Pro'); + + expect(formatAIModelRouteLabel('deepseek:deepseek-v4-pro')).toBe( + 'DeepSeek: DeepSeek V4 Pro', + ); }); it('does not duplicate an existing OpenRouter route label', () => { diff --git a/frontend-modern/src/utils/aiProviderPresentation.ts b/frontend-modern/src/utils/aiProviderPresentation.ts index 8e6b4ee86..39ed8509b 100644 --- a/frontend-modern/src/utils/aiProviderPresentation.ts +++ b/frontend-modern/src/utils/aiProviderPresentation.ts @@ -191,12 +191,26 @@ const gatewayFallbackLabel = (model: AIModelRouteLabelInput): string => { return `${upstreamProvider}: ${modelLabel}`; }; +const directProviderRouteFallbackLabel = (model: AIModelRouteLabelInput): string => { + if (explicitModelNameForLabel(model)) return ''; + const id = modelIdForLabel(model); + if (isPulseOwnedLocalModelRoute(id)) return ''; + const separator = id.indexOf(':'); + if (separator <= 0 || separator === id.length - 1) return ''; + const provider = id.slice(0, separator); + if (GATEWAY_MODEL_PROVIDERS.has(provider)) return ''; + const providerName = getAIProviderDisplayName(provider); + const modelLabel = titleizeModelRouteId(id.slice(separator + 1)); + if (!providerName || !modelLabel) return ''; + return `${providerName}: ${modelLabel}`; +}; + export const formatAIModelRouteLabel = (model: AIModelRouteLabelInput): string => { const provider = transportProviderForLabel(model); const label = provider && GATEWAY_MODEL_PROVIDERS.has(provider) ? gatewayFallbackLabel(model) || baseModelLabel(model) - : baseModelLabel(model); + : directProviderRouteFallbackLabel(model) || baseModelLabel(model); if (!provider || !GATEWAY_MODEL_PROVIDERS.has(provider)) { return label; }