import type { ApiKeyEntry, GeminiKeyConfig, ModelAlias, OpenAIProviderConfig, ProviderKeyConfig, AmpcodeConfig, AmpcodeModelMapping } from '@/types'; import type { Config } from '@/types/config'; import { buildHeaderObject } from '@/utils/headers'; const isRecord = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value); const normalizeBoolean = (value: unknown): boolean | undefined => { if (value === undefined || value === null) return undefined; if (typeof value === 'boolean') return value; if (typeof value === 'number') return value !== 0; if (typeof value === 'string') { const trimmed = value.trim().toLowerCase(); if (['true', '1', 'yes', 'y', 'on'].includes(trimmed)) return true; if (['false', '0', 'no', 'n', 'off'].includes(trimmed)) return false; } return Boolean(value); }; const normalizeModelAliases = (models: unknown): ModelAlias[] => { if (!Array.isArray(models)) return []; return models .map((item) => { if (item === undefined || item === null) return null; if (typeof item === 'string') { const trimmed = item.trim(); return trimmed ? ({ name: trimmed } satisfies ModelAlias) : null; } if (!isRecord(item)) return null; const name = item.name || item.id || item.model; if (!name) return null; const alias = item.alias || item.display_name || item.displayName; const priority = item.priority ?? item['priority']; const testModel = item['test-model'] ?? item.testModel; const entry: ModelAlias = { name: String(name) }; if (alias && alias !== name) { entry.alias = String(alias); } if (priority !== undefined) { const parsed = Number(priority); if (Number.isFinite(parsed)) { entry.priority = parsed; } } if (testModel) { entry.testModel = String(testModel); } return entry; }) .filter(Boolean) as ModelAlias[]; }; const normalizeHeaders = (headers: unknown) => { if (!headers || typeof headers !== 'object') return undefined; const normalized = buildHeaderObject( Array.isArray(headers) ? (headers as Array<{ key: string; value: string }>) : (headers as Record) ); return Object.keys(normalized).length ? normalized : undefined; }; const normalizeExcludedModels = (input: unknown): string[] => { const rawList = Array.isArray(input) ? input : typeof input === 'string' ? input.split(/[\n,]/) : []; const seen = new Set(); const normalized: string[] = []; rawList.forEach((item) => { const trimmed = String(item ?? '').trim(); if (!trimmed) return; const key = trimmed.toLowerCase(); if (seen.has(key)) return; seen.add(key); normalized.push(trimmed); }); return normalized; }; const normalizePrefix = (value: unknown): string | undefined => { if (value === undefined || value === null) return undefined; const trimmed = String(value).trim(); return trimmed ? trimmed : undefined; }; const normalizeApiKeyEntry = (entry: unknown): ApiKeyEntry | null => { if (entry === undefined || entry === null) return null; const record = isRecord(entry) ? entry : null; const apiKey = record?.['api-key'] ?? record?.apiKey ?? record?.key ?? (typeof entry === 'string' ? entry : ''); const trimmed = String(apiKey || '').trim(); if (!trimmed) return null; const proxyUrl = record ? record['proxy-url'] ?? record.proxyUrl : undefined; const headers = record ? normalizeHeaders(record.headers) : undefined; return { apiKey: trimmed, proxyUrl: proxyUrl ? String(proxyUrl) : undefined, headers }; }; const normalizeProviderKeyConfig = (item: unknown): ProviderKeyConfig | null => { if (item === undefined || item === null) return null; const record = isRecord(item) ? item : null; const apiKey = record?.['api-key'] ?? record?.apiKey ?? (typeof item === 'string' ? item : ''); const trimmed = String(apiKey || '').trim(); if (!trimmed) return null; const config: ProviderKeyConfig = { apiKey: trimmed }; const prefix = normalizePrefix(record?.prefix ?? record?.['prefix']); if (prefix) config.prefix = prefix; const baseUrl = record ? record['base-url'] ?? record.baseUrl : undefined; const proxyUrl = record ? record['proxy-url'] ?? record.proxyUrl : undefined; if (baseUrl) config.baseUrl = String(baseUrl); if (proxyUrl) config.proxyUrl = String(proxyUrl); const headers = normalizeHeaders(record?.headers); if (headers) config.headers = headers; const models = normalizeModelAliases(record?.models); if (models.length) config.models = models; const excludedModels = normalizeExcludedModels( record?.['excluded-models'] ?? record?.excludedModels ?? record?.['excluded_models'] ?? record?.excluded_models ); if (excludedModels.length) config.excludedModels = excludedModels; return config; }; const normalizeGeminiKeyConfig = (item: unknown): GeminiKeyConfig | null => { if (item === undefined || item === null) return null; const record = isRecord(item) ? item : null; let apiKey = record?.['api-key'] ?? record?.apiKey; if (!apiKey && typeof item === 'string') { apiKey = item; } const trimmed = String(apiKey || '').trim(); if (!trimmed) return null; const config: GeminiKeyConfig = { apiKey: trimmed }; const prefix = normalizePrefix(record?.prefix ?? record?.['prefix']); if (prefix) config.prefix = prefix; const baseUrl = record ? record['base-url'] ?? record.baseUrl ?? record['base_url'] : undefined; if (baseUrl) config.baseUrl = String(baseUrl); const headers = normalizeHeaders(record?.headers); if (headers) config.headers = headers; const excludedModels = normalizeExcludedModels(record?.['excluded-models'] ?? record?.excludedModels); if (excludedModels.length) config.excludedModels = excludedModels; return config; }; const normalizeOpenAIProvider = (provider: unknown): OpenAIProviderConfig | null => { if (!isRecord(provider)) return null; const name = provider.name || provider.id; const baseUrl = provider['base-url'] ?? provider.baseUrl; if (!name || !baseUrl) return null; let apiKeyEntries: ApiKeyEntry[] = []; if (Array.isArray(provider['api-key-entries'])) { apiKeyEntries = provider['api-key-entries'] .map((entry) => normalizeApiKeyEntry(entry)) .filter(Boolean) as ApiKeyEntry[]; } else if (Array.isArray(provider['api-keys'])) { apiKeyEntries = provider['api-keys'] .map((key) => normalizeApiKeyEntry({ 'api-key': key })) .filter(Boolean) as ApiKeyEntry[]; } const headers = normalizeHeaders(provider.headers); const models = normalizeModelAliases(provider.models); const priority = provider.priority ?? provider['priority']; const testModel = provider['test-model'] ?? provider.testModel; const result: OpenAIProviderConfig = { name: String(name), baseUrl: String(baseUrl), apiKeyEntries }; const prefix = normalizePrefix(provider.prefix ?? provider['prefix']); if (prefix) result.prefix = prefix; if (headers) result.headers = headers; if (models.length) result.models = models; if (priority !== undefined) result.priority = Number(priority); if (testModel) result.testModel = String(testModel); return result; }; const normalizeOauthExcluded = (payload: unknown): Record | undefined => { if (!isRecord(payload)) return undefined; const source = payload['oauth-excluded-models'] ?? payload.items ?? payload; if (!isRecord(source)) return undefined; const map: Record = {}; Object.entries(source).forEach(([provider, models]) => { const key = String(provider || '').trim(); if (!key) return; const normalized = normalizeExcludedModels(models); map[key.toLowerCase()] = normalized; }); return map; }; const normalizeAmpcodeModelMappings = (input: unknown): AmpcodeModelMapping[] => { if (!Array.isArray(input)) return []; const seen = new Set(); const mappings: AmpcodeModelMapping[] = []; input.forEach((entry) => { if (!isRecord(entry)) return; const from = String(entry.from ?? entry['from'] ?? '').trim(); const to = String(entry.to ?? entry['to'] ?? '').trim(); if (!from || !to) return; const key = from.toLowerCase(); if (seen.has(key)) return; seen.add(key); mappings.push({ from, to }); }); return mappings; }; const normalizeAmpcodeConfig = (payload: unknown): AmpcodeConfig | undefined => { const sourceRaw = isRecord(payload) ? (payload.ampcode ?? payload) : payload; if (!isRecord(sourceRaw)) return undefined; const source = sourceRaw; const config: AmpcodeConfig = {}; const upstreamUrl = source['upstream-url'] ?? source.upstreamUrl ?? source['upstream_url']; if (upstreamUrl) config.upstreamUrl = String(upstreamUrl); const upstreamApiKey = source['upstream-api-key'] ?? source.upstreamApiKey ?? source['upstream_api_key']; if (upstreamApiKey) config.upstreamApiKey = String(upstreamApiKey); const forceModelMappings = normalizeBoolean( source['force-model-mappings'] ?? source.forceModelMappings ?? source['force_model_mappings'] ); if (forceModelMappings !== undefined) { config.forceModelMappings = forceModelMappings; } const modelMappings = normalizeAmpcodeModelMappings( source['model-mappings'] ?? source.modelMappings ?? source['model_mappings'] ); if (modelMappings.length) { config.modelMappings = modelMappings; } return config; }; /** * 规范化 /config 返回值 */ export const normalizeConfigResponse = (raw: unknown): Config => { const config: Config = { raw: isRecord(raw) ? raw : {} }; if (!isRecord(raw)) { return config; } config.debug = normalizeBoolean(raw.debug); const proxyUrl = raw['proxy-url'] ?? raw.proxyUrl; config.proxyUrl = typeof proxyUrl === 'string' ? proxyUrl : proxyUrl === undefined || proxyUrl === null ? undefined : String(proxyUrl); const requestRetry = raw['request-retry'] ?? raw.requestRetry; if (typeof requestRetry === 'number' && Number.isFinite(requestRetry)) { config.requestRetry = requestRetry; } else if (typeof requestRetry === 'string' && requestRetry.trim() !== '') { const parsed = Number(requestRetry); if (Number.isFinite(parsed)) { config.requestRetry = parsed; } } const quota = raw['quota-exceeded'] ?? raw.quotaExceeded; if (isRecord(quota)) { config.quotaExceeded = { switchProject: normalizeBoolean(quota['switch-project'] ?? quota.switchProject), switchPreviewModel: normalizeBoolean(quota['switch-preview-model'] ?? quota.switchPreviewModel) }; } config.usageStatisticsEnabled = normalizeBoolean( raw['usage-statistics-enabled'] ?? raw.usageStatisticsEnabled ); config.requestLog = normalizeBoolean(raw['request-log'] ?? raw.requestLog); config.loggingToFile = normalizeBoolean(raw['logging-to-file'] ?? raw.loggingToFile); const logsMaxTotalSizeMb = raw['logs-max-total-size-mb'] ?? raw.logsMaxTotalSizeMb; if (typeof logsMaxTotalSizeMb === 'number' && Number.isFinite(logsMaxTotalSizeMb)) { config.logsMaxTotalSizeMb = logsMaxTotalSizeMb; } else if (typeof logsMaxTotalSizeMb === 'string' && logsMaxTotalSizeMb.trim() !== '') { const parsed = Number(logsMaxTotalSizeMb); if (Number.isFinite(parsed)) { config.logsMaxTotalSizeMb = parsed; } } config.wsAuth = normalizeBoolean(raw['ws-auth'] ?? raw.wsAuth); config.forceModelPrefix = normalizeBoolean(raw['force-model-prefix'] ?? raw.forceModelPrefix); const routing = raw.routing; const strategyRaw = isRecord(routing) ? (routing.strategy ?? routing['strategy']) : (raw['routing-strategy'] ?? raw.routingStrategy); if (strategyRaw !== undefined && strategyRaw !== null) { config.routingStrategy = String(strategyRaw); } const apiKeysRaw = raw['api-keys'] ?? raw.apiKeys; if (Array.isArray(apiKeysRaw)) { config.apiKeys = apiKeysRaw.map((key) => String(key)).filter((key) => key.trim() !== ''); } const geminiList = raw['gemini-api-key'] ?? raw.geminiApiKey ?? raw.geminiApiKeys; if (Array.isArray(geminiList)) { config.geminiApiKeys = geminiList .map((item) => normalizeGeminiKeyConfig(item)) .filter(Boolean) as GeminiKeyConfig[]; } const codexList = raw['codex-api-key'] ?? raw.codexApiKey ?? raw.codexApiKeys; if (Array.isArray(codexList)) { config.codexApiKeys = codexList .map((item) => normalizeProviderKeyConfig(item)) .filter(Boolean) as ProviderKeyConfig[]; } const claudeList = raw['claude-api-key'] ?? raw.claudeApiKey ?? raw.claudeApiKeys; if (Array.isArray(claudeList)) { config.claudeApiKeys = claudeList .map((item) => normalizeProviderKeyConfig(item)) .filter(Boolean) as ProviderKeyConfig[]; } const vertexList = raw['vertex-api-key'] ?? raw.vertexApiKey ?? raw.vertexApiKeys; if (Array.isArray(vertexList)) { config.vertexApiKeys = vertexList .map((item) => normalizeProviderKeyConfig(item)) .filter(Boolean) as ProviderKeyConfig[]; } const openaiList = raw['openai-compatibility'] ?? raw.openaiCompatibility ?? raw.openAICompatibility; if (Array.isArray(openaiList)) { config.openaiCompatibility = openaiList .map((item) => normalizeOpenAIProvider(item)) .filter(Boolean) as OpenAIProviderConfig[]; } const ampcode = normalizeAmpcodeConfig(raw.ampcode); if (ampcode) { config.ampcode = ampcode; } const oauthExcluded = normalizeOauthExcluded(raw['oauth-excluded-models'] ?? raw.oauthExcludedModels); if (oauthExcluded) { config.oauthExcludedModels = oauthExcluded; } return config; }; export { normalizeApiKeyEntry, normalizeGeminiKeyConfig, normalizeModelAliases, normalizeOpenAIProvider, normalizeProviderKeyConfig, normalizeHeaders, normalizeExcludedModels, normalizeAmpcodeConfig, normalizeAmpcodeModelMappings };