Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ src/renderer/src/components/ui/*
.github
.cursor
.vscode
electron.vite.config.ts
electron.vite.config.ts
*.md
159 changes: 117 additions & 42 deletions src/main/presenter/configPresenter/modelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,56 @@ export class ModelConfigHelper {
})
}

/**
* Generate a safe cache key by escaping special characters that could cause JSON parsing issues
* @param providerId - The provider ID
* @param modelId - The model ID
* @returns Safe cache key string
*/
private generateCacheKey(providerId: string, modelId: string): string {
// Replace dots and other problematic characters that could interfere with electron-store's key parsing
const sanitizeString = (str: string): string => {
return str
.replace(/\./g, '_DOT_') // Replace dots with _DOT_
.replace(/\[/g, '_LBRACKET_') // Replace [ with _LBRACKET_
.replace(/\]/g, '_RBRACKET_') // Replace ] with _RBRACKET_
.replace(/"/g, '_QUOTE_') // Replace " with _QUOTE_
.replace(/'/g, '_SQUOTE_') // Replace ' with _SQUOTE_
}

const sanitizedProviderId = sanitizeString(providerId)
const sanitizedModelId = sanitizeString(modelId)

return sanitizedProviderId + SPECIAL_CONCAT_CHAR + sanitizedModelId
}

/**
* Reverse the sanitization process to get original IDs from cache key
* @param sanitizedString - The sanitized string
* @returns Original string with special characters restored
*/
private desanitizeString(sanitizedString: string): string {
return sanitizedString
.replace(/_DOT_/g, '.')
.replace(/_LBRACKET_/g, '[')
.replace(/_RBRACKET_/g, ']')
.replace(/_QUOTE_/g, '"')
.replace(/_SQUOTE_/g, "'")
}

/**
* Parse cache key to extract original provider ID and model ID
* @param cacheKey - The cache key to parse
* @returns Object with providerId and modelId
*/
private parseCacheKey(cacheKey: string): { providerId: string; modelId: string } {
const [sanitizedProviderId, sanitizedModelId] = cacheKey.split(SPECIAL_CONCAT_CHAR)
return {
providerId: this.desanitizeString(sanitizedProviderId),
modelId: this.desanitizeString(sanitizedModelId)
}
}

/**
* Initialize memory cache by loading all data from store
* This is called lazily on first access
Expand All @@ -35,64 +85,89 @@ export class ModelConfigHelper {
* Get model configuration with priority: user config > provider config > default config
* @param modelId - The model ID
* @param providerId - Optional provider ID
* @returns ModelConfig
* @returns ModelConfig with isUserDefined flag
*/
getModelConfig(modelId: string, providerId?: string): ModelConfig {
// Initialize cache if not already done
this.initializeCache()

let hasUserConfig = false
let userConfig: ModelConfig | null = null

// 1. First try to get user-defined config for this specific provider + model
if (providerId) {
const cacheKey = providerId + SPECIAL_CONCAT_CHAR + modelId
let userConfig = this.memoryCache.get(cacheKey)
const cacheKey = this.generateCacheKey(providerId, modelId)
let userConfigData = this.memoryCache.get(cacheKey)

// If not in cache, try to load from store and cache it
if (!userConfig) {
userConfig = this.modelConfigStore.get(cacheKey)
if (userConfig) {
this.memoryCache.set(cacheKey, userConfig)
if (!userConfigData) {
userConfigData = this.modelConfigStore.get(cacheKey)
if (userConfigData) {
this.memoryCache.set(cacheKey, userConfigData)
}
}

if (userConfig?.config) {
return userConfig.config
if (userConfigData?.config) {
hasUserConfig = true
userConfig = userConfigData.config
}
}

// 2. Try to get provider-specific default config
if (providerId) {
const providerConfig = getProviderSpecificModelConfig(providerId, modelId)
if (providerConfig) {
return providerConfig
let finalConfig: ModelConfig

if (hasUserConfig && userConfig) {
// Use user config as base
finalConfig = { ...userConfig }
} else {
// 2. Try to get provider-specific default config
let providerConfig: ModelConfig | null = null
if (providerId) {
providerConfig = getProviderSpecificModelConfig(providerId, modelId) || null
}
}

// 3. Try to get default model config by pattern matching
const lowerModelId = modelId.toLowerCase()
for (const config of defaultModelsSettings) {
if (config.match.some((matchStr) => lowerModelId.includes(matchStr.toLowerCase()))) {
return {
maxTokens: config.maxTokens,
contextLength: config.contextLength,
temperature: config.temperature,
vision: config.vision,
functionCall: config.functionCall || false,
reasoning: config.reasoning || false,
type: config.type || ModelType.Chat
if (providerConfig) {
finalConfig = { ...providerConfig }
} else {
// 3. Try to get default model config by pattern matching
const lowerModelId = modelId.toLowerCase()
let defaultConfig: ModelConfig | null = null

for (const config of defaultModelsSettings) {
if (config.match.some((matchStr) => lowerModelId.includes(matchStr.toLowerCase()))) {
defaultConfig = {
maxTokens: config.maxTokens,
contextLength: config.contextLength,
temperature: config.temperature,
vision: config.vision,
functionCall: config.functionCall || false,
reasoning: config.reasoning || false,
type: config.type || ModelType.Chat
}
break
}
}

if (defaultConfig) {
finalConfig = defaultConfig
} else {
// 4. Return safe default config if nothing matches
finalConfig = {
maxTokens: 4096,
contextLength: 8192,
temperature: 0.6,
vision: false,
functionCall: false,
reasoning: false,
type: ModelType.Chat
}
}
}
}

// 4. Return safe default config if nothing matches
return {
maxTokens: 4096,
contextLength: 8192,
temperature: 0.6,
vision: false,
functionCall: false,
reasoning: false,
type: ModelType.Chat
}
// Add source information to the config
finalConfig.isUserDefined = hasUserConfig

return finalConfig
}

/**
Expand All @@ -102,7 +177,7 @@ export class ModelConfigHelper {
* @param config - The model configuration
*/
setModelConfig(modelId: string, providerId: string, config: ModelConfig): void {
const cacheKey = providerId + SPECIAL_CONCAT_CHAR + modelId
const cacheKey = this.generateCacheKey(providerId, modelId)
const configData: IModelConfig = {
id: modelId,
providerId: providerId,
Expand All @@ -120,7 +195,7 @@ export class ModelConfigHelper {
* @param providerId - The provider ID
*/
resetModelConfig(modelId: string, providerId: string): void {
const cacheKey = providerId + SPECIAL_CONCAT_CHAR + modelId
const cacheKey = this.generateCacheKey(providerId, modelId)

// Remove from both store and cache
this.modelConfigStore.delete(cacheKey)
Expand Down Expand Up @@ -153,10 +228,10 @@ export class ModelConfigHelper {
const result: Array<{ modelId: string; config: ModelConfig }> = []

Object.entries(allConfigs).forEach(([key, value]) => {
const [keyProviderId] = key.split(SPECIAL_CONCAT_CHAR)
const { providerId: keyProviderId, modelId: keyModelId } = this.parseCacheKey(key)
if (keyProviderId === providerId) {
result.push({
modelId: value.id,
modelId: keyModelId,
config: value.config
})
}
Expand All @@ -175,7 +250,7 @@ export class ModelConfigHelper {
// Initialize cache if not already done
this.initializeCache()

const cacheKey = providerId + SPECIAL_CONCAT_CHAR + modelId
const cacheKey = this.generateCacheKey(providerId, modelId)

// Check cache first
if (this.memoryCache.has(cacheKey)) {
Expand Down
31 changes: 18 additions & 13 deletions src/main/presenter/llmProviderPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,21 +244,26 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
let models = await provider.fetchModels()
models = models.map((model) => {
const config = this.configPresenter.getModelConfig(model.id, providerId)
if (config) {
model.maxTokens = config.maxTokens
model.contextLength = config.contextLength
// 如果模型中已经有这些属性则保留,否则使用配置中的值或默认为false
model.vision = model.vision !== undefined ? model.vision : config.vision || false
model.functionCall =
model.functionCall !== undefined ? model.functionCall : config.functionCall || false
model.reasoning =
model.reasoning !== undefined ? model.reasoning : config.reasoning || false

// Always use config values for maxTokens, contextLength, and temperature
model.maxTokens = config.maxTokens
model.contextLength = config.contextLength

if (config.isUserDefined) {
// User has explicitly configured this model, use all config values
model.vision = config.vision
model.functionCall = config.functionCall
model.reasoning = config.reasoning
model.type = config.type
} else {
// 确保模型具有这些属性,如果没有配置,默认为false
model.vision = model.vision || false
model.functionCall = model.functionCall || false
model.reasoning = model.reasoning || false
// Default config, prioritize model's own capabilities if they exist
model.vision = model.vision !== undefined ? model.vision : config.vision
model.functionCall =
model.functionCall !== undefined ? model.functionCall : config.functionCall
model.reasoning = model.reasoning !== undefined ? model.reasoning : config.reasoning
model.type = model.type || config.type
}

return model
})
return models
Expand Down
Loading