UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

172 lines 7.79 kB
import { existsSync } from 'fs'; import { join } from 'path'; import { AISDKClient } from './ai-sdk-client/index.js'; import { getCopilotNoCredentialsMessage, loadCopilotCredential, } from './config/copilot-credentials.js'; import { getClosestConfigFile } from './config/index.js'; import { loadAllProviderConfigs } from './config/mcp-config-loader.js'; import { loadPreferences } from './config/preferences.js'; import { TIMEOUT_PROVIDER_CONNECTION_MS } from './constants.js'; import { isLocalURL } from './utils/url-utils.js'; // Custom error class for configuration errors that need special UI handling export class ConfigurationError extends Error { configPath; cwdPath; isEmptyConfig; constructor(message, configPath, cwdPath, isEmptyConfig = false) { super(message); this.configPath = configPath; this.cwdPath = cwdPath; this.isEmptyConfig = isEmptyConfig; this.name = 'ConfigurationError'; } } export async function createLLMClient(provider, model) { // Check if agents.config.json exists const agentsJsonPath = getClosestConfigFile('agents.config.json'); const hasConfigFile = existsSync(agentsJsonPath); // Use AI SDK - it handles both tool-calling and non-tool-calling models return createAISDKClient(provider, model, hasConfigFile); } async function createAISDKClient(requestedProvider, requestedModel, hasConfigFile = true) { // Load provider configs const providers = loadProviderConfigs(); const configPath = getClosestConfigFile('agents.config.json'); const cwd = process.cwd(); const isInCwd = configPath.startsWith(cwd); const cwdPath = !isInCwd ? join(cwd, 'agents.config.json') : undefined; if (providers.length === 0) { if (!hasConfigFile) { throw new ConfigurationError('No agents.config.json found', configPath, cwdPath, false); } else { throw new ConfigurationError('No providers configured in agents.config.json', configPath, cwdPath, true); } } // Determine which provider to try first let targetProvider; if (requestedProvider) { targetProvider = requestedProvider; } else { // Use preferences or default to first available provider const preferences = loadPreferences(); targetProvider = preferences.lastProvider || providers[0].name; } // Validate provider exists if specified if (requestedProvider) { const providerConfig = providers.find(p => p.name === requestedProvider); if (!providerConfig) { const availableProviders = providers.map(p => p.name).join(', '); throw new ConfigurationError(`Provider '${requestedProvider}' not found in agents.config.json. Available providers: ${availableProviders}`, configPath, cwdPath, false); } } // Validate model exists in the target provider's model list if specified if (requestedModel) { const resolvedProviderConfig = providers.find(p => p.name === targetProvider) || providers[0]; if (resolvedProviderConfig && resolvedProviderConfig.models.length > 0 && !resolvedProviderConfig.models.includes(requestedModel)) { const availableModels = resolvedProviderConfig.models.join(', '); throw new ConfigurationError(`Model '${requestedModel}' not available for provider '${resolvedProviderConfig.name}'. Available models: ${availableModels}`, configPath, cwdPath, false); } } // Order providers: requested first, then others const availableProviders = providers.map(p => p.name); const providerOrder = [ targetProvider, ...availableProviders.filter(p => p !== targetProvider), ]; const errors = []; for (const providerType of providerOrder) { try { const providerConfig = providers.find(p => p.name === providerType); if (!providerConfig) { continue; } // Test provider connection await testProviderConnection(providerConfig); const client = await AISDKClient.create(providerConfig); // Set model if specified if (requestedModel) { client.setModel(requestedModel); } return { client, actualProvider: providerType }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; errors.push(`${providerType}: ${errorMessage}`); } } // If we get here, all providers failed if (!hasConfigFile) { const combinedError = `No providers available: ${errors[0]?.split(': ')[1] || 'Unknown error'}\n\nPlease create an agents.config.json file with provider configuration.`; throw new Error(combinedError); } else { const combinedError = `All configured providers failed:\n${errors .map(e => `• ${e}`) .join('\n')}\n\nPlease check your provider configuration in agents.config.json`; throw new Error(combinedError); } } function loadProviderConfigs() { // Use the new hierarchical provider loading system to get providers from all levels const allProviderConfigs = loadAllProviderConfigs(); return allProviderConfigs.map(provider => ({ name: provider.name, type: 'openai', models: provider.models || [], requestTimeout: provider.requestTimeout, socketTimeout: provider.socketTimeout, connectionPool: provider.connectionPool, // Tool configuration disableTools: provider.disableTools, disableToolModels: provider.disableToolModels, // SDK provider package to use sdkProvider: provider.sdkProvider, config: { baseURL: provider.baseUrl, apiKey: provider.apiKey || 'dummy-key', headers: provider.headers ?? {}, }, })); } async function testProviderConnection(providerConfig) { // Test local servers for connectivity if (providerConfig.config.baseURL && isLocalURL(providerConfig.config.baseURL)) { try { await fetch(providerConfig.config.baseURL, { signal: AbortSignal.timeout(TIMEOUT_PROVIDER_CONNECTION_MS), headers: providerConfig.config.headers, }); // Don't check response.ok as some servers return 404 for root path // We just need to confirm the server responded (not a network error) } catch (error) { // Only throw if it's a network error, not a 404 or other HTTP response if (error instanceof TypeError) { throw new Error(`Server not accessible at ${providerConfig.config.baseURL}`); } // For AbortError (timeout), also throw if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Server not accessible at ${providerConfig.config.baseURL}`); } // Other errors (like HTTP errors) mean the server is responding, so pass } } // GitHub Copilot: require stored credential instead of apiKey if (providerConfig.sdkProvider === 'github-copilot') { const credential = loadCopilotCredential(providerConfig.name); if (!credential?.oauthToken) { throw new Error(getCopilotNoCredentialsMessage(providerConfig.name)); } return; } // Require API key for other hosted providers if (!providerConfig.config.apiKey && !(providerConfig.config.baseURL && isLocalURL(providerConfig.config.baseURL))) { throw new Error('API key required for hosted providers'); } } //# sourceMappingURL=client-factory.js.map