import { CONFIG_FILE, readConfigLayers, isPlainObject, getConfigForPath, writeConfig, } from './shared.js'; const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/; const BASE_URL_PATTERN = /^https?:\/\//; const OPENAI_COMPATIBLE_NPM = '@ai-sdk/openai-compatible'; function getProviderSources(providerId, workingDirectory) { const layers = readConfigLayers(workingDirectory); const { userConfig, projectConfig, customConfig, paths } = layers; const customProviders = isPlainObject(customConfig?.provider) ? customConfig.provider : {}; const customProvidersAlias = isPlainObject(customConfig?.providers) ? customConfig.providers : {}; const projectProviders = isPlainObject(projectConfig?.provider) ? projectConfig.provider : {}; const projectProvidersAlias = isPlainObject(projectConfig?.providers) ? projectConfig.providers : {}; const userProviders = isPlainObject(userConfig?.provider) ? userConfig.provider : {}; const userProvidersAlias = isPlainObject(userConfig?.providers) ? userConfig.providers : {}; const customExists = Object.prototype.hasOwnProperty.call(customProviders, providerId) || Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId); const projectExists = Object.prototype.hasOwnProperty.call(projectProviders, providerId) || Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId); const userExists = Object.prototype.hasOwnProperty.call(userProviders, providerId) || Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId); return { sources: { auth: { exists: false }, user: { exists: userExists, path: paths.userPath }, project: { exists: projectExists, path: paths.projectPath || null }, custom: { exists: customExists, path: paths.customPath } } }; } /** * Validate a custom OpenAI-compatible provider config payload before persistence. * Returns { ok: true, value } or { ok: false, error }. */ function validateCustomProviderConfig(providerId, config) { if (!providerId || typeof providerId !== 'string' || !PROVIDER_ID_PATTERN.test(providerId)) { return { ok: false, error: 'Provider ID must match /^[a-z0-9][a-z0-9-_]*$/' }; } if (!isPlainObject(config)) { return { ok: false, error: 'Provider config must be an object' }; } const name = typeof config.name === 'string' ? config.name.trim() : ''; if (!name) { return { ok: false, error: 'Provider name is required' }; } const npm = typeof config.npm === 'string' ? config.npm.trim() : OPENAI_COMPATIBLE_NPM; if (npm !== OPENAI_COMPATIBLE_NPM) { return { ok: false, error: `Custom providers must use npm package ${OPENAI_COMPATIBLE_NPM}` }; } const options = isPlainObject(config.options) ? config.options : null; if (!options) { return { ok: false, error: 'Provider options are required' }; } const baseURL = typeof options.baseURL === 'string' ? options.baseURL.trim() : ''; if (!baseURL) { return { ok: false, error: 'Base URL is required' }; } if (!BASE_URL_PATTERN.test(baseURL)) { return { ok: false, error: 'Base URL must start with http:// or https://' }; } const models = isPlainObject(config.models) ? config.models : null; if (!models || Object.keys(models).length === 0) { return { ok: false, error: 'At least one model is required' }; } const normalizedModels = {}; for (const [modelId, modelValue] of Object.entries(models)) { const trimmedId = typeof modelId === 'string' ? modelId.trim() : ''; if (!trimmedId) { return { ok: false, error: 'Model id is required' }; } if (!isPlainObject(modelValue)) { return { ok: false, error: `Model "${trimmedId}" must be an object` }; } const modelName = typeof modelValue.name === 'string' ? modelValue.name.trim() : ''; if (!modelName) { return { ok: false, error: `Model "${trimmedId}" requires a name` }; } normalizedModels[trimmedId] = { name: modelName }; } const normalized = { npm: OPENAI_COMPATIBLE_NPM, name, options: { baseURL, }, models: normalizedModels, }; if (Array.isArray(config.env)) { const env = config.env .filter((entry) => typeof entry === 'string' && entry.trim().length > 0) .map((entry) => entry.trim()); if (env.length > 0) { normalized.env = env; } } if (isPlainObject(options.headers)) { const headers = {}; for (const [headerKey, headerValue] of Object.entries(options.headers)) { if (typeof headerKey !== 'string' || !headerKey.trim()) { continue; } if (typeof headerValue !== 'string' || !headerValue.trim()) { return { ok: false, error: `Header "${headerKey}" requires a non-empty value` }; } headers[headerKey.trim()] = headerValue.trim(); } if (Object.keys(headers).length > 0) { normalized.options.headers = headers; } } return { ok: true, value: { providerId, config: normalized } }; } /** * Persist (create or update) a custom provider block in OpenCode user/project/custom config. * Does not write secrets — API keys remain in auth.json via the OpenCode auth API. */ function upsertProviderConfig(providerId, config, workingDirectory, scope = 'user') { const validated = validateCustomProviderConfig(providerId, config); if (!validated.ok) { const error = new Error(validated.error); error.statusCode = 400; throw error; } const layers = readConfigLayers(workingDirectory); let targetPath = layers.paths.userPath; if (scope === 'project') { if (!workingDirectory) { throw new Error('Working directory is required for project scope'); } targetPath = layers.paths.projectPath || targetPath; } else if (scope === 'custom') { if (!layers.paths.customPath) { throw new Error('Custom config path (OPENCODE_CONFIG) is not set'); } targetPath = layers.paths.customPath; } else if (scope !== 'user') { throw new Error('Invalid scope'); } const targetConfig = getConfigForPath(layers, targetPath); const providerConfig = isPlainObject(targetConfig.provider) ? { ...targetConfig.provider } : {}; providerConfig[validated.value.providerId] = validated.value.config; targetConfig.provider = providerConfig; if (Array.isArray(targetConfig.disabled_providers)) { targetConfig.disabled_providers = targetConfig.disabled_providers.filter( (entry) => entry !== validated.value.providerId, ); } const writePath = targetPath || CONFIG_FILE; writeConfig(targetConfig, writePath); return { providerId: validated.value.providerId, path: writePath, config: validated.value.config, }; } function removeProviderConfig(providerId, workingDirectory, scope = 'user') { if (!providerId || typeof providerId !== 'string') { throw new Error('Provider ID is required'); } const layers = readConfigLayers(workingDirectory); let targetPath = layers.paths.userPath; if (scope === 'project') { if (!workingDirectory) { throw new Error('Working directory is required for project scope'); } targetPath = layers.paths.projectPath || targetPath; } else if (scope === 'custom') { if (!layers.paths.customPath) { return false; } targetPath = layers.paths.customPath; } const targetConfig = getConfigForPath(layers, targetPath); const providerConfig = isPlainObject(targetConfig.provider) ? targetConfig.provider : {}; const providersConfig = isPlainObject(targetConfig.providers) ? targetConfig.providers : {}; const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId); const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId); if (!removedProvider && !removedProviders) { return false; } if (removedProvider) { delete providerConfig[providerId]; if (Object.keys(providerConfig).length === 0) { delete targetConfig.provider; } else { targetConfig.provider = providerConfig; } } if (removedProviders) { delete providersConfig[providerId]; if (Object.keys(providersConfig).length === 0) { delete targetConfig.providers; } else { targetConfig.providers = providersConfig; } } writeConfig(targetConfig, targetPath || CONFIG_FILE); console.log(`Removed provider ${providerId} from config: ${targetPath}`); return true; } export { getProviderSources, removeProviderConfig, upsertProviderConfig, validateCustomProviderConfig, };