infio-copilot/src/types/settings.ts
2025-06-03 10:03:55 +08:00

415 lines
11 KiB
TypeScript

import { z } from 'zod';
import { DEFAULT_MODELS } from '../constants';
import {
MAX_DELAY,
MAX_MAX_CHAR_LIMIT,
MIN_DELAY,
MIN_MAX_CHAR_LIMIT,
fewShotExampleSchema,
modelOptionsSchema
} from '../settings/versions/shared';
import { DEFAULT_SETTINGS } from "../settings/versions/v1/v1";
import { ApiProvider } from '../types/llm/model';
import { isRegexValid, isValidIgnorePattern } from '../utils/auto-complete';
export const SETTINGS_SCHEMA_VERSION = 0.4
const InfioProviderSchema = z.object({
name: z.literal('Infio'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'Infio',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const OpenRouterProviderSchema = z.object({
name: z.literal('OpenRouter'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'OpenRouter',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const SiliconFlowProviderSchema = z.object({
name: z.literal('SiliconFlow'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'SiliconFlow',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const AlibabaQwenProviderSchema = z.object({
name: z.literal('AlibabaQwen'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'AlibabaQwen',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const AnthropicProviderSchema = z.object({
name: z.literal('Anthropic'),
apiKey: z.string().catch(''),
baseUrl: z.string().optional(),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'Anthropic',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const DeepSeekProviderSchema = z.object({
name: z.literal('DeepSeek'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'DeepSeek',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const GoogleProviderSchema = z.object({
name: z.literal('Google'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'Google',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const OpenAIProviderSchema = z.object({
name: z.literal('OpenAI'),
apiKey: z.string().catch(''),
baseUrl: z.string().optional(),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'OpenAI',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const OpenAICompatibleProviderSchema = z.object({
name: z.literal('OpenAICompatible'),
apiKey: z.string().catch(''),
baseUrl: z.string().optional(),
useCustomUrl: z.boolean().catch(true)
}).catch({
name: 'OpenAICompatible',
apiKey: '',
baseUrl: '',
useCustomUrl: true
})
const OllamaProviderSchema = z.object({
name: z.literal('Ollama'),
apiKey: z.string().catch('ollama'),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'Ollama',
apiKey: 'ollama',
baseUrl: '',
useCustomUrl: true
})
const GroqProviderSchema = z.object({
name: z.literal('Groq'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'Groq',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const GrokProviderSchema = z.object({
name: z.literal('Grok'),
apiKey: z.string().catch(''),
baseUrl: z.string().catch(''),
useCustomUrl: z.boolean().catch(false)
}).catch({
name: 'Grok',
apiKey: '',
baseUrl: '',
useCustomUrl: false
})
const ollamaModelSchema = z.object({
baseUrl: z.string().catch(''),
model: z.string().catch(''),
})
const openAICompatibleModelSchema = z.object({
baseUrl: z.string().catch(''),
apiKey: z.string().catch(''),
model: z.string().catch(''),
})
const ragOptionsSchema = z.object({
chunkSize: z.number().catch(1000),
thresholdTokens: z.number().catch(8192),
minSimilarity: z.number().catch(0.0),
limit: z.number().catch(10),
excludePatterns: z.array(z.string()).catch([]),
includePatterns: z.array(z.string()).catch([]),
})
export const triggerSchema = z.object({
type: z.enum(['string', 'regex']),
value: z.string().min(1, { message: "Trigger value must be at least 1 character long" })
}).strict().superRefine((trigger, ctx) => {
if (trigger.type === "regex") {
if (!trigger.value.endsWith("$")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Regex triggers must end with a $.",
path: ["value"],
});
}
if (!isRegexValid(trigger.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid regex: "${trigger.value}"`,
path: ["value"],
});
}
}
});
export const InfioSettingsSchema = z.object({
// Version
version: z.literal(SETTINGS_SCHEMA_VERSION).catch(SETTINGS_SCHEMA_VERSION),
// Provider
defaultProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.OpenRouter),
infioProvider: InfioProviderSchema,
openrouterProvider: OpenRouterProviderSchema,
siliconflowProvider: SiliconFlowProviderSchema,
alibabaQwenProvider: AlibabaQwenProviderSchema,
anthropicProvider: AnthropicProviderSchema,
deepseekProvider: DeepSeekProviderSchema,
openaiProvider: OpenAIProviderSchema,
googleProvider: GoogleProviderSchema,
ollamaProvider: OllamaProviderSchema,
groqProvider: GroqProviderSchema,
grokProvider: GrokProviderSchema,
openaicompatibleProvider: OpenAICompatibleProviderSchema,
// MCP Servers
mcpEnabled: z.boolean().catch(false),
// Chat Model start list
collectedChatModels: z.array(z.object({
provider: z.nativeEnum(ApiProvider),
modelId: z.string(),
})).catch([]),
// Chat Model
chatModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.OpenRouter),
chatModelId: z.string().catch(''),
// Apply Model
applyModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.OpenRouter),
applyModelId: z.string().catch(''),
// Embedding Model
embeddingModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Google),
embeddingModelId: z.string().catch(''),
// fuzzyMatchThreshold
fuzzyMatchThreshold: z.number().catch(0.85),
// experimentalDiffStrategy
experimentalDiffStrategy: z.boolean().catch(false),
// multiSearchReplaceDiffStrategy
multiSearchReplaceDiffStrategy: z.boolean().catch(true),
// Mode
mode: z.string().catch('ask'),
defaultMention: z.enum(['none', 'current-file', 'vault']).catch('none'),
// web search
serperApiKey: z.string().catch(''),
serperSearchEngine: z.enum(['google', 'duckduckgo', 'bing']).catch('google'),
jinaApiKey: z.string().catch(''),
// Files Search
filesSearchMethod: z.enum(['regex', 'semantic', 'auto']).catch('auto'),
ripgrepPath: z.string().catch(''),
/// [compatible]
// activeModels [compatible]
activeModels: z.array(
z.object({
name: z.string(),
provider: z.string(),
enabled: z.boolean(),
isEmbeddingModel: z.boolean(),
isBuiltIn: z.boolean(),
apiKey: z.string().optional(),
baseUrl: z.string().optional(),
dimension: z.number().optional(),
})
).catch(DEFAULT_MODELS),
// API Keys [compatible]
infioApiKey: z.string().catch(''),
openAIApiKey: z.string().catch(''),
anthropicApiKey: z.string().catch(''),
geminiApiKey: z.string().catch(''),
groqApiKey: z.string().catch(''),
deepseekApiKey: z.string().catch(''),
ollamaEmbeddingModel: ollamaModelSchema.catch({
baseUrl: '',
model: '',
}),
ollamaChatModel: ollamaModelSchema.catch({
baseUrl: '',
model: '',
}),
openAICompatibleChatModel: openAICompatibleModelSchema.catch({
baseUrl: '',
apiKey: '',
model: '',
}),
ollamaApplyModel: ollamaModelSchema.catch({
baseUrl: '',
model: '',
}),
openAICompatibleApplyModel: openAICompatibleModelSchema.catch({
baseUrl: '',
apiKey: '',
model: '',
}),
// System Prompt
systemPrompt: z.string().catch(''),
// RAG Options
ragOptions: ragOptionsSchema.catch({
chunkSize: 1000,
thresholdTokens: 8192,
minSimilarity: 0.0,
limit: 10,
excludePatterns: [],
includePatterns: [],
}),
// autocomplete options
autocompleteEnabled: z.boolean(),
advancedMode: z.boolean(),
// [compatible]
apiProvider: z.enum(['azure', 'openai', "ollama"]),
azureOAIApiSettings: z.string().catch(''),
openAIApiSettings: z.string().catch(''),
ollamaApiSettings: z.string().catch(''),
triggers: z.array(triggerSchema),
delay: z.number().int().min(MIN_DELAY, { message: "Delay must be between 0ms and 2000ms" }).max(MAX_DELAY, { message: "Delay must be between 0ms and 2000ms" }),
modelOptions: modelOptionsSchema,
systemMessage: z.string().min(3, { message: "System message must be at least 3 characters long" }),
fewShotExamples: z.array(fewShotExampleSchema),
userMessageTemplate: z.string().min(3, { message: "User message template must be at least 3 characters long" }),
chainOfThoughRemovalRegex: z.string().refine((regex) => isRegexValid(regex), { message: "Invalid regex" }),
dontIncludeDataviews: z.boolean(),
maxPrefixCharLimit: z.number().int().min(MIN_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at least ${MIN_MAX_CHAR_LIMIT}` }).max(MAX_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at most ${MAX_MAX_CHAR_LIMIT}` }),
maxSuffixCharLimit: z.number().int().min(MIN_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at least ${MIN_MAX_CHAR_LIMIT}` }).max(MAX_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at most ${MAX_MAX_CHAR_LIMIT}` }),
removeDuplicateMathBlockIndicator: z.boolean(),
removeDuplicateCodeBlockIndicator: z.boolean(),
ignoredFilePatterns: z.string().refine((value) => value
.split("\n")
.filter(s => s.trim().length > 0)
.filter(s => !isValidIgnorePattern(s)).length === 0,
{ message: "Invalid ignore pattern" }
),
ignoredTags: z.string().refine((value) => value
.split("\n")
.filter(s => s.includes(" ")).length === 0, { message: "Tags cannot contain spaces" }
).refine((value) => value
.split("\n")
.filter(s => s.includes("#")).length === 0, { message: "Enter tags without the # symbol" }
).refine((value) => value
.split("\n")
.filter(s => s.includes(",")).length === 0, { message: "Enter each tag on a new line without commas" }
),
cacheSuggestions: z.boolean(),
debugMode: z.boolean(),
})
export type InfioSettings = z.infer<typeof InfioSettingsSchema>
type Migration = {
fromVersion: number
toVersion: number
migrate: (data: Record<string, unknown>) => Record<string, unknown>
}
const MIGRATIONS: Migration[] = [
{
fromVersion: 0.1,
toVersion: 0.4,
migrate: (data) => {
const newData = { ...data }
newData.version = SETTINGS_SCHEMA_VERSION
return newData
},
},
]
function migrateSettings(
data: Record<string, unknown>,
): Record<string, unknown> {
let currentData = { ...data }
const currentVersion = (currentData.version as number) ?? 0
for (const migration of MIGRATIONS) {
if (
currentVersion >= migration.fromVersion &&
currentVersion < migration.toVersion &&
migration.toVersion <= SETTINGS_SCHEMA_VERSION
) {
console.log(
`Migrating settings from ${migration.fromVersion} to ${migration.toVersion}`,
)
currentData = migration.migrate(currentData)
}
}
return currentData
}
export function parseInfioSettings(data: unknown): InfioSettings {
try {
const migratedData = migrateSettings(data as Record<string, unknown>)
return InfioSettingsSchema.parse(migratedData)
} catch (error) {
return InfioSettingsSchema.parse({ ...DEFAULT_SETTINGS })
}
}