fix(desktop): include configured Fable models in model selector

This commit is contained in:
程序员阿江(Relakkes)
2026-09-28 02:29:56 +08:00
parent 7a82ce9aec
commit 2ec845d084
4 changed files with 52 additions and 11 deletions
@@ -114,13 +114,13 @@ describe('ModelSelector', () => {
providers: [{ providers: [{
id: 'provider-1m', presetId: 'custom', name: 'Provider 1M', id: 'provider-1m', presetId: 'custom', name: 'Provider 1M',
apiFormat: 'anthropic', apiKey: 'fixture', baseUrl: 'http://127.0.0.1:9999', apiFormat: 'anthropic', apiKey: 'fixture', baseUrl: 'http://127.0.0.1:9999',
models: { main: 'main-model', haiku: 'haiku-model', sonnet: 'sonnet-model', opus: 'opus-model' }, models: { main: 'main-model', fable: 'fable-model', haiku: 'haiku-model', sonnet: 'sonnet-model', opus: 'opus-model' },
model1mSupport: { main: enabled, fable: enabled, haiku: enabled, sonnet: enabled, opus: enabled }, model1mSupport: { main: enabled, fable: enabled, haiku: enabled, sonnet: enabled, opus: enabled },
}], }],
}) })
const runtimeChange = vi.fn() const runtimeChange = vi.fn()
render(<ModelSelector runtimeKey="__draft__" onRuntimeSelectionChange={runtimeChange} />) render(<ModelSelector runtimeKey="__draft__" onRuntimeSelectionChange={runtimeChange} />)
for (const slot of ['main', 'haiku', 'sonnet', 'opus']) { for (const slot of ['main', 'fable', 'haiku', 'sonnet', 'opus']) {
await clickByRole(/, Provider 1M$/) await clickByRole(/, Provider 1M$/)
fireEvent.click(within(screen.getByTestId('model-selector-dropdown')).getByRole('button', { name: new RegExp(`^${slot}-model`) })) fireEvent.click(within(screen.getByTestId('model-selector-dropdown')).getByRole('button', { name: new RegExp(`^${slot}-model`) }))
expect(runtimeChange).toHaveBeenLastCalledWith({ expect(runtimeChange).toHaveBeenLastCalledWith({
@@ -130,6 +130,34 @@ describe('ModelSelector', () => {
} }
}) })
it('finds a separately configured Fable model by provider, role and model ID', async () => {
useSettingsStore.setState({ locale: 'en' })
useProviderStore.setState({
activeId: 'relay', hasLoadedProviders: true, isLoading: false,
providers: [{
id: 'relay', presetId: 'custom', name: 'AruHub',
apiFormat: 'anthropic', apiKey: 'fixture', baseUrl: 'http://127.0.0.1:9999',
models: {
main: 'claude-opus-5-5', fable: 'claude-fable-5-1',
haiku: 'claude-opus-5-5', sonnet: 'claude-opus-5-5', opus: 'claude-opus-5-5',
},
}],
})
const runtimeChange = vi.fn()
render(<ModelSelector runtimeKey="__draft__" onRuntimeSelectionChange={runtimeChange} />)
await clickByRole(/, AruHub$/)
const dropdown = within(screen.getByTestId('model-selector-dropdown'))
const search = dropdown.getByRole('searchbox', { name: 'Search models' })
for (const query of ['Aru', 'Fable Model', 'claude-fable-5-1']) {
fireEvent.change(search, { target: { value: query } })
expect(dropdown.getByRole('button', { name: /claude-fable-5-1/ })).toBeInTheDocument()
}
fireEvent.click(dropdown.getByRole('button', { name: /claude-fable-5-1/ }))
expect(runtimeChange).toHaveBeenLastCalledWith(expect.objectContaining({
providerId: 'relay', modelId: 'claude-fable-5-1',
}))
})
it.each(['unknown', 'mixed', 'anthropic'] as const)( it.each(['unknown', 'mixed', 'anthropic'] as const)(
'allows cross-protocol selection despite retained %s session metadata', async (sessionApiFormat) => { 'allows cross-protocol selection despite retained %s session metadata', async (sessionApiFormat) => {
const sessionId = 'protocol-rollback-session' const sessionId = 'protocol-rollback-session'
@@ -1,3 +1,4 @@
import { MODEL_SLOTS, type ModelSlot } from '@/lib/providerModelContext'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react' import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { import {
@@ -138,14 +139,12 @@ function mergeOfficialModels(availableModels: ModelInfo[]): ModelInfo[] {
function buildProviderModels( function buildProviderModels(
provider: SavedProvider, provider: SavedProvider,
labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>, labels: Record<ModelSlot, string>,
): ModelInfo[] { ): ModelInfo[] {
const entries: Array<{ id: string; label: string }> = [ const entries = MODEL_SLOTS.map(slot => ({
{ id: resolveProviderSlotModelId(provider, 'main'), label: labels.main }, id: resolveProviderSlotModelId(provider, slot),
{ id: resolveProviderSlotModelId(provider, 'haiku'), label: labels.haiku }, label: labels[slot],
{ id: resolveProviderSlotModelId(provider, 'sonnet'), label: labels.sonnet }, }))
{ id: resolveProviderSlotModelId(provider, 'opus'), label: labels.opus },
]
const byId = new Map<string, { id: string; labels: string[] }>() const byId = new Map<string, { id: string; labels: string[] }>()
for (const entry of entries) { for (const entry of entries) {
@@ -187,7 +186,7 @@ function buildProviderChoices(
officialName: string, officialName: string,
openAIOfficialName: string, openAIOfficialName: string,
grokOfficialName: string, grokOfficialName: string,
labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>, labels: Record<ModelSlot, string>,
claudeOfficialLoggedIn: boolean, claudeOfficialLoggedIn: boolean,
openAIOfficialLoggedIn: boolean, openAIOfficialLoggedIn: boolean,
grokOfficialLoggedIn: boolean, grokOfficialLoggedIn: boolean,
@@ -397,6 +396,7 @@ export const ModelSelector = forwardRef<ModelSelectorHandle, Props>(function Mod
const roleLabels = useMemo( const roleLabels = useMemo(
() => ({ () => ({
main: t('settings.providers.mainModel'), main: t('settings.providers.mainModel'),
fable: t('settings.providers.fableModel'),
haiku: t('settings.providers.haikuModel'), haiku: t('settings.providers.haikuModel'),
sonnet: t('settings.providers.sonnetModel'), sonnet: t('settings.providers.sonnetModel'),
opus: t('settings.providers.opusModel'), opus: t('settings.providers.opusModel'),
+13
View File
@@ -153,6 +153,19 @@ describe('provider 1M runtime selection', () => {
model1mSupport: { main: true, fable: false, haiku: false, sonnet: true, opus: false }, model1mSupport: { main: true, fable: false, haiku: false, sonnet: true, opus: false },
} }
it.each([true, false, undefined])('resolves Fable 1M support %s for selection and restoration', (enabled) => {
const relay: SavedProvider = {
...provider,
models: { ...provider.models, fable: 'claude-fable-5-1[1m]' },
model1mSupport: enabled === undefined ? undefined : { ...provider.model1mSupport!, fable: enabled },
}
const modelId = enabled === false ? 'claude-fable-5-1' : 'claude-fable-5-1[1m]'
expect(resolveProviderSlotModelId(relay, 'fable')).toBe(modelId)
expect(resolveProviderRuntimeModelId(relay, 'claude-fable-5-1')).toBe(modelId)
expect(resolveDefaultRuntimeSelection(relay.id, relay.name, [relay], 'claude-fable-5-1'))
.toEqual({ providerId: relay.id, modelId })
})
it('waits for provider hydration before recovering a removed provider and preserves valid session choices', () => { it('waits for provider hydration before recovering a removed provider and preserves valid session choices', () => {
const selection = { providerId: 'deleted-provider', modelId: 'old-model', effortLevel: 'max' as const } const selection = { providerId: 'deleted-provider', modelId: 'old-model', effortLevel: 'max' as const }
const context = { activeId: provider.id, providers: [provider], hasLoadedProviders: false } const context = { activeId: provider.id, providers: [provider], hasLoadedProviders: false }
+1 -1
View File
@@ -31,7 +31,7 @@ export function resolveProviderSlotModelId(
slot: keyof SavedProvider['models'], slot: keyof SavedProvider['models'],
): string { ): string {
const modelId = provider.models[slot]?.trim() ?? '' const modelId = provider.models[slot]?.trim() ?? ''
const enabled = slot === 'fable' ? undefined : provider.model1mSupport?.[slot] const enabled = provider.model1mSupport?.[slot]
// Missing flags are legacy configuration: preserve explicit model suffixes. // Missing flags are legacy configuration: preserve explicit model suffixes.
if (!modelId || enabled === undefined) return modelId if (!modelId || enabled === undefined) return modelId
const baseModelId = baseProviderModelId(modelId) const baseModelId = baseProviderModelId(modelId)