feat: remove API key requirement for local AI services

- Make API key optional for local services like Ollama
- Update validation logic to support key-less configurations
- Update UI descriptions and localization for optional API key

Signed-off-by: ChaoLiu <chaoliu719@gmail.com>
This commit is contained in:
ChaoLiu 2025-08-18 18:35:37 +08:00
parent b711e801af
commit 07fe11a3f9
5 changed files with 21 additions and 29 deletions

View File

@ -11,7 +11,7 @@ import (
"github.com/openai/openai-go/v2" "github.com/openai/openai-go/v2"
"github.com/openai/openai-go/v2/option" "github.com/openai/openai-go/v2/option"
storepb "github.com/usememos/memos/proto/gen/store" storepb "github.com/usememos/memos/proto/gen/store"
) )
@ -51,9 +51,9 @@ func LoadConfigFromEnv() *Config {
TimeoutSeconds: timeoutSeconds, TimeoutSeconds: timeoutSeconds,
} }
// Enable AI if all required fields are provided // Enable AI if all required fields are provided (API Key is optional for local services like Ollama)
config.Enabled = config.BaseURL != "" && config.APIKey != "" && config.Model != "" config.Enabled = config.BaseURL != "" && config.Model != ""
return config return config
} }
@ -81,7 +81,7 @@ func LoadConfigFromDatabase(aiSetting *storepb.WorkspaceAISetting) *Config {
// Environment variables take precedence if they are set // Environment variables take precedence if they are set
func (c *Config) MergeWithEnv() *Config { func (c *Config) MergeWithEnv() *Config {
envConfig := LoadConfigFromEnv() envConfig := LoadConfigFromEnv()
// Start with current config // Start with current config
merged := &Config{ merged := &Config{
Enabled: c.Enabled, Enabled: c.Enabled,
@ -90,7 +90,7 @@ func (c *Config) MergeWithEnv() *Config {
Model: c.Model, Model: c.Model,
TimeoutSeconds: c.TimeoutSeconds, TimeoutSeconds: c.TimeoutSeconds,
} }
// Override with env vars if they are set // Override with env vars if they are set
if envConfig.BaseURL != "" { if envConfig.BaseURL != "" {
merged.BaseURL = envConfig.BaseURL merged.BaseURL = envConfig.BaseURL
@ -104,16 +104,16 @@ func (c *Config) MergeWithEnv() *Config {
if os.Getenv("AI_TIMEOUT_SECONDS") != "" { if os.Getenv("AI_TIMEOUT_SECONDS") != "" {
merged.TimeoutSeconds = envConfig.TimeoutSeconds merged.TimeoutSeconds = envConfig.TimeoutSeconds
} }
// Enable if all required fields are present // Enable if all required fields are present (API Key is optional for local services like Ollama)
merged.Enabled = merged.BaseURL != "" && merged.APIKey != "" && merged.Model != "" merged.Enabled = merged.BaseURL != "" && merged.Model != ""
return merged return merged
} }
// IsConfigured returns true if AI is properly configured // IsConfigured returns true if AI is properly configured
func (c *Config) IsConfigured() bool { func (c *Config) IsConfigured() bool {
return c.Enabled && c.BaseURL != "" && c.APIKey != "" && c.Model != "" return c.Enabled && c.BaseURL != "" && c.Model != ""
} }
// Client wraps OpenAI client with convenience methods // Client wraps OpenAI client with convenience methods

View File

@ -397,12 +397,6 @@ func (s *APIV1Service) TestAiConnection(ctx context.Context, request *v1pb.TestA
Message: "Base URL is required", Message: "Base URL is required",
}, nil }, nil
} }
if request.ApiKey == "" {
return &v1pb.TestAiConnectionResponse{
Success: false,
Message: "API Key is required",
}, nil
}
if request.Model == "" { if request.Model == "" {
return &v1pb.TestAiConnectionResponse{ return &v1pb.TestAiConnectionResponse{
Success: false, Success: false,

View File

@ -48,8 +48,8 @@ const AISettings = observer(() => {
}; };
const updateSetting = async () => { const updateSetting = async () => {
if (aiSetting.enableAi && (!aiSetting.apiKey || !aiSetting.model)) { if (aiSetting.enableAi && !aiSetting.model) {
toast.error(t("setting.ai-section.api-key-model-required")); toast.error(t("setting.ai-section.model-required"));
return; return;
} }
@ -65,7 +65,7 @@ const AISettings = observer(() => {
const resetSetting = () => setAiSetting(originalSetting); const resetSetting = () => setAiSetting(originalSetting);
const testConnection = async () => { const testConnection = async () => {
if (!aiSetting.baseUrl || !aiSetting.apiKey || !aiSetting.model) { if (!aiSetting.baseUrl || !aiSetting.model) {
toast.error(t("setting.ai-section.test-connection-incomplete")); toast.error(t("setting.ai-section.test-connection-incomplete"));
return; return;
} }
@ -222,7 +222,7 @@ const AISettings = observer(() => {
<Button <Button
variant="outline" variant="outline"
onClick={testConnection} onClick={testConnection}
disabled={isTestingConnection || !aiSetting.baseUrl || !aiSetting.apiKey || !aiSetting.model} disabled={isTestingConnection || !aiSetting.baseUrl || !aiSetting.model}
> >
{isTestingConnection ? t("setting.ai-section.testing-connection") : t("setting.ai-section.test-connection")} {isTestingConnection ? t("setting.ai-section.testing-connection") : t("setting.ai-section.test-connection")}
</Button> </Button>

View File

@ -331,7 +331,7 @@
"base-url": "Base URL", "base-url": "Base URL",
"base-url-description": "API endpoint for your AI service (e.g., https://api.openai.com/v1)", "base-url-description": "API endpoint for your AI service (e.g., https://api.openai.com/v1)",
"api-key": "API Key", "api-key": "API Key",
"api-key-description": "Authentication key for your AI service", "api-key-description": "Authentication key for your AI service (optional for local services like Ollama)",
"model": "Model", "model": "Model",
"model-description": "AI model to use for processing requests (e.g., gpt-4o, claude-3-5-sonnet-20241022)", "model-description": "AI model to use for processing requests (e.g., gpt-4o, claude-3-5-sonnet-20241022)",
"timeout": "Timeout (seconds)", "timeout": "Timeout (seconds)",
@ -341,9 +341,8 @@
"test-connection-description": "Verify that your AI service configuration is working", "test-connection-description": "Verify that your AI service configuration is working",
"test-connection-success": "Connection successful! AI service is properly configured.", "test-connection-success": "Connection successful! AI service is properly configured.",
"test-connection-failed": "Connection failed. Please check your configuration.", "test-connection-failed": "Connection failed. Please check your configuration.",
"test-connection-incomplete": "Please fill in all required fields before testing.", "test-connection-incomplete": "Please fill in Base URL and Model before testing.",
"fields-required": "All fields are required when AI is enabled.", "model-required": "Model is required when AI is enabled."
"api-key-model-required": "API key and model are required when AI is enabled."
}, },
"tag-recommendation": { "tag-recommendation": {
"title": "Tag Recommendation", "title": "Tag Recommendation",

View File

@ -329,7 +329,7 @@
"base-url": "基础 URL", "base-url": "基础 URL",
"base-url-description": "AI 服务的 API 端点例如https://api.openai.com/v1", "base-url-description": "AI 服务的 API 端点例如https://api.openai.com/v1",
"api-key": "API 密钥", "api-key": "API 密钥",
"api-key-description": "AI 服务的身份验证密钥", "api-key-description": "AI 服务的身份验证密钥(本地服务如 Ollama 可选)",
"model": "模型", "model": "模型",
"model-description": "用于处理请求的 AI 模型例如gpt-4o, claude-3-5-sonnet-20241022", "model-description": "用于处理请求的 AI 模型例如gpt-4o, claude-3-5-sonnet-20241022",
"timeout": "超时(秒)", "timeout": "超时(秒)",
@ -339,9 +339,8 @@
"test-connection-description": "验证您的 AI 服务配置是否正常工作", "test-connection-description": "验证您的 AI 服务配置是否正常工作",
"test-connection-success": "连接成功AI 服务配置正确。", "test-connection-success": "连接成功AI 服务配置正确。",
"test-connection-failed": "连接失败。请检查您的配置。", "test-connection-failed": "连接失败。请检查您的配置。",
"test-connection-incomplete": "请在测试前填写所有必填字段。", "test-connection-incomplete": "请在测试前填写基础 URL 和模型。",
"fields-required": "启用 AI 时,所有字段都是必填的。", "model-required": "启用 AI 时,模型是必填的。"
"api-key-model-required": "启用 AI 时API 密钥和模型是必填的。"
}, },
"sso": "单点登录", "sso": "单点登录",
"sso-section": { "sso-section": {