From 2244e2a1526105a422c485f6d8b82b67d06c1677 Mon Sep 17 00:00:00 2001 From: Kayvan Sylvan Date: Mon, 16 Feb 2026 17:52:56 -0800 Subject: [PATCH 1/3] feat: add optional API key authentication to LM Studio client - Add optional API key setup question to client configuration - Add `ApiKey` field to the LM Studio `Client` struct - Create `addAuthorizationHeader` helper to attach Bearer token to requests - Apply authorization header to all outgoing HTTP requests - Skip authorization header when API key is empty or unset --- internal/plugins/ai/lmstudio/lmstudio.go | 19 ++++ internal/plugins/ai/lmstudio/lmstudio_test.go | 92 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 internal/plugins/ai/lmstudio/lmstudio_test.go diff --git a/internal/plugins/ai/lmstudio/lmstudio.go b/internal/plugins/ai/lmstudio/lmstudio.go index 169f7672..0b73e7c8 100644 --- a/internal/plugins/ai/lmstudio/lmstudio.go +++ b/internal/plugins/ai/lmstudio/lmstudio.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/danielmiessler/fabric/internal/chat" @@ -31,6 +32,7 @@ func NewClientCompatible(vendorName string, defaultBaseUrl string, configureCust ret.PluginBase = plugins.NewVendorPluginBase(vendorName, configureCustom) ret.ApiUrl = ret.AddSetupQuestionCustom("API URL", true, fmt.Sprintf(i18n.T("lmstudio_api_url_question"), vendorName, defaultBaseUrl)) + ret.ApiKey = ret.AddSetupQuestion("API key", false) return } @@ -38,6 +40,7 @@ func NewClientCompatible(vendorName string, defaultBaseUrl string, configureCust type Client struct { *plugins.PluginBase ApiUrl *plugins.SetupQuestion + ApiKey *plugins.SetupQuestion HttpClient *http.Client } @@ -55,6 +58,7 @@ func (c *Client) ListModels() ([]string, error) { if err != nil { return nil, fmt.Errorf(i18n.T("lmstudio_failed_create_request"), err) } + c.addAuthorizationHeader(req) resp, err := c.HttpClient.Do(req) if err != nil { @@ -109,6 +113,7 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha } req.Header.Set("Content-Type", "application/json") + c.addAuthorizationHeader(req) var resp *http.Response if resp, err = c.HttpClient.Do(req); err != nil { @@ -216,6 +221,7 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o } req.Header.Set("Content-Type", "application/json") + c.addAuthorizationHeader(req) var resp *http.Response if resp, err = c.HttpClient.Do(req); err != nil { @@ -278,6 +284,7 @@ func (c *Client) Complete(ctx context.Context, prompt string, opts *domain.ChatO } req.Header.Set("Content-Type", "application/json") + c.addAuthorizationHeader(req) var resp *http.Response if resp, err = c.HttpClient.Do(req); err != nil { @@ -334,6 +341,7 @@ func (c *Client) GetEmbeddings(ctx context.Context, input string, opts *domain.C } req.Header.Set("Content-Type", "application/json") + c.addAuthorizationHeader(req) var resp *http.Response if resp, err = c.HttpClient.Do(req); err != nil { @@ -370,3 +378,14 @@ func (c *Client) GetEmbeddings(ctx context.Context, input string, opts *domain.C func (c *Client) NeedsRawMode(modelName string) bool { return false } + +func (c *Client) addAuthorizationHeader(req *http.Request) { + if c.ApiKey == nil { + return + } + apiKey := strings.TrimSpace(c.ApiKey.Value) + if apiKey == "" { + return + } + req.Header.Set("Authorization", "Bearer "+apiKey) +} diff --git a/internal/plugins/ai/lmstudio/lmstudio_test.go b/internal/plugins/ai/lmstudio/lmstudio_test.go new file mode 100644 index 00000000..2bb1d96b --- /dev/null +++ b/internal/plugins/ai/lmstudio/lmstudio_test.go @@ -0,0 +1,92 @@ +package lmstudio + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/danielmiessler/fabric/internal/chat" + "github.com/danielmiessler/fabric/internal/domain" + "github.com/stretchr/testify/require" +) + +func TestListModelsUsesBearerTokenWhenConfigured(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/models", r.URL.Path) + require.Equal(t, "Bearer secret", r.Header.Get("Authorization")) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":[{"id":"model-1"}]}`)) + })) + defer server.Close() + + client := NewClient() + client.ApiUrl.Value = server.URL + client.ApiKey.Value = "secret" + client.HttpClient = server.Client() + + models, err := client.ListModels() + require.NoError(t, err) + require.Equal(t, []string{"model-1"}, models) +} + +func TestSendEndpointsUseBearerTokenWhenConfigured(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "Bearer secret", r.Header.Get("Authorization")) + switch r.URL.Path { + case "/chat/completions": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`)) + case "/completions": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"choices":[{"text":"ok"}]}`)) + case "/embeddings": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":[{"embedding":[1,2]}]}`)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + client := NewClient() + client.ApiUrl.Value = server.URL + client.ApiKey.Value = "secret" + client.HttpClient = server.Client() + + msgs := []*chat.ChatCompletionMessage{{Role: chat.ChatMessageRoleUser, Content: "hello"}} + opts := &domain.ChatOptions{Model: "test-model"} + + _, err := client.Send(context.Background(), msgs, opts) + require.NoError(t, err) + + _, err = client.Complete(context.Background(), "hello", opts) + require.NoError(t, err) + + _, err = client.GetEmbeddings(context.Background(), "hello", opts) + require.NoError(t, err) +} + +func TestListModelsDoesNotSendBearerForWhitespaceOnlyKey(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Empty(t, r.Header.Get("Authorization")) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":[{"id":"model-1"}]}`)) + })) + defer server.Close() + + client := NewClient() + client.ApiUrl.Value = server.URL + client.ApiKey.Value = " " + client.HttpClient = server.Client() + + models, err := client.ListModels() + require.NoError(t, err) + require.Equal(t, []string{"model-1"}, models) +} From a688c02991f0d6acad63162d3e57d49ae8f203f2 Mon Sep 17 00:00:00 2001 From: Kayvan Sylvan Date: Mon, 16 Feb 2026 17:57:27 -0800 Subject: [PATCH 2/3] chore: incoming 2007 changelog entry --- cmd/generate_changelog/incoming/2007.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 cmd/generate_changelog/incoming/2007.txt diff --git a/cmd/generate_changelog/incoming/2007.txt b/cmd/generate_changelog/incoming/2007.txt new file mode 100644 index 00000000..e763a58b --- /dev/null +++ b/cmd/generate_changelog/incoming/2007.txt @@ -0,0 +1,7 @@ +### PR [#2007](https://github.com/danielmiessler/Fabric/pull/2007) by [ksylvan](https://github.com/ksylvan): Add optional API key authentication to LM Studio client + +- Add optional API key authentication to LM Studio client +- Add optional API key setup question to client configuration +- Add `ApiKey` field to the LM Studio `Client` struct +- Create `addAuthorizationHeader` helper to attach Bearer token to requests +- Apply authorization header to all outgoing HTTP requests From e7441da0d9c581d728d2ffab1a2c977f953561be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Feb 2026 02:10:31 +0000 Subject: [PATCH 3/3] chore(release): Update version to v1.4.408 --- CHANGELOG.md | 10 ++++++++++ cmd/fabric/version.go | 2 +- cmd/generate_changelog/changelog.db | Bin 3960832 -> 3969024 bytes cmd/generate_changelog/incoming/2007.txt | 7 ------- nix/pkgs/fabric/version.nix | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) delete mode 100644 cmd/generate_changelog/incoming/2007.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d99426..02b09e80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v1.4.408 (2026-02-17) + +### PR [#2007](https://github.com/danielmiessler/Fabric/pull/2007) by [ksylvan](https://github.com/ksylvan): Add optional API key authentication to LM Studio client + +- Add optional API key authentication to LM Studio client +- Add optional API key setup question to client configuration +- Add `ApiKey` field to the LM Studio `Client` struct +- Create `addAuthorizationHeader` helper to attach Bearer token to requests +- Apply authorization header to all outgoing HTTP requests + ## v1.4.407 (2026-02-16) ### PR [#2005](https://github.com/danielmiessler/Fabric/pull/2005) by [ksylvan](https://github.com/ksylvan): I18N: For file manager, Vertex AI, and Copilot errors diff --git a/cmd/fabric/version.go b/cmd/fabric/version.go index f0a196ac..7ad6bc20 100644 --- a/cmd/fabric/version.go +++ b/cmd/fabric/version.go @@ -1,3 +1,3 @@ package main -var version = "v1.4.407" +var version = "v1.4.408" diff --git a/cmd/generate_changelog/changelog.db b/cmd/generate_changelog/changelog.db index 2961dc06fca939fceeb04119cec23e4a89f96ad0..f7e15e1252261bed08b38af2f30ca4e6e0c2872a 100644 GIT binary patch delta 5750 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 IE8mF!16lN7q5uE@ delta 862 zcmbWyO-NKx6bJBk-@G?(W}JD?Q71KZe2!n^$21@M=1VcnaZyPOA<-x@N_>ZVPbTKw+f-1|T0+{EN2 zN__f;ph%qIINjoyx);qn?(>$moDwRvhSUASBk{B6;=>ou9P@d6ewWAR^7^}c-f%-> zxY6JJr>3%+<_)v6QuAILOusEH$qW4D`ic5nL0{4@-0haHq7viU!pz$)B65$SW3mk7&Z1QKB$|-(Oj*)^slp*u;NKY60 z&}CU*-Q4LPC+dhgiW-yCLx@ff@!i>i@{T)1#01|#Ium@4v57aMH8jg@jz)*-`H-;h%#TZIN6S~TE5E1O{jLL2LR4`VKj^^XI5qXcDr* zZ0^0A$Vn{sRf#G+>SpBsvPmVQGG&Z|DQ7B}N~Vgbrb@>7eMI1X@kgkn)+7;uvYKb4 z75MCQ@jhcR?x