🐛 fix: fix data inconsistency in ai provider config (#11198)

🐛 fix: fix ai provider api error
This commit is contained in:
Arvin Xu
2026-01-04 17:09:22 +08:00
committed by GitHub
parent 13f3725929
commit f8346f2440
7 changed files with 80 additions and 21 deletions

View File

@@ -4,8 +4,16 @@ import { cleanObject } from './object';
describe('cleanObject', () => {
it('should remove null, undefined and empty string fields', () => {
const input = { a: 1, b: null, c: undefined, d: '', e: 0, f: false } as const;
const input = {
a: 1,
b: null,
c: undefined,
d: '',
e: 0,
f: false,
abc: { d: undefined },
} as const;
const res = cleanObject({ ...input });
expect(res).toEqual({ a: 1, e: 0, f: false });
expect(res).toEqual({ a: 1, e: 0, f: false, abc: {} });
});
});

View File

@@ -155,7 +155,7 @@ const ProviderConfig = memo<ProviderConfigProps>(
isProviderEndpointNotEmpty,
isProviderApiKeyNotEmpty,
] = useAiInfraStore((s) => [
aiProviderSelectors.activeProviderConfig(s),
aiProviderSelectors.providerDetailById(id)(s),
s.updateAiProviderConfig,
aiProviderSelectors.isProviderEnabled(id)(s),
aiProviderSelectors.isAiProviderConfigLoading(id)(s),

View File

@@ -62,6 +62,7 @@ describe('aiModelSelectors', () => {
],
activeProviderModelList: [],
aiProviderConfigUpdatingIds: [],
aiProviderDetailMap: {},
aiProviderList: [],
aiProviderLoadingIds: [],
providerSearchKeyword: '',

View File

@@ -10,11 +10,13 @@ describe('aiProviderSelectors', () => {
{ id: 'provider3', enabled: true, sort: 0, source: 'builtin' },
{ id: 'custom1', enabled: false, sort: 3, source: 'custom' },
],
aiProviderDetail: {
id: 'provider1',
keyVaults: {
baseURL: 'https://api.example.com',
apiKey: 'test-key',
aiProviderDetailMap: {
provider1: {
id: 'provider1',
keyVaults: {
baseURL: 'https://api.example.com',
apiKey: 'test-key',
},
},
},
aiProviderLoadingIds: ['loading-provider'],
@@ -97,20 +99,37 @@ describe('aiProviderSelectors', () => {
});
});
describe('activeProviderConfig', () => {
it('should return active provider config', () => {
expect(aiProviderSelectors.activeProviderConfig(mockState)).toEqual(
mockState.aiProviderDetail,
describe('providerDetailById', () => {
it('should return provider detail by id', () => {
expect(aiProviderSelectors.providerDetailById('provider1')(mockState)).toEqual(
mockState.aiProviderDetailMap.provider1,
);
});
it('should return undefined for non-existing provider', () => {
expect(aiProviderSelectors.providerDetailById('non-existing')(mockState)).toBeUndefined();
});
});
describe('activeProviderConfig', () => {
it('should return active provider config from map', () => {
expect(aiProviderSelectors.activeProviderConfig(mockState)).toEqual(
mockState.aiProviderDetailMap.provider1,
);
});
it('should return undefined when no active provider', () => {
const stateWithoutActive = { ...mockState, activeAiProvider: undefined };
expect(aiProviderSelectors.activeProviderConfig(stateWithoutActive)).toBeUndefined();
});
});
describe('isAiProviderConfigLoading', () => {
it('should return true if provider id does not match active provider', () => {
it('should return true if provider is not in detail map (not loaded)', () => {
expect(aiProviderSelectors.isAiProviderConfigLoading('provider2')(mockState)).toBe(true);
});
it('should return false if provider id matches active provider', () => {
it('should return false if provider is in detail map (loaded)', () => {
expect(aiProviderSelectors.isAiProviderConfigLoading('provider1')(mockState)).toBe(false);
});
});
@@ -123,7 +142,9 @@ describe('aiProviderSelectors', () => {
it('should return false when no endpoint info exists', () => {
const stateWithoutEndpoint = {
...mockState,
aiProviderDetail: { keyVaults: {} },
aiProviderDetailMap: {
provider1: { id: 'provider1', keyVaults: {} },
},
};
expect(aiProviderSelectors.isActiveProviderEndpointNotEmpty(stateWithoutEndpoint)).toBe(
false,
@@ -139,7 +160,9 @@ describe('aiProviderSelectors', () => {
it('should return false when no api key exists', () => {
const stateWithoutApiKey = {
...mockState,
aiProviderDetail: { keyVaults: {} },
aiProviderDetailMap: {
provider1: { id: 'provider1', keyVaults: {} },
},
};
expect(aiProviderSelectors.isActiveProviderApiKeyNotEmpty(stateWithoutApiKey)).toBe(false);
});

View File

@@ -29,6 +29,7 @@ import {
type UpdateAiProviderParams,
} from '@/types/aiProvider';
export type ProviderModelListItem = {
abilities: ModelAbilities;
approximatePricePerImage?: number;
@@ -293,7 +294,14 @@ export const createAiProviderSlice: StateCreator<
onSuccess: (data) => {
if (!data) return;
set({ activeAiProvider: id, aiProviderDetail: data }, false, 'useFetchAiProviderItem');
set(
(state) => ({
activeAiProvider: id,
aiProviderDetailMap: { ...state.aiProviderDetailMap, [id]: data },
}),
false,
'useFetchAiProviderItem',
);
},
},
),

View File

@@ -12,7 +12,11 @@ export interface AIProviderState {
activeAiProvider?: string;
activeProviderModelList: any[];
aiProviderConfigUpdatingIds: string[];
aiProviderDetail?: AiProviderDetailItem | null;
/**
* Map of provider id to provider detail, used for caching provider details
* to avoid data inconsistency when switching providers
*/
aiProviderDetailMap: Record<string, AiProviderDetailItem>;
aiProviderList: AiProviderListItem[];
aiProviderLoadingIds: string[];
aiProviderRuntimeConfig: Record<string, AiProviderRuntimeConfig>;
@@ -29,6 +33,7 @@ export interface AIProviderState {
export const initialAIProviderState: AIProviderState = {
activeProviderModelList: [],
aiProviderConfigUpdatingIds: [],
aiProviderDetailMap: {},
aiProviderList: [],
aiProviderLoadingIds: [],
aiProviderRuntimeConfig: {},

View File

@@ -1,4 +1,5 @@
import { isProviderDisableBrowserRequest } from 'model-bank/modelProviders';
import { type AIProviderStoreState } from '@/store/aiInfra/initialState';
import { type AiProviderRuntimeConfig, AiProviderSourceEnum } from '@/types/aiProvider';
import { type GlobalLLMProviderKey } from '@/types/user/settings';
@@ -21,12 +22,24 @@ const isProviderEnabled = (id: string) => (s: AIProviderStoreState) =>
const isProviderLoading = (id: string) => (s: AIProviderStoreState) =>
s.aiProviderLoadingIds.includes(id);
const activeProviderConfig = (s: AIProviderStoreState) => s.aiProviderDetail;
// Detail
/**
* Get provider detail by id from the cache map
*/
const providerDetailById = (id: string) => (s: AIProviderStoreState) => s.aiProviderDetailMap[id];
/**
* Get active provider config from the cache map
*/
const activeProviderConfig = (s: AIProviderStoreState) =>
s.activeAiProvider ? s.aiProviderDetailMap[s.activeAiProvider] : undefined;
/**
* Check if provider config is loading (data not yet in cache)
*/
const isAiProviderConfigLoading = (id: string) => (s: AIProviderStoreState) =>
s.activeAiProvider !== id;
!s.aiProviderDetailMap[id];
const providerWhitelist = new Set(['ollama', 'lmstudio']);
@@ -134,5 +147,6 @@ export const aiProviderSelectors = {
isProviderHasBuiltinSearchConfig,
isProviderLoading,
providerConfigById,
providerDetailById,
providerKeyVaults,
};