mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
fix(desktop): include configured Fable models in model selector
This commit is contained in:
@@ -114,13 +114,13 @@ describe('ModelSelector', () => {
|
||||
providers: [{
|
||||
id: 'provider-1m', presetId: 'custom', name: 'Provider 1M',
|
||||
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 },
|
||||
}],
|
||||
})
|
||||
const runtimeChange = vi.fn()
|
||||
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$/)
|
||||
fireEvent.click(within(screen.getByTestId('model-selector-dropdown')).getByRole('button', { name: new RegExp(`^${slot}-model`) }))
|
||||
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)(
|
||||
'allows cross-protocol selection despite retained %s session metadata', async (sessionApiFormat) => {
|
||||
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 { createPortal } from 'react-dom'
|
||||
import {
|
||||
@@ -138,14 +139,12 @@ function mergeOfficialModels(availableModels: ModelInfo[]): ModelInfo[] {
|
||||
|
||||
function buildProviderModels(
|
||||
provider: SavedProvider,
|
||||
labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>,
|
||||
labels: Record<ModelSlot, string>,
|
||||
): ModelInfo[] {
|
||||
const entries: Array<{ id: string; label: string }> = [
|
||||
{ id: resolveProviderSlotModelId(provider, 'main'), label: labels.main },
|
||||
{ id: resolveProviderSlotModelId(provider, 'haiku'), label: labels.haiku },
|
||||
{ id: resolveProviderSlotModelId(provider, 'sonnet'), label: labels.sonnet },
|
||||
{ id: resolveProviderSlotModelId(provider, 'opus'), label: labels.opus },
|
||||
]
|
||||
const entries = MODEL_SLOTS.map(slot => ({
|
||||
id: resolveProviderSlotModelId(provider, slot),
|
||||
label: labels[slot],
|
||||
}))
|
||||
|
||||
const byId = new Map<string, { id: string; labels: string[] }>()
|
||||
for (const entry of entries) {
|
||||
@@ -187,7 +186,7 @@ function buildProviderChoices(
|
||||
officialName: string,
|
||||
openAIOfficialName: string,
|
||||
grokOfficialName: string,
|
||||
labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>,
|
||||
labels: Record<ModelSlot, string>,
|
||||
claudeOfficialLoggedIn: boolean,
|
||||
openAIOfficialLoggedIn: boolean,
|
||||
grokOfficialLoggedIn: boolean,
|
||||
@@ -397,6 +396,7 @@ export const ModelSelector = forwardRef<ModelSelectorHandle, Props>(function Mod
|
||||
const roleLabels = useMemo(
|
||||
() => ({
|
||||
main: t('settings.providers.mainModel'),
|
||||
fable: t('settings.providers.fableModel'),
|
||||
haiku: t('settings.providers.haikuModel'),
|
||||
sonnet: t('settings.providers.sonnetModel'),
|
||||
opus: t('settings.providers.opusModel'),
|
||||
|
||||
@@ -153,6 +153,19 @@ describe('provider 1M runtime selection', () => {
|
||||
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', () => {
|
||||
const selection = { providerId: 'deleted-provider', modelId: 'old-model', effortLevel: 'max' as const }
|
||||
const context = { activeId: provider.id, providers: [provider], hasLoadedProviders: false }
|
||||
|
||||
@@ -31,7 +31,7 @@ export function resolveProviderSlotModelId(
|
||||
slot: keyof SavedProvider['models'],
|
||||
): string {
|
||||
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.
|
||||
if (!modelId || enabled === undefined) return modelId
|
||||
const baseModelId = baseProviderModelId(modelId)
|
||||
|
||||
Reference in New Issue
Block a user