* fix: update Docker workflow to use correct target stage name - Change target from bolt-ai-production to runtime - Matches the actual stage name in the new Dockerfile structure - Fixes CI failure: target stage 'bolt-ai-production' could not be found * feat: auto-enable local providers when configured via environment variables (#1881) - Add server-side API endpoint `/api/configured-providers` to detect environment-configured providers - Auto-enable Ollama, LMStudio, and OpenAILike providers when their environment variables are set - Filter out placeholder values (like "your_*_here") to only detect real configuration - Preserve user preferences: auto-enabling only applies on first load or previously auto-enabled providers - Track auto-enabled vs manually-enabled providers in localStorage for proper user choice handling - Solve issue where Ollama appears configured server-side but disabled in UI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -52,11 +52,37 @@ export const shortcutsStore = map<Shortcuts>({
|
||||
|
||||
// Create a single key for provider settings
|
||||
const PROVIDER_SETTINGS_KEY = 'provider_settings';
|
||||
const AUTO_ENABLED_KEY = 'auto_enabled_providers';
|
||||
|
||||
// Add this helper function at the top of the file
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
// Initialize provider settings from both localStorage and defaults
|
||||
// Interface for configured provider info from server
|
||||
interface ConfiguredProvider {
|
||||
name: string;
|
||||
isConfigured: boolean;
|
||||
configMethod: 'environment' | 'none';
|
||||
}
|
||||
|
||||
// Fetch configured providers from server
|
||||
const fetchConfiguredProviders = async (): Promise<ConfiguredProvider[]> => {
|
||||
try {
|
||||
const response = await fetch('/api/configured-providers');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { providers?: ConfiguredProvider[] };
|
||||
|
||||
return data.providers || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching configured providers:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize provider settings from both localStorage and server-detected configuration
|
||||
const getInitialProviderSettings = (): ProviderSetting => {
|
||||
const initialSettings: ProviderSetting = {};
|
||||
|
||||
@@ -92,8 +118,84 @@ const getInitialProviderSettings = (): ProviderSetting => {
|
||||
return initialSettings;
|
||||
};
|
||||
|
||||
// Auto-enable providers that are configured on the server
|
||||
const autoEnableConfiguredProviders = async () => {
|
||||
if (!isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configuredProviders = await fetchConfiguredProviders();
|
||||
const currentSettings = providersStore.get();
|
||||
const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
|
||||
const autoEnabledProviders = localStorage.getItem(AUTO_ENABLED_KEY);
|
||||
|
||||
// Track which providers were auto-enabled to avoid overriding user preferences
|
||||
const previouslyAutoEnabled = autoEnabledProviders ? JSON.parse(autoEnabledProviders) : [];
|
||||
const newlyAutoEnabled: string[] = [];
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
configuredProviders.forEach(({ name, isConfigured, configMethod }) => {
|
||||
if (isConfigured && configMethod === 'environment' && LOCAL_PROVIDERS.includes(name)) {
|
||||
const currentProvider = currentSettings[name];
|
||||
|
||||
if (currentProvider) {
|
||||
/*
|
||||
* Only auto-enable if:
|
||||
* 1. Provider is not already enabled, AND
|
||||
* 2. Either we haven't saved settings yet (first time) OR provider was previously auto-enabled
|
||||
*/
|
||||
const hasUserSettings = savedSettings !== null;
|
||||
const wasAutoEnabled = previouslyAutoEnabled.includes(name);
|
||||
const shouldAutoEnable = !currentProvider.settings.enabled && (!hasUserSettings || wasAutoEnabled);
|
||||
|
||||
if (shouldAutoEnable) {
|
||||
currentSettings[name] = {
|
||||
...currentProvider,
|
||||
settings: {
|
||||
...currentProvider.settings,
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
newlyAutoEnabled.push(name);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
// Update the store
|
||||
providersStore.set(currentSettings);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem(PROVIDER_SETTINGS_KEY, JSON.stringify(currentSettings));
|
||||
|
||||
// Update the auto-enabled providers list
|
||||
const allAutoEnabled = [...new Set([...previouslyAutoEnabled, ...newlyAutoEnabled])];
|
||||
localStorage.setItem(AUTO_ENABLED_KEY, JSON.stringify(allAutoEnabled));
|
||||
|
||||
console.log(`Auto-enabled providers: ${newlyAutoEnabled.join(', ')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error auto-enabling configured providers:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const providersStore = map<ProviderSetting>(getInitialProviderSettings());
|
||||
|
||||
// Export the auto-enable function for use in components
|
||||
export const initializeProviders = autoEnableConfiguredProviders;
|
||||
|
||||
// Initialize providers when the module loads (in browser only)
|
||||
if (isBrowser) {
|
||||
// Use a small delay to ensure DOM and other resources are ready
|
||||
setTimeout(() => {
|
||||
autoEnableConfiguredProviders();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Create a function to update provider settings that handles both store and persistence
|
||||
export const updateProviderSettings = (provider: string, settings: ProviderSetting) => {
|
||||
const currentSettings = providersStore.get();
|
||||
@@ -113,6 +215,37 @@ export const updateProviderSettings = (provider: string, settings: ProviderSetti
|
||||
// Save to localStorage
|
||||
const allSettings = providersStore.get();
|
||||
localStorage.setItem(PROVIDER_SETTINGS_KEY, JSON.stringify(allSettings));
|
||||
|
||||
// If this is a local provider, update the auto-enabled tracking
|
||||
if (LOCAL_PROVIDERS.includes(provider) && updatedProvider.settings.enabled !== undefined) {
|
||||
updateAutoEnabledTracking(provider, updatedProvider.settings.enabled);
|
||||
}
|
||||
};
|
||||
|
||||
// Update auto-enabled tracking when user manually changes provider settings
|
||||
const updateAutoEnabledTracking = (providerName: string, isEnabled: boolean) => {
|
||||
if (!isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const autoEnabledProviders = localStorage.getItem(AUTO_ENABLED_KEY);
|
||||
const currentAutoEnabled = autoEnabledProviders ? JSON.parse(autoEnabledProviders) : [];
|
||||
|
||||
if (isEnabled) {
|
||||
// If user enables provider, add to auto-enabled list (for future detection)
|
||||
if (!currentAutoEnabled.includes(providerName)) {
|
||||
currentAutoEnabled.push(providerName);
|
||||
localStorage.setItem(AUTO_ENABLED_KEY, JSON.stringify(currentAutoEnabled));
|
||||
}
|
||||
} else {
|
||||
// If user disables provider, remove from auto-enabled list (respect user choice)
|
||||
const updatedAutoEnabled = currentAutoEnabled.filter((name: string) => name !== providerName);
|
||||
localStorage.setItem(AUTO_ENABLED_KEY, JSON.stringify(updatedAutoEnabled));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating auto-enabled tracking:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const isDebugMode = atom(false);
|
||||
|
||||
110
app/routes/api.configured-providers.ts
Normal file
110
app/routes/api.configured-providers.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
|
||||
|
||||
interface ConfiguredProvider {
|
||||
name: string;
|
||||
isConfigured: boolean;
|
||||
configMethod: 'environment' | 'none';
|
||||
}
|
||||
|
||||
interface ConfiguredProvidersResponse {
|
||||
providers: ConfiguredProvider[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint that detects which providers are configured via environment variables
|
||||
* This helps auto-enable providers that have been set up by the user
|
||||
*/
|
||||
export const loader: LoaderFunction = async ({ context }) => {
|
||||
try {
|
||||
const llmManager = LLMManager.getInstance(context?.cloudflare?.env as any);
|
||||
const configuredProviders: ConfiguredProvider[] = [];
|
||||
|
||||
// Check each local provider for environment configuration
|
||||
for (const providerName of LOCAL_PROVIDERS) {
|
||||
const providerInstance = llmManager.getProvider(providerName);
|
||||
let isConfigured = false;
|
||||
let configMethod: 'environment' | 'none' = 'none';
|
||||
|
||||
if (providerInstance) {
|
||||
const config = providerInstance.config;
|
||||
|
||||
/*
|
||||
* Check if required environment variables are set
|
||||
* For providers with baseUrlKey (Ollama, LMStudio, OpenAILike)
|
||||
*/
|
||||
if (config.baseUrlKey) {
|
||||
const baseUrlEnvVar = config.baseUrlKey;
|
||||
const cloudflareEnv = (context?.cloudflare?.env as Record<string, any>)?.[baseUrlEnvVar];
|
||||
const processEnv = process.env[baseUrlEnvVar];
|
||||
const managerEnv = llmManager.env[baseUrlEnvVar];
|
||||
|
||||
const envBaseUrl = cloudflareEnv || processEnv || managerEnv;
|
||||
|
||||
/*
|
||||
* Only consider configured if environment variable is explicitly set
|
||||
* Don't count default config.baseUrl values or placeholder values
|
||||
*/
|
||||
const isValidEnvValue =
|
||||
envBaseUrl &&
|
||||
typeof envBaseUrl === 'string' &&
|
||||
envBaseUrl.trim().length > 0 &&
|
||||
!envBaseUrl.includes('your_') && // Filter out placeholder values like "your_openai_like_base_url_here"
|
||||
!envBaseUrl.includes('_here') &&
|
||||
envBaseUrl.startsWith('http'); // Must be a valid URL
|
||||
|
||||
if (isValidEnvValue) {
|
||||
isConfigured = true;
|
||||
configMethod = 'environment';
|
||||
}
|
||||
}
|
||||
|
||||
// For providers that might need API keys as well (check this separately, not as fallback)
|
||||
if (config.apiTokenKey && !isConfigured) {
|
||||
const apiTokenEnvVar = config.apiTokenKey;
|
||||
const envApiToken =
|
||||
(context?.cloudflare?.env as Record<string, any>)?.[apiTokenEnvVar] ||
|
||||
process.env[apiTokenEnvVar] ||
|
||||
llmManager.env[apiTokenEnvVar];
|
||||
|
||||
// Only consider configured if API key is set and not a placeholder
|
||||
const isValidApiToken =
|
||||
envApiToken &&
|
||||
typeof envApiToken === 'string' &&
|
||||
envApiToken.trim().length > 0 &&
|
||||
!envApiToken.includes('your_') && // Filter out placeholder values
|
||||
!envApiToken.includes('_here') &&
|
||||
envApiToken.length > 10; // API keys are typically longer than 10 chars
|
||||
|
||||
if (isValidApiToken) {
|
||||
isConfigured = true;
|
||||
configMethod = 'environment';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configuredProviders.push({
|
||||
name: providerName,
|
||||
isConfigured,
|
||||
configMethod,
|
||||
});
|
||||
}
|
||||
|
||||
return json<ConfiguredProvidersResponse>({
|
||||
providers: configuredProviders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error detecting configured providers:', error);
|
||||
|
||||
// Return default state on error
|
||||
return json<ConfiguredProvidersResponse>({
|
||||
providers: LOCAL_PROVIDERS.map((name) => ({
|
||||
name,
|
||||
isConfigured: false,
|
||||
configMethod: 'none' as const,
|
||||
})),
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user