diff --git a/README.md b/README.md index e0febc9..82ea553 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Built in Go for performance and simplicity, llama-swap has zero dependencies and - `v1/chat/completions` - `v1/responses` - `v1/embeddings` + - `v1/models` - list available models - `v1/audio/speech` ([#36](https://github.com/mostlygeek/llama-swap/issues/36)) - `v1/audio/transcriptions` ([docs](https://github.com/mostlygeek/llama-swap/issues/41#issuecomment-2722637867)) - `v1/audio/voices` @@ -39,9 +40,17 @@ Built in Go for performance and simplicity, llama-swap has zero dependencies and - ✅ llama-swap API - `/ui` - web UI - `/upstream/:model_id` - direct access to upstream server ([demo](https://github.com/mostlygeek/llama-swap/pull/31)) - - `/models/unload` - manually unload running models ([#58](https://github.com/mostlygeek/llama-swap/issues/58)) - `/running` - list currently running models ([#61](https://github.com/mostlygeek/llama-swap/issues/61)) - - `/log` - remote log monitoring + - `POST /api/models/unload` - manually unload all running models ([#58](https://github.com/mostlygeek/llama-swap/issues/58)) + - `POST /api/models/unload/:model_id` - unload a specific model + - `/logs` - remote log monitoring + - `GET /logs` returns buffered plain text logs. + - If `Accept: text/html` is sent, `/logs` redirects to `/ui/`. + - `GET /logs/stream` keeps the connection open for live log streaming. + - Stream endpoints send buffered history first by default; add `?no-history` to stream only new lines. + - `GET /logs/stream/proxy` streams proxy logs only. + - `GET /logs/stream/upstream` streams upstream process logs only. + - `GET /logs/stream/{model_id}` streams logs for one model (including IDs with slashes, like `author/model`). - `/health` - just returns "OK" - ✅ API Key support - define keys to restrict access to API endpoints - ✅ Customizable diff --git a/proxy/proxymanager.go b/proxy/proxymanager.go index 6b27508..80bedc6 100644 --- a/proxy/proxymanager.go +++ b/proxy/proxymanager.go @@ -683,7 +683,7 @@ func (pm *ProxyManager) proxyToUpstream(c *gin.Context) { searchModelName, modelID, remainingPath, modelFound := pm.findModelInPath(upstreamPath) if !modelFound { - pm.sendErrorResponse(c, http.StatusBadRequest, "model id required in path") + pm.sendErrorResponse(c, http.StatusNotFound, "model not found") return } diff --git a/proxy/proxymanager_loghandlers.go b/proxy/proxymanager_loghandlers.go index daeb786..484d26c 100644 --- a/proxy/proxymanager_loghandlers.go +++ b/proxy/proxymanager_loghandlers.go @@ -32,6 +32,13 @@ func (pm *ProxyManager) streamLogsHandler(c *gin.Context) { c.Header("X-Accel-Buffering", "no") logMonitorId := strings.TrimPrefix(c.Param("logMonitorID"), "/") + + // Handle case where query string might be included in the parameter + // (can happen with catch-all routes on some versions/setups) + if idx := strings.Index(logMonitorId, "?"); idx != -1 { + logMonitorId = logMonitorId[:idx] + } + logger, err := pm.getLogger(logMonitorId) if err != nil { c.String(http.StatusBadRequest, err.Error()) diff --git a/proxy/proxymanager_loghandlers_test.go b/proxy/proxymanager_loghandlers_test.go new file mode 100644 index 0000000..21c3a9b --- /dev/null +++ b/proxy/proxymanager_loghandlers_test.go @@ -0,0 +1,49 @@ +package proxy + +import ( + "strings" + "testing" +) + +func TestLogMonitorIdQueryParameterStripping(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "upstream without query param", + input: "upstream", + expected: "upstream", + }, + { + name: "upstream with query param", + input: "upstream?no-history", + expected: "upstream", + }, + { + name: "proxy with multiple query params", + input: "proxy?no-history&foo=bar", + expected: "proxy", + }, + { + name: "model with slash and query param", + input: "author/model?no-history", + expected: "author/model", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Simulate the query parameter stripping logic + logMonitorId := tt.input + if idx := strings.Index(logMonitorId, "?"); idx != -1 { + logMonitorId = logMonitorId[:idx] + } + + if logMonitorId != tt.expected { + t.Errorf("Query parameter stripping failed: got %q, want %q", logMonitorId, tt.expected) + } + }) + } +}