1- import { IModelManager , ModelConfig , TextModelConfig } from './types' ;
1+ import { IModelManager , ModelConfig , TextModel , TextModelConfig , TextProvider } from './types' ;
22import { IStorageProvider } from '../storage/types' ;
33import { StorageAdapter } from '../storage/adapter' ;
44import { getAllModels , getBuiltinModelIds } from './defaults' ;
@@ -285,6 +285,252 @@ export class ModelManager implements IModelManager {
285285 }
286286 }
287287
288+ private isRecord ( value : unknown ) : value is Record < string , any > {
289+ return typeof value === 'object' && value !== null && ! Array . isArray ( value )
290+ }
291+
292+ private toOptionalString ( value : unknown ) : string | undefined {
293+ return typeof value === 'string' && value . trim ( ) . length > 0 ? value : undefined
294+ }
295+
296+ private getProviderIdFromRaw ( raw : Record < string , any > ) : string | undefined {
297+ const providerId = this . toOptionalString ( raw . providerId )
298+ || this . toOptionalString ( raw . providerMeta ?. id )
299+ || this . toOptionalString ( raw . modelMeta ?. providerId )
300+ || this . toOptionalString ( raw . provider )
301+
302+ return providerId === 'custom' ? 'openai-compatible' : providerId
303+ }
304+
305+ private getModelIdFromRaw ( raw : Record < string , any > ) : string | undefined {
306+ return this . toOptionalString ( raw . modelId )
307+ || this . toOptionalString ( raw . modelMeta ?. id )
308+ || this . toOptionalString ( raw . defaultModel )
309+ }
310+
311+ private createDisabledPlaceholderConfig (
312+ key : string ,
313+ rawConfig : unknown ,
314+ error ?: unknown
315+ ) : TextModelConfig {
316+ const raw = this . isRecord ( rawConfig ) ? rawConfig : { }
317+ const providerId = this . getProviderIdFromRaw ( raw ) || 'unknown'
318+ const modelId = this . toOptionalString ( raw . modelId )
319+ || this . toOptionalString ( raw . modelMeta ?. id )
320+ || this . toOptionalString ( raw . defaultModel )
321+ || 'unknown'
322+ const providerMeta = this . resolvePlaceholderProviderMeta ( providerId , raw . providerMeta )
323+ const modelMeta = this . resolvePlaceholderModelMeta ( providerId , modelId , raw . modelMeta )
324+ const connectionConfig = this . isRecord ( raw . connectionConfig )
325+ ? { ...raw . connectionConfig }
326+ : {
327+ ...( this . toOptionalString ( raw . apiKey ) ? { apiKey : raw . apiKey } : { } ) ,
328+ ...( this . toOptionalString ( raw . baseURL ) ? { baseURL : raw . baseURL } : { } )
329+ }
330+ const paramOverrides = this . isRecord ( raw . paramOverrides )
331+ ? { ...raw . paramOverrides }
332+ : this . isRecord ( raw . llmParams )
333+ ? { ...raw . llmParams }
334+ : { }
335+
336+ console . warn (
337+ `[ModelManager] Failed to normalize model config ${ key } , returning disabled placeholder:` ,
338+ error
339+ )
340+
341+ return {
342+ id : this . toOptionalString ( raw . id ) || key ,
343+ name : this . toOptionalString ( raw . name ) || `Invalid Model (${ key } )` ,
344+ enabled : false ,
345+ providerId,
346+ modelId,
347+ providerMeta,
348+ modelMeta,
349+ connectionConfig,
350+ paramOverrides
351+ }
352+ }
353+
354+ private buildDefaultBackfilledConfig (
355+ key : string ,
356+ rawConfig : unknown
357+ ) : TextModelConfig | null {
358+ if ( ! this . isRecord ( rawConfig ) ) {
359+ return null
360+ }
361+
362+ const defaultConfig = this . getDefaultModels ( ) [ key ]
363+ if ( ! defaultConfig ) {
364+ return null
365+ }
366+
367+ const providerId = this . getProviderIdFromRaw ( rawConfig )
368+ || defaultConfig . providerId
369+ || defaultConfig . providerMeta . id
370+ const modelId = this . getModelIdFromRaw ( rawConfig )
371+ || defaultConfig . modelId
372+ || defaultConfig . modelMeta . id
373+ const repairedConfig = {
374+ ...defaultConfig ,
375+ ...rawConfig ,
376+ id : this . toOptionalString ( rawConfig . id ) || defaultConfig . id ,
377+ name : this . toOptionalString ( rawConfig . name ) || defaultConfig . name ,
378+ enabled : typeof rawConfig . enabled === 'boolean' ? rawConfig . enabled : defaultConfig . enabled ,
379+ providerId,
380+ modelId,
381+ providerMeta : this . isRecord ( rawConfig . providerMeta ) && rawConfig . providerMeta . id === providerId
382+ ? rawConfig . providerMeta as TextProvider
383+ : defaultConfig . providerMeta ,
384+ modelMeta : this . isRecord ( rawConfig . modelMeta ) && rawConfig . modelMeta . id === modelId && rawConfig . modelMeta . providerId === providerId
385+ ? rawConfig . modelMeta as TextModel
386+ : defaultConfig . modelMeta ,
387+ connectionConfig : this . isRecord ( rawConfig . connectionConfig )
388+ ? { ...defaultConfig . connectionConfig , ...rawConfig . connectionConfig }
389+ : { ...defaultConfig . connectionConfig } ,
390+ paramOverrides : this . isRecord ( rawConfig . paramOverrides )
391+ ? { ...rawConfig . paramOverrides }
392+ : defaultConfig . paramOverrides ,
393+ customParamOverrides : this . isRecord ( rawConfig . customParamOverrides )
394+ ? { ...rawConfig . customParamOverrides }
395+ : defaultConfig . customParamOverrides
396+ }
397+
398+ return this . migrateConfig ( repairedConfig )
399+ }
400+
401+ private resolvePlaceholderProviderMeta (
402+ providerId : string ,
403+ existingProviderMeta : unknown
404+ ) : TextProvider {
405+ if ( this . isRecord ( existingProviderMeta ) && existingProviderMeta . id === providerId ) {
406+ return {
407+ id : providerId ,
408+ name : this . toOptionalString ( existingProviderMeta . name ) || providerId ,
409+ description : this . toOptionalString ( existingProviderMeta . description ) ,
410+ corsRestricted : existingProviderMeta . corsRestricted ,
411+ requiresApiKey : ! ! existingProviderMeta . requiresApiKey ,
412+ defaultBaseURL : this . toOptionalString ( existingProviderMeta . defaultBaseURL ) || '' ,
413+ supportsDynamicModels : ! ! existingProviderMeta . supportsDynamicModels ,
414+ connectionSchema : this . isRecord ( existingProviderMeta . connectionSchema )
415+ ? existingProviderMeta . connectionSchema as TextProvider [ 'connectionSchema' ]
416+ : { required : [ ] , optional : [ ] , fieldTypes : { } } ,
417+ apiKeyUrl : this . toOptionalString ( existingProviderMeta . apiKeyUrl )
418+ }
419+ }
420+
421+ try {
422+ return this . registry ?. getAdapter ( providerId ) . getProvider ( ) || this . getUnknownProviderMeta ( providerId )
423+ } catch {
424+ return this . getUnknownProviderMeta ( providerId )
425+ }
426+ }
427+
428+ private resolvePlaceholderModelMeta (
429+ providerId : string ,
430+ modelId : string ,
431+ existingModelMeta : unknown
432+ ) : TextModel {
433+ if (
434+ this . isRecord ( existingModelMeta ) &&
435+ existingModelMeta . id === modelId &&
436+ existingModelMeta . providerId === providerId
437+ ) {
438+ return {
439+ id : modelId ,
440+ name : this . toOptionalString ( existingModelMeta . name ) || modelId ,
441+ description : this . toOptionalString ( existingModelMeta . description ) ,
442+ providerId,
443+ capabilities : this . isRecord ( existingModelMeta . capabilities )
444+ ? existingModelMeta . capabilities as TextModel [ 'capabilities' ]
445+ : { supportsTools : false } ,
446+ parameterDefinitions : Array . isArray ( existingModelMeta . parameterDefinitions )
447+ ? existingModelMeta . parameterDefinitions
448+ : [ ] ,
449+ defaultParameterValues : this . isRecord ( existingModelMeta . defaultParameterValues )
450+ ? existingModelMeta . defaultParameterValues
451+ : { }
452+ }
453+ }
454+
455+ try {
456+ const adapter = this . registry ?. getAdapter ( providerId )
457+ return adapter ?. getModels ( ) . find ( ( model ) => model . id === modelId )
458+ || adapter ?. buildDefaultModel ( modelId )
459+ || this . getUnknownModelMeta ( providerId , modelId )
460+ } catch {
461+ return this . getUnknownModelMeta ( providerId , modelId )
462+ }
463+ }
464+
465+ private getUnknownProviderMeta ( providerId : string ) : TextProvider {
466+ return {
467+ id : providerId ,
468+ name : providerId === 'unknown' ? 'Unknown Provider' : `Unknown Provider (${ providerId } )` ,
469+ description : 'This configuration could not be normalized. Please edit or delete it.' ,
470+ requiresApiKey : false ,
471+ defaultBaseURL : '' ,
472+ supportsDynamicModels : false ,
473+ connectionSchema : { required : [ ] , optional : [ ] , fieldTypes : { } }
474+ }
475+ }
476+
477+ private getUnknownModelMeta ( providerId : string , modelId : string ) : TextModel {
478+ return {
479+ id : modelId ,
480+ name : modelId === 'unknown' ? 'Unknown Model' : `Unknown Model (${ modelId } )` ,
481+ description : 'This configuration could not be normalized. Please edit or delete it.' ,
482+ providerId,
483+ capabilities : {
484+ supportsTools : false
485+ } ,
486+ parameterDefinitions : [ ] ,
487+ defaultParameterValues : { }
488+ }
489+ }
490+
491+ private normalizeStoredTextModelConfig (
492+ key : string ,
493+ rawConfig : unknown ,
494+ registry : ITextAdapterRegistry
495+ ) : TextModelConfig {
496+ try {
497+ let textConfig : TextModelConfig
498+
499+ if ( isTextModelConfig ( rawConfig ) ) {
500+ textConfig = rawConfig as TextModelConfig ;
501+ } else if ( isLegacyConfig ( rawConfig ) ) {
502+ textConfig = convertLegacyToTextModelConfig ( key , rawConfig ) ;
503+ } else {
504+ textConfig = convertLegacyToTextModelConfig ( key , rawConfig as ModelConfig ) ;
505+ }
506+
507+ const migrated = this . migrateConfig ( textConfig )
508+ return this . normalizeTextModelConfig (
509+ this . patchProviderMeta ( this . patchDeepseekConfig ( migrated ) ) ,
510+ registry ,
511+ { allowLegacyMetadataMismatch : true }
512+ )
513+ } catch ( error ) {
514+ const defaultBackfilledConfig = this . buildDefaultBackfilledConfig ( key , rawConfig )
515+ if ( defaultBackfilledConfig ) {
516+ try {
517+ return this . normalizeTextModelConfig (
518+ this . patchProviderMeta ( this . patchDeepseekConfig ( defaultBackfilledConfig ) ) ,
519+ registry ,
520+ { allowLegacyMetadataMismatch : true }
521+ )
522+ } catch ( repairError ) {
523+ console . warn (
524+ `[ModelManager] Failed to repair default model config ${ key } , returning disabled placeholder:` ,
525+ repairError
526+ )
527+ }
528+ }
529+
530+ return this . createDisabledPlaceholderConfig ( key , rawConfig , error )
531+ }
532+ }
533+
288534 private normalizeTextModelConfig (
289535 config : Partial < TextModelConfig > & Pick < TextModelConfig , 'id' | 'name' | 'enabled' > ,
290536 registry : ITextAdapterRegistry ,
@@ -538,35 +784,11 @@ export class ModelManager implements IModelManager {
538784 async getAllModels ( ) : Promise < TextModelConfig [ ] > {
539785 await this . ensureInitialized ( ) ;
540786 const models = await this . getModelsFromStorage ( ) ;
541-
542- // 转换为 TextModelConfig 数组(先完成格式/字段迁移)
543- const migratedConfigs = Object . entries ( models ) . map ( ( [ key , config ] ) => {
544- let textConfig : TextModelConfig
545-
546- // 检查是否已经是新格式
547- if ( isTextModelConfig ( config ) ) {
548- textConfig = config as TextModelConfig ;
549- }
550- // 传统格式,转换为新格式
551- else if ( isLegacyConfig ( config ) ) {
552- textConfig = convertLegacyToTextModelConfig ( key , config ) ;
553- }
554- // 未知格式,尝试转换
555- else {
556- textConfig = convertLegacyToTextModelConfig ( key , config as ModelConfig ) ;
557- }
558-
559- // 读时迁移:合并 customParamOverrides 到 paramOverrides
560- return this . migrateConfig ( textConfig )
561- } ) ;
562-
563787 const registry = await this . getRegistry ( )
564788
565- return migratedConfigs . map ( ( cfg ) => this . normalizeTextModelConfig (
566- this . patchProviderMeta ( this . patchDeepseekConfig ( cfg ) ) ,
567- registry ,
568- { allowLegacyMetadataMismatch : true }
569- ) )
789+ return Object . entries ( models ) . map ( ( [ key , config ] ) =>
790+ this . normalizeStoredTextModelConfig ( key , config , registry )
791+ )
570792 }
571793
572794 /**
@@ -581,30 +803,8 @@ export class ModelManager implements IModelManager {
581803 return undefined ;
582804 }
583805
584- let textConfig : TextModelConfig
585-
586- // 检查是否已经是新格式
587- if ( isTextModelConfig ( config ) ) {
588- textConfig = config as TextModelConfig ;
589- }
590- // 传统格式,转换为新格式
591- else if ( isLegacyConfig ( config ) ) {
592- textConfig = convertLegacyToTextModelConfig ( key , config ) ;
593- }
594- // 未知格式,尝试转换
595- else {
596- textConfig = convertLegacyToTextModelConfig ( key , config as ModelConfig ) ;
597- }
598-
599- // 读时迁移:合并 customParamOverrides 到 paramOverrides
600- const migrated = this . migrateConfig ( textConfig )
601806 const registry = await this . getRegistry ( )
602-
603- return this . normalizeTextModelConfig (
604- this . patchProviderMeta ( this . patchDeepseekConfig ( migrated ) ) ,
605- registry ,
606- { allowLegacyMetadataMismatch : true }
607- )
807+ return this . normalizeStoredTextModelConfig ( key , config , registry )
608808 }
609809
610810 /**
0 commit comments