refactor(tts,config,admin): update provider resolution logic and ordering for consistency
Remove fallbackProviderRef usage from settings and config context to streamline provider resolution. Adjust normalizeLegacyProviderRef to retain 'default-openai' unless a non-default fallback is explicitly provided. Update provider listing to enforce deterministic ordering by updatedAt, createdAt, and slug. Expand unit tests to verify provider ordering, legacy ref normalization, and selection logic.
This commit is contained in:
parent
b4f4d43d6a
commit
913c6d5d76
8 changed files with 139 additions and 10 deletions
|
|
@ -820,7 +820,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
const defaults = resolveProviderDefaults({
|
const defaults = resolveProviderDefaults({
|
||||||
providerRef: runtimeConfig.defaultTtsProvider,
|
providerRef: runtimeConfig.defaultTtsProvider,
|
||||||
sharedProviders,
|
sharedProviders,
|
||||||
fallbackProviderRef: 'custom-openai',
|
|
||||||
});
|
});
|
||||||
setLocalApiKey('');
|
setLocalApiKey('');
|
||||||
setLocalBaseUrl('');
|
setLocalBaseUrl('');
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
providerRef: APP_CONFIG_DEFAULTS.providerRef,
|
providerRef: APP_CONFIG_DEFAULTS.providerRef,
|
||||||
providerType: APP_CONFIG_DEFAULTS.providerType,
|
providerType: APP_CONFIG_DEFAULTS.providerType,
|
||||||
sharedProviders,
|
sharedProviders,
|
||||||
fallbackProviderRef: 'custom-openai',
|
|
||||||
});
|
});
|
||||||
}, [sharedProviders]);
|
}, [sharedProviders]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { and, eq } from 'drizzle-orm';
|
import { and, asc, desc, eq } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { adminProviders } from '@/db/schema';
|
import { adminProviders } from '@/db/schema';
|
||||||
import { apiKeyLast4, decryptSecret, encryptSecret } from '@/lib/server/crypto/secrets';
|
import { apiKeyLast4, decryptSecret, encryptSecret } from '@/lib/server/crypto/secrets';
|
||||||
|
|
@ -184,7 +184,14 @@ function assertInstructionsCompatibility(model: string | null, instructions: str
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAdminProviders(): Promise<AdminProviderRecord[]> {
|
export async function listAdminProviders(): Promise<AdminProviderRecord[]> {
|
||||||
const rows = await db.select().from(adminProviders);
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(adminProviders)
|
||||||
|
.orderBy(
|
||||||
|
desc(adminProviders.updatedAt),
|
||||||
|
desc(adminProviders.createdAt),
|
||||||
|
asc(adminProviders.slug),
|
||||||
|
);
|
||||||
return (rows as Array<Record<string, unknown>>).map(rowToRecord);
|
return (rows as Array<Record<string, unknown>>).map(rowToRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ export function normalizeLegacyProviderRef(
|
||||||
if (!raw) return '';
|
if (!raw) return '';
|
||||||
if (raw !== 'default-openai') return raw;
|
if (raw !== 'default-openai') return raw;
|
||||||
const fallback = typeof fallbackProviderRef === 'string' ? fallbackProviderRef.trim() : '';
|
const fallback = typeof fallbackProviderRef === 'string' ? fallbackProviderRef.trim() : '';
|
||||||
return fallback || 'custom-openai';
|
if (fallback && fallback !== 'default-openai') return fallback;
|
||||||
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProviderDefaults(input: {
|
export function resolveProviderDefaults(input: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { DocumentListState } from '@/types/documents';
|
import type { DocumentListState } from '@/types/documents';
|
||||||
import { isBuiltInTtsProviderId, type TtsProviderType } from '@/lib/shared/tts-provider-catalog';
|
import { isBuiltInTtsProviderId, type TtsProviderType } from '@/lib/shared/tts-provider-catalog';
|
||||||
import { defaultModelForProviderType, normalizeLegacyProviderRef } from '@/lib/shared/tts-provider-policy';
|
import { defaultModelForProviderType } from '@/lib/shared/tts-provider-policy';
|
||||||
|
|
||||||
// Runtime config (admin-controlled) is layered on top of the static defaults
|
// Runtime config (admin-controlled) is layered on top of the static defaults
|
||||||
// below. We resolve it lazily so this module stays importable from non-React
|
// below. We resolve it lazily so this module stays importable from non-React
|
||||||
|
|
@ -96,7 +96,7 @@ export interface AppConfigValues {
|
||||||
export function getAppConfigDefaults(): AppConfigValues {
|
export function getAppConfigDefaults(): AppConfigValues {
|
||||||
const wordHighlightEnabledByDefault = readRuntimeFlag('enableWordHighlight', true);
|
const wordHighlightEnabledByDefault = readRuntimeFlag('enableWordHighlight', true);
|
||||||
const runtimeProviderRef = readRuntimeString('defaultTtsProvider', 'custom-openai');
|
const runtimeProviderRef = readRuntimeString('defaultTtsProvider', 'custom-openai');
|
||||||
const defaultProviderRef = normalizeLegacyProviderRef(runtimeProviderRef, 'custom-openai');
|
const defaultProviderRef = runtimeProviderRef.trim();
|
||||||
const defaultProviderType = isBuiltInTtsProviderId(defaultProviderRef) ? defaultProviderRef : 'unknown';
|
const defaultProviderType = isBuiltInTtsProviderId(defaultProviderRef) ? defaultProviderRef : 'unknown';
|
||||||
const defaultModel = isBuiltInTtsProviderId(defaultProviderType)
|
const defaultModel = isBuiltInTtsProviderId(defaultProviderType)
|
||||||
? defaultModelForProviderType(defaultProviderType)
|
? defaultModelForProviderType(defaultProviderType)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { inArray } from 'drizzle-orm';
|
||||||
|
import { db } from '../../src/db';
|
||||||
|
import { adminProviders } from '../../src/db/schema';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AdminProviderError,
|
AdminProviderError,
|
||||||
|
listAdminProviders,
|
||||||
toMasked,
|
toMasked,
|
||||||
validateProviderType,
|
validateProviderType,
|
||||||
validateSlug,
|
validateSlug,
|
||||||
|
|
@ -48,5 +52,87 @@ test.describe('admin provider validation', () => {
|
||||||
|
|
||||||
expect(toMasked(record).apiKeyMask).toBe('••••abcd');
|
expect(toMasked(record).apiKeyMask).toBe('••••abcd');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
test('lists providers in deterministic updated/created/slug order', async () => {
|
||||||
|
const suffix = `${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const inserted = [
|
||||||
|
{
|
||||||
|
id: `prov-aaa-${suffix}`,
|
||||||
|
slug: `ord-aaa-${suffix}`,
|
||||||
|
displayName: 'Order AAA',
|
||||||
|
providerType: 'openai' as const,
|
||||||
|
baseUrl: null,
|
||||||
|
apiKeyCiphertext: 'cipher',
|
||||||
|
apiKeyIv: 'iv',
|
||||||
|
apiKeyLast4: '1111',
|
||||||
|
defaultModel: null,
|
||||||
|
defaultInstructions: null,
|
||||||
|
enabled: 1,
|
||||||
|
createdAt: 200,
|
||||||
|
updatedAt: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `prov-zzz-${suffix}`,
|
||||||
|
slug: `ord-zzz-${suffix}`,
|
||||||
|
displayName: 'Order ZZZ',
|
||||||
|
providerType: 'openai' as const,
|
||||||
|
baseUrl: null,
|
||||||
|
apiKeyCiphertext: 'cipher',
|
||||||
|
apiKeyIv: 'iv',
|
||||||
|
apiKeyLast4: '2222',
|
||||||
|
defaultModel: null,
|
||||||
|
defaultInstructions: null,
|
||||||
|
enabled: 1,
|
||||||
|
createdAt: 200,
|
||||||
|
updatedAt: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `prov-mid-${suffix}`,
|
||||||
|
slug: `ord-mid-${suffix}`,
|
||||||
|
displayName: 'Order MID',
|
||||||
|
providerType: 'openai' as const,
|
||||||
|
baseUrl: null,
|
||||||
|
apiKeyCiphertext: 'cipher',
|
||||||
|
apiKeyIv: 'iv',
|
||||||
|
apiKeyLast4: '3333',
|
||||||
|
defaultModel: null,
|
||||||
|
defaultInstructions: null,
|
||||||
|
enabled: 1,
|
||||||
|
createdAt: 150,
|
||||||
|
updatedAt: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `prov-old-${suffix}`,
|
||||||
|
slug: `ord-old-${suffix}`,
|
||||||
|
displayName: 'Order OLD',
|
||||||
|
providerType: 'openai' as const,
|
||||||
|
baseUrl: null,
|
||||||
|
apiKeyCiphertext: 'cipher',
|
||||||
|
apiKeyIv: 'iv',
|
||||||
|
apiKeyLast4: '4444',
|
||||||
|
defaultModel: null,
|
||||||
|
defaultInstructions: null,
|
||||||
|
enabled: 1,
|
||||||
|
createdAt: 999,
|
||||||
|
updatedAt: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.insert(adminProviders).values(inserted);
|
||||||
|
const listed = await listAdminProviders();
|
||||||
|
const ordered = listed
|
||||||
|
.filter((row) => row.slug.endsWith(suffix))
|
||||||
|
.map((row) => row.slug);
|
||||||
|
|
||||||
|
expect(ordered).toEqual([
|
||||||
|
`ord-aaa-${suffix}`,
|
||||||
|
`ord-zzz-${suffix}`,
|
||||||
|
`ord-mid-${suffix}`,
|
||||||
|
`ord-old-${suffix}`,
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
await db.delete(adminProviders).where(inArray(adminProviders.id, inserted.map((row) => row.id)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,20 @@ test.describe('tts provider catalog', () => {
|
||||||
expect(supportsNativeModelSpeed('replicate', 'qwen/qwen3-tts')).toBe(false);
|
expect(supportsNativeModelSpeed('replicate', 'qwen/qwen3-tts')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('normalizes legacy default-openai provider ref to fallback', () => {
|
test('preserves default-openai unless explicit non-default fallback is provided', () => {
|
||||||
expect(normalizeLegacyProviderRef('default-openai', 'shared-replicate')).toBe('shared-replicate');
|
expect(normalizeLegacyProviderRef('default-openai', 'shared-replicate')).toBe('shared-replicate');
|
||||||
expect(normalizeLegacyProviderRef('default-openai', '')).toBe('custom-openai');
|
expect(normalizeLegacyProviderRef('default-openai', '')).toBe('default-openai');
|
||||||
|
expect(normalizeLegacyProviderRef('default-openai', 'default-openai')).toBe('default-openai');
|
||||||
|
expect(normalizeLegacyProviderRef('default-openai')).toBe('default-openai');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveProviderDefaults keeps runtime/admin default provider ref when valid', () => {
|
||||||
|
const defaults = resolveProviderDefaults({
|
||||||
|
providerRef: 'default-openai',
|
||||||
|
providerType: 'unknown',
|
||||||
|
});
|
||||||
|
expect(defaults.providerRef).toBe('default-openai');
|
||||||
|
expect(defaults.providerType).toBe('unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolves shared-provider default model as authoritative', () => {
|
test('resolves shared-provider default model as authoritative', () => {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,32 @@ const SHARED: SharedProviderEntry[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
test.describe('resolveTtsSettingsViewModel (admin/shared modes)', () => {
|
test.describe('resolveTtsSettingsViewModel (admin/shared modes)', () => {
|
||||||
|
test('keeps default-openai selection when that shared provider exists', () => {
|
||||||
|
const vm = resolveTtsSettingsViewModel({
|
||||||
|
providerRef: 'default-openai',
|
||||||
|
providerType: 'unknown',
|
||||||
|
modelValue: 'kokoro',
|
||||||
|
customModelInput: '',
|
||||||
|
showAllDeepInfra: false,
|
||||||
|
showAllProviderModels: true,
|
||||||
|
sharedProviders: [
|
||||||
|
{
|
||||||
|
slug: 'default-openai',
|
||||||
|
displayName: 'Default OpenAI',
|
||||||
|
providerType: 'openai',
|
||||||
|
defaultModel: 'gpt-4o-mini-tts',
|
||||||
|
defaultInstructions: null,
|
||||||
|
},
|
||||||
|
...SHARED,
|
||||||
|
],
|
||||||
|
allowBuiltInProviders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(vm.selectedProviderRef).toBe('default-openai');
|
||||||
|
expect(vm.selectedSharedProvider?.slug).toBe('default-openai');
|
||||||
|
expect(vm.selectedModelId).toBe('gpt-4o-mini-tts');
|
||||||
|
});
|
||||||
|
|
||||||
test('restrict mode exposes only shared providers', () => {
|
test('restrict mode exposes only shared providers', () => {
|
||||||
const vm = resolveTtsSettingsViewModel({
|
const vm = resolveTtsSettingsViewModel({
|
||||||
providerRef: 'shared-openai',
|
providerRef: 'shared-openai',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue