fix: legacy usage

This commit is contained in:
linonetwo 2025-12-10 20:31:59 +08:00
parent 0bf3816d1c
commit 4169972094
11 changed files with 144 additions and 136 deletions

View file

@ -80,7 +80,7 @@ export const serviceInstances: {
} as Partial<IPreferenceService>;
})(),
externalAPI: {
getAIConfig: vi.fn(async () => ({ api: { model: 'test-model', provider: 'test-provider' }, modelParameters: {} })),
getAIConfig: vi.fn(async () => ({ default: { model: 'test-model', provider: 'test-provider' }, modelParameters: {} })),
getAIProviders: vi.fn(async () => []),
generateFromAI: vi.fn(async function*() {
// harmless await for linter

View file

@ -602,10 +602,13 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
id: 'test-agent',
agentDefId: 'test-agent-def',
aiApiConfig: {
api: {
default: {
provider: 'openai',
model: 'gpt-4',
embeddingModel: 'text-embedding-ada-002',
},
embedding: {
provider: 'openai',
model: 'text-embedding-ada-002',
},
modelParameters: {},
},
@ -665,10 +668,14 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
expect.any(String), // workspaceID
'How to use AI agents',
expect.objectContaining({
api: expect.objectContaining({
default: expect.objectContaining({
provider: 'openai',
model: 'gpt-4',
}),
embedding: expect.objectContaining({
provider: 'openai',
model: 'text-embedding-ada-002',
}),
}),
10,
0.7,
@ -701,7 +708,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
id: 'test-agent',
agentDefId: 'test-agent-def',
aiApiConfig: {
api: {
default: {
provider: 'openai',
model: 'gpt-4',
},
@ -772,7 +779,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
id: 'test-agent',
agentDefId: 'test-agent-def',
aiApiConfig: {
api: {
default: {
provider: 'openai',
model: 'gpt-4',
},

View file

@ -151,7 +151,7 @@ export default {
},
],
defaultConfig: {
api: {
default: {
provider: 'siliconflow',
model: 'Qwen/Qwen2.5-7B-Instruct',
},

View file

@ -353,8 +353,8 @@ export class ExternalAPIService implements IExternalAPIService {
async deleteFieldFromDefaultAIConfig(fieldPath: string): Promise<void> {
this.ensureSettingsLoaded();
// Support nested field deletion like 'api.embeddingModel'
const pathParts = fieldPath.split('.');
// Support field deletion like 'embedding', 'speech', 'default'
const parts = fieldPath.split('.');
let current: Record<string, unknown> = this.userSettings.defaultConfig;
// Navigate to the parent object

View file

@ -340,7 +340,7 @@ export interface IExternalAPIService {
/**
* Delete a field from default AI configuration
* @param fieldPath - Dot-separated path to the field (e.g., 'api.embeddingModel')
* @param fieldPath - Dot-separated path to the field (e.g., 'embedding', 'speech', 'default')
*/
deleteFieldFromDefaultAIConfig(fieldPath: string): Promise<void>;

View file

@ -51,11 +51,11 @@ describe('ExternalAPI Add Provider with Embedding Model', () => {
Object.defineProperty(window.service.externalAPI, 'getAIConfig', {
value: vi.fn().mockResolvedValue({
api: {
default: {
provider: 'existing-provider',
model: 'gpt-4o',
// No embeddingModel initially
},
// No embedding initially
modelParameters: {
temperature: 0.7,
systemPrompt: 'You are a helpful assistant.',
@ -87,7 +87,7 @@ describe('ExternalAPI Add Provider with Embedding Model', () => {
// Mock observables for externalAPI
const mockConfig: AiAPIConfig = {
api: {
default: {
provider: 'existing-provider',
model: 'gpt-4o',
},

View file

@ -23,21 +23,71 @@ const mockEmbeddingModel: ModelInfo = {
features: ['embedding' as ModelFeature],
};
const mockSpeechModel: ModelInfo = {
name: 'gpt-speech',
caption: 'GPT Speech',
features: ['speech' as ModelFeature],
};
const mockImageModel: ModelInfo = {
name: 'dall-e',
caption: 'DALL-E',
features: ['imageGeneration' as ModelFeature],
};
const mockTranscriptionsModel: ModelInfo = {
name: 'whisper',
caption: 'Whisper',
features: ['transcriptions' as ModelFeature],
};
const mockFreeModel: ModelInfo = {
name: 'gpt-free',
caption: 'GPT Free',
features: ['free' as ModelFeature],
};
const mockProvider: AIProviderConfig = {
provider: 'openai',
apiKey: 'sk-test',
baseURL: 'https://api.openai.com/v1',
models: [mockLanguageModel, mockEmbeddingModel],
models: [
mockLanguageModel,
mockEmbeddingModel,
mockSpeechModel,
mockImageModel,
mockTranscriptionsModel,
mockFreeModel,
],
providerClass: 'openai',
isPreset: false,
enabled: true,
};
const mockAIConfig = {
api: {
default: {
provider: 'openai',
model: 'gpt-4',
embeddingModel: 'text-embedding-3-small',
},
embedding: {
provider: 'openai',
model: 'text-embedding-3-small',
},
speech: {
provider: 'openai',
model: 'gpt-speech',
},
imageGeneration: {
provider: 'openai',
model: 'dall-e',
},
transcriptions: {
provider: 'openai',
model: 'whisper',
},
free: {
provider: 'openai',
model: 'gpt-free',
},
modelParameters: {
temperature: 0.7,
@ -155,11 +205,11 @@ describe('ExternalAPI Component', () => {
// Mock config with no embedding model
Object.defineProperty(window.service.externalAPI, 'getAIConfig', {
value: vi.fn().mockResolvedValue({
api: {
default: {
provider: 'openai',
model: 'gpt-4',
// No embeddingModel
},
// No embedding
modelParameters: {
temperature: 0.7,
systemPrompt: 'You are a helpful assistant.',
@ -180,12 +230,9 @@ describe('ExternalAPI Component', () => {
if (clearButton) {
await user.click(clearButton as HTMLElement);
// Verify both model and provider fields are deleted when no embedding model exists
// Verify default field is deleted
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.model');
});
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.provider');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('default');
});
// Also verify that handleConfigChange was called to update local state
@ -203,23 +250,25 @@ describe('ExternalAPI Component', () => {
// Verify the delete API was called
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.model');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.provider');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('default');
});
}
}
});
it('should only clear model field when embedding model exists', async () => {
it('should only clear default field when embedding model exists', async () => {
const user = userEvent.setup();
// Mock config with embedding model - this should preserve the provider
// Mock config with embedding model
Object.defineProperty(window.service.externalAPI, 'getAIConfig', {
value: vi.fn().mockResolvedValue({
api: {
default: {
provider: 'openai',
model: 'gpt-4',
embeddingModel: 'text-embedding-3-small', // Has embedding model
},
embedding: {
provider: 'openai',
model: 'text-embedding-3-small',
},
modelParameters: {
temperature: 0.7,
@ -241,14 +290,11 @@ describe('ExternalAPI Component', () => {
if (clearButton) {
await user.click(clearButton as HTMLElement);
// Should only delete model, NOT provider (because embedding model uses the provider)
// Should delete default field
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.model');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('default');
});
// Should NOT delete provider when embedding model exists
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).not.toHaveBeenCalledWith('api.provider');
// Verify that handleConfigChange was called
await waitFor(() => {
expect(window.service.externalAPI.updateDefaultAIConfig).toHaveBeenCalled();
@ -262,10 +308,8 @@ describe('ExternalAPI Component', () => {
await user.keyboard('{Escape}');
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.model');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('default');
});
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).not.toHaveBeenCalledWith('api.provider');
}
}
});
@ -285,7 +329,7 @@ describe('ExternalAPI Component', () => {
// Verify the delete API was called
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.embeddingModel');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('embedding');
});
// Also verify that handleConfigChange was called to update local state
@ -303,7 +347,7 @@ describe('ExternalAPI Component', () => {
// Verify the delete API was called
await waitFor(() => {
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('api.embeddingModel');
expect(window.service.externalAPI.deleteFieldFromDefaultAIConfig).toHaveBeenCalledWith('embedding');
});
}
}
@ -316,19 +360,15 @@ describe('ExternalAPI Component', () => {
// Create a simple test for ModelSelector clear functionality
const { ModelSelector } = await import('../components/ModelSelector');
const testConfig = {
api: {
provider: 'openai',
model: 'text-embedding-3-small',
embeddingModel: 'text-embedding-3-small',
},
modelParameters: {},
const testModel = {
provider: 'openai',
model: 'text-embedding-3-small',
};
render(
<TestWrapper>
<ModelSelector
selectedConfig={testConfig}
selectedModel={testModel}
modelOptions={[[mockProvider, mockEmbeddingModel]]}
onChange={vi.fn()}
onClear={mockOnClear}
@ -346,6 +386,34 @@ describe('ExternalAPI Component', () => {
}
});
it('should display default models from backend config on initial load', async () => {
await renderExternalAPI();
// Wait for all comboboxes to be rendered
const comboboxes = screen.getAllByRole('combobox');
// We have 6 model selectors (default, embedding, speech, imageGeneration, transcriptions, free)
expect(comboboxes).toHaveLength(6);
// Check that default model is displayed (first combobox)
expect(comboboxes[0]).toHaveValue('gpt-4');
// Check that embedding model is displayed (second combobox)
expect(comboboxes[1]).toHaveValue('text-embedding-3-small');
// Check that speech model is displayed (third combobox)
expect(comboboxes[2]).toHaveValue('gpt-speech');
// Check that image generation model is displayed (fourth combobox)
expect(comboboxes[3]).toHaveValue('dall-e');
// Check that transcriptions model is displayed (fifth combobox)
expect(comboboxes[4]).toHaveValue('whisper');
// Check that free model is displayed (sixth combobox)
expect(comboboxes[5]).toHaveValue('gpt-free');
});
it('should render provider configuration section', async () => {
await renderExternalAPI();

View file

@ -7,7 +7,7 @@ import { useAIConfigManagement } from '../useAIConfigManagement';
describe('useAIConfigManagement', () => {
const mockAIConfig: AiAPIConfig = {
api: {
default: {
provider: 'openai',
model: 'gpt-4',
},

View file

@ -1,32 +1,24 @@
import { Autocomplete } from '@mui/material';
import { AiAPIConfig } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { ModelSelection } from '@services/agentInstance/promptConcat/promptConcatSchema';
import { AIProviderConfig, ModelInfo } from '@services/externalAPI/interface';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { TextField } from '../../../PreferenceComponents';
interface ModelSelectorProps {
selectedConfig: AiAPIConfig | null;
selectedModel: ModelSelection | undefined;
modelOptions: Array<[AIProviderConfig, ModelInfo]>;
onChange: (provider: string, model: string) => void;
onClear?: () => void;
onlyShowEnabled?: boolean;
}
/**
* Type guard to check if config has api field
*/
const hasApiField = (config: AiAPIConfig | null): config is AiAPIConfig & { api: { provider: string; model: string } } => {
return config !== null && 'api' in config && typeof config.api === 'object' && config.api !== null &&
'provider' in config.api && 'model' in config.api;
};
export function ModelSelector({ selectedConfig, modelOptions, onChange, onClear, onlyShowEnabled }: ModelSelectorProps) {
export function ModelSelector({ selectedModel, modelOptions, onChange, onClear, onlyShowEnabled }: ModelSelectorProps) {
const { t } = useTranslation('agent');
const selectedValue = hasApiField(selectedConfig) && selectedConfig.api.model && selectedConfig.api.provider &&
selectedConfig.api.model !== '' && selectedConfig.api.provider !== ''
? modelOptions.find(m => m[0].provider === selectedConfig.api.provider && m[1].name === selectedConfig.api.model) || null
const selectedValue = selectedModel && selectedModel.model && selectedModel.provider &&
selectedModel.model !== '' && selectedModel.provider !== ''
? modelOptions.find(m => m[0].provider === selectedModel.provider && m[1].name === selectedModel.model) || null
: null;
const filteredModelOptions = onlyShowEnabled

View file

@ -62,7 +62,7 @@ describe('ProviderConfig Component', () => {
Object.defineProperty(window.service.externalAPI, 'getAIConfig', {
value: vi.fn().mockResolvedValue({
api: {
default: {
provider: 'openai',
model: 'gpt-4',
},
@ -194,7 +194,7 @@ describe('ProviderConfig Component', () => {
// Mock AI config to simulate no existing embedding model
Object.defineProperty(window.service.externalAPI, 'getAIConfig', {
value: vi.fn().mockResolvedValue({
api: {
default: {
provider: '',
model: '',
},

View file

@ -107,72 +107,13 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
await handleConfigChange(updatedConfig);
};
// Create default model config for ModelSelector
const defaultModelConfig = config && config.default
? {
api: {
provider: config.default.provider,
model: config.default.model,
},
modelParameters: config.modelParameters,
}
: null;
// Create embedding config from current AI config
// Use the provider that actually has the embedding model
const embeddingConfig = config && config.embedding
? {
api: {
provider: config.embedding.provider,
model: config.embedding.model,
},
modelParameters: config.modelParameters,
}
: null;
// Create speech config from current AI config
const speechConfig = config && config.speech
? {
api: {
provider: config.speech.provider,
model: config.speech.model,
},
modelParameters: config.modelParameters,
}
: null;
// Create image generation config from current AI config
const imageGenerationConfig = config && config.imageGeneration
? {
api: {
provider: config.imageGeneration.provider,
model: config.imageGeneration.model,
},
modelParameters: config.modelParameters,
}
: null;
// Create transcriptions config from current AI config
const transcriptionsConfig = config && config.transcriptions
? {
api: {
provider: config.transcriptions.provider,
model: config.transcriptions.model,
},
modelParameters: config.modelParameters,
}
: null;
// Create free model config from current AI config
const freeModelConfig = config && config.free
? {
api: {
provider: config.free.provider,
model: config.free.model,
},
modelParameters: config.modelParameters,
}
: null;
// Extract model selections directly from config
const defaultModelConfig = config?.default;
const embeddingConfig = config?.embedding;
const speechConfig = config?.speech;
const imageGenerationConfig = config?.imageGeneration;
const transcriptionsConfig = config?.transcriptions;
const freeModelConfig = config?.free;
const handleFreeModelClear = async () => {
if (!config) return;
@ -201,7 +142,7 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
secondary={t('Preference.DefaultAIModelSelectionDescription')}
/>
<ModelSelector
selectedConfig={defaultModelConfig}
selectedModel={defaultModelConfig}
modelOptions={providers.flatMap(provider =>
provider.models
.filter(model => Array.isArray(model.features) && model.features.includes('language'))
@ -218,7 +159,7 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
secondary={t('Preference.DefaultEmbeddingModelSelectionDescription')}
/>
<ModelSelector
selectedConfig={embeddingConfig}
selectedModel={embeddingConfig}
modelOptions={providers.flatMap(provider =>
provider.models
.filter(model => Array.isArray(model.features) && model.features.includes('embedding'))
@ -235,7 +176,7 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
secondary={t('Preference.DefaultSpeechModelSelectionDescription')}
/>
<ModelSelector
selectedConfig={speechConfig}
selectedModel={speechConfig}
modelOptions={providers.flatMap(provider =>
provider.models
.filter(model => Array.isArray(model.features) && model.features.includes('speech'))
@ -252,7 +193,7 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
secondary={t('Preference.DefaultImageGenerationModelSelectionDescription')}
/>
<ModelSelector
selectedConfig={imageGenerationConfig}
selectedModel={imageGenerationConfig}
modelOptions={providers.flatMap(provider =>
provider.models
.filter(model => Array.isArray(model.features) && model.features.includes('imageGeneration'))
@ -269,7 +210,7 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
secondary={t('Preference.DefaultTranscriptionsModelSelectionDescription')}
/>
<ModelSelector
selectedConfig={transcriptionsConfig}
selectedModel={transcriptionsConfig}
modelOptions={providers.flatMap(provider =>
provider.models
.filter(model => Array.isArray(model.features) && model.features.includes('transcriptions'))
@ -286,7 +227,7 @@ export function ExternalAPI(props: Partial<ISectionProps>): React.JSX.Element {
secondary={t('Preference.DefaultFreeModelSelectionDescription')}
/>
<ModelSelector
selectedConfig={freeModelConfig}
selectedModel={freeModelConfig}
modelOptions={providers.flatMap(provider =>
provider.models
.filter(model => Array.isArray(model.features) && model.features.includes('free'))