@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
JavaScript
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