refactor!: move LLM configuration from in-app settings to CLI/env vars

- Remove `api_endpoint` from Settings model and settings UI
- Add `--llm-endpoint` / `AETHERA_LLM_ENDPOINT` and `--llm-key` /
  `AETHERA_LLM_KEY` CLI flags (endpoint is required)
- Update client constructor to accept API key parameter
- Update tests and documentation to reflect new configuration approach

BREAKING CHANGE: LLM endpoint and key must now be provided via
`AETHERA_LLM_ENDPOINT` and `AETHERA_LLM_KEY` environment variables or
CLI flags instead of the Settings page.
This commit is contained in:
2026-05-01 23:30:34 -04:00
parent 54e24cb304
commit 74b8d43032
13 changed files with 47 additions and 71 deletions

View File

@@ -28,14 +28,18 @@ type API struct {
store store.Store
client *client.Client
dataDir string
llmEndpoint string
llmKey string
generationManager *generationManager
}
func New(s store.Store, dataDir string, logger *logrus.Logger) *API {
func New(s store.Store, dataDir string, logger *logrus.Logger, llmEndpoint, llmKey string) *API {
return &API{
store: s,
dataDir: dataDir,
logger: logger.WithField("service", "api"),
llmEndpoint: llmEndpoint,
llmKey: llmKey,
generationManager: newGenerationManager(),
}
}
@@ -68,24 +72,6 @@ func (a *API) PostSettings(w http.ResponseWriter, r *http.Request) {
return
}
if apiEndpoint := newSettings.APIEndpoint; apiEndpoint != "" {
baseURL, err := url.Parse(apiEndpoint)
if err != nil {
errMsg := fmt.Sprintf("Invalid API Endpoint URL: %q", baseURL)
log.WithError(err).Error(errMsg)
http.Error(w, errMsg, http.StatusBadRequest)
return
}
testClient := client.NewClient(baseURL)
if _, err := testClient.GetModels(r.Context()); err != nil {
log.WithError(err).Error("failed to access configured API endpoint")
http.Error(w, "API endpoint inaccessible", http.StatusBadRequest)
return
}
a.client = nil
}
if err := a.store.SaveSettings(&newSettings); err != nil {
log.WithError(err).Error("failed to save settings")
http.Error(w, "Failed to save application settings", http.StatusInternalServerError)
@@ -516,20 +502,13 @@ func (a *API) getClient() (*client.Client, error) {
return a.client, nil
}
// Get Settings & Validate Endpoint
settings, err := a.store.GetSettings()
if err != nil {
return nil, fmt.Errorf("failed to retrieve application settings: %w", err)
} else if settings.APIEndpoint == "" {
return nil, errors.New("no API endpoint configured in settings")
}
baseURL, err := url.Parse(settings.APIEndpoint)
// Parse LLM Endpoint from Config
baseURL, err := url.Parse(a.llmEndpoint)
if err != nil {
return nil, fmt.Errorf("invalid API endpoint URL: %w", err)
}
a.client = client.NewClient(baseURL)
a.client = client.NewClient(baseURL, a.llmKey)
return a.client, nil
}

View File

@@ -288,8 +288,12 @@ func populateUsageTimings(msgStats *types.MessageStats, usage openai.CompletionU
return didChange
}
func NewClient(baseURL *url.URL) *Client {
oaiClient := openai.NewClient(option.WithBaseURL(baseURL.String()))
func NewClient(baseURL *url.URL, apiKey string) *Client {
opts := []option.RequestOption{option.WithBaseURL(baseURL.String())}
if apiKey != "" {
opts = append(opts, option.WithAPIKey(apiKey))
}
oaiClient := openai.NewClient(opts...)
return &Client{oaiClient: &oaiClient}
}

View File

@@ -20,7 +20,7 @@ func TestSendMessage(t *testing.T) {
if err != nil {
t.Fatalf("Failed to parse base URL: %v", err)
}
client := NewClient(baseURL)
client := NewClient(baseURL, os.Getenv("AETHERA_LLM_KEY"))
// Create Context
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
@@ -69,7 +69,7 @@ func TestSummarizeChat(t *testing.T) {
if err != nil {
t.Fatalf("Failed to parse base URL: %v", err)
}
client := NewClient(baseURL)
client := NewClient(baseURL, os.Getenv("AETHERA_LLM_KEY"))
// Create Context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -96,7 +96,7 @@ func TestSendMessageWithImage(t *testing.T) {
if err != nil {
t.Fatalf("Failed to parse base URL: %v", err)
}
client := NewClient(baseURL)
client := NewClient(baseURL, os.Getenv("AETHERA_LLM_KEY"))
// Create Context
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)

View File

@@ -13,12 +13,12 @@ import (
"reichard.io/aethera/web"
)
func StartServer(settingsStore store.Store, dataDir, staticDir, listenAddress string, listenPort int) {
func StartServer(settingsStore store.Store, dataDir, staticDir, listenAddress string, listenPort int, llmEndpoint, llmKey string) {
mux := http.NewServeMux()
// Create API Instance - use settingsStore as the unified store for both settings and chat
logger := logrus.New()
api := api.New(settingsStore, dataDir, logger)
api := api.New(settingsStore, dataDir, logger, llmEndpoint, llmKey)
// Serve Static Assets
if staticDir != "" {

View File

@@ -141,7 +141,6 @@ func TestInMemoryStore_SaveSettings(t *testing.T) {
store := NewInMemoryStore()
settings := &Settings{
APIEndpoint: "http://example.com",
ImageEditSelector: ".image-edit",
ImageGenerationSelector: ".image-gen",
TextGenerationSelector: ".text-gen",
@@ -161,7 +160,6 @@ func TestInMemoryStore_GetSettings(t *testing.T) {
// Set some settings
settings = &Settings{
APIEndpoint: "http://example.com",
ImageEditSelector: ".image-edit",
ImageGenerationSelector: ".image-gen",
TextGenerationSelector: ".text-gen",
@@ -172,5 +170,5 @@ func TestInMemoryStore_GetSettings(t *testing.T) {
// Get the settings
settings, err = store.GetSettings()
require.NoError(t, err)
assert.Equal(t, "http://example.com", settings.APIEndpoint)
assert.Equal(t, ".image-edit", settings.ImageEditSelector)
}

View File

@@ -15,7 +15,6 @@ var _ Store = (*FileStore)(nil)
// Settings represents the application settings
type Settings struct {
APIEndpoint string `json:"api_endpoint,omitempty"`
ImageEditSelector string `json:"image_edit_selector,omitempty"`
ImageGenerationSelector string `json:"image_generation_selector,omitempty"`
TextGenerationSelector string `json:"text_generation_selector,omitempty"`

View File

@@ -209,7 +209,6 @@ func TestFileStore_SaveSettings(t *testing.T) {
require.NoError(t, err)
settings := &Settings{
APIEndpoint: "http://example.com",
ImageEditSelector: ".image-edit",
ImageGenerationSelector: ".image-gen",
TextGenerationSelector: ".text-gen",
@@ -237,7 +236,6 @@ func TestFileStore_GetSettings(t *testing.T) {
// Set some settings
settings = &Settings{
APIEndpoint: "http://example.com",
ImageEditSelector: ".image-edit",
ImageGenerationSelector: ".image-gen",
TextGenerationSelector: ".text-gen",
@@ -248,5 +246,5 @@ func TestFileStore_GetSettings(t *testing.T) {
// Get the settings
settings, err = store.GetSettings()
require.NoError(t, err)
assert.Equal(t, "http://example.com", settings.APIEndpoint)
assert.Equal(t, ".image-edit", settings.ImageEditSelector)
}