From 3c756771ecc8e08def88e90368b7ef0f3758147f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 20:50:36 +0000 Subject: [PATCH] feat: llamacpp health check client Add internal/llamacpp package with Client type and Health() method. Client communicates with llama-server via HTTP; Health checks the /health endpoint and reports readiness. Foundation type for the streaming methods (Tasks 2-3). Co-Authored-By: Virgil Co-Authored-By: Claude Opus 4.6 --- go.mod | 7 +++++ go.sum | 10 ++++++ internal/llamacpp/health.go | 52 ++++++++++++++++++++++++++++++++ internal/llamacpp/health_test.go | 42 ++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 go.sum create mode 100644 internal/llamacpp/health.go create mode 100644 internal/llamacpp/health_test.go diff --git a/go.mod b/go.mod index 8fd3920..410861f 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,11 @@ go 1.25.5 require forge.lthn.ai/core/go-inference v0.0.0 +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 // indirect +) + replace forge.lthn.ai/core/go-inference => ../go-inference diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/llamacpp/health.go b/internal/llamacpp/health.go new file mode 100644 index 0000000..c642ccd --- /dev/null +++ b/internal/llamacpp/health.go @@ -0,0 +1,52 @@ +package llamacpp + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// Client communicates with a llama-server instance. +type Client struct { + baseURL string + httpClient *http.Client +} + +// NewClient creates a client for the llama-server at the given base URL. +func NewClient(baseURL string) *Client { + return &Client{ + baseURL: strings.TrimRight(baseURL, "/"), + httpClient: &http.Client{}, + } +} + +type healthResponse struct { + Status string `json:"status"` +} + +// Health checks whether the llama-server is ready to accept requests. +func (c *Client) Health(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/health", nil) + if err != nil { + return err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("llamacpp: health returned %d", resp.StatusCode) + } + var h healthResponse + if err := json.NewDecoder(resp.Body).Decode(&h); err != nil { + return fmt.Errorf("llamacpp: health decode: %w", err) + } + if h.Status != "ok" { + return fmt.Errorf("llamacpp: server not ready (status: %s)", h.Status) + } + return nil +} diff --git a/internal/llamacpp/health_test.go b/internal/llamacpp/health_test.go new file mode 100644 index 0000000..3a3a971 --- /dev/null +++ b/internal/llamacpp/health_test.go @@ -0,0 +1,42 @@ +package llamacpp + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHealth_OK(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/health", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"ok"}`)) + })) + defer ts.Close() + + c := NewClient(ts.URL) + err := c.Health(context.Background()) + require.NoError(t, err) +} + +func TestHealth_NotReady(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"loading model"}`)) + })) + defer ts.Close() + + c := NewClient(ts.URL) + err := c.Health(context.Background()) + assert.ErrorContains(t, err, "not ready") +} + +func TestHealth_ServerDown(t *testing.T) { + c := NewClient("http://127.0.0.1:1") // nothing listening + err := c.Health(context.Background()) + assert.Error(t, err) +}