From 86e9b93c371cdec5bc8510e423a78462125fe271 Mon Sep 17 00:00:00 2001 From: Ryan Steed Date: Mon, 17 Nov 2025 18:43:47 +0000 Subject: [PATCH] proxy,ui: add version endpoint and display version info in UI (#395) - Add /api/version endpoint to ProxyManager that returns build date, commit hash, and version - Implement SetVersion method to configure version info in ProxyManager - Add version info fetching to APIProvider and display in ConnectionStatus component - Include version info in UI context and update dependencies - Add tests for version endpoint functionality --- llama-swap.go | 8 ++++-- proxy/proxymanager.go | 17 ++++++++++++ proxy/proxymanager_api.go | 9 ++++++ proxy/proxymanager_test.go | 38 ++++++++++++++++++++++++++ ui/src/components/ConnectionStatus.tsx | 4 +-- ui/src/contexts/APIProvider.tsx | 36 +++++++++++++++++++++++- 6 files changed, 107 insertions(+), 5 deletions(-) diff --git a/llama-swap.go b/llama-swap.go index 95fb67c..9706e07 100644 --- a/llama-swap.go +++ b/llama-swap.go @@ -95,7 +95,9 @@ func main() { fmt.Println("Configuration Changed") currentPM.Shutdown() - srv.Handler = proxy.New(conf) + newPM := proxy.New(conf) + newPM.SetVersion(date, commit, version) + srv.Handler = newPM fmt.Println("Configuration Reloaded") // wait a few seconds and tell any UI to reload @@ -110,7 +112,9 @@ func main() { fmt.Printf("Error, unable to load configuration: %v\n", err) os.Exit(1) } - srv.Handler = proxy.New(conf) + newPM := proxy.New(conf) + newPM.SetVersion(date, commit, version) + srv.Handler = newPM } } diff --git a/proxy/proxymanager.go b/proxy/proxymanager.go index 8367387..39295e0 100644 --- a/proxy/proxymanager.go +++ b/proxy/proxymanager.go @@ -45,6 +45,11 @@ type ProxyManager struct { // shutdown signaling shutdownCtx context.Context shutdownCancel context.CancelFunc + + // version info + buildDate string + commit string + version string } func New(config config.Config) *ProxyManager { @@ -122,6 +127,10 @@ func New(config config.Config) *ProxyManager { shutdownCtx: shutdownCtx, shutdownCancel: shutdownCancel, + + buildDate: "unknown", + commit: "abcd1234", + version: "0", } // create the process groups @@ -770,3 +779,11 @@ func (pm *ProxyManager) findGroupByModelName(modelName string) *ProcessGroup { } return nil } + +func (pm *ProxyManager) SetVersion(buildDate string, commit string, version string) { + pm.Lock() + defer pm.Unlock() + pm.buildDate = buildDate + pm.commit = commit + pm.version = version +} diff --git a/proxy/proxymanager_api.go b/proxy/proxymanager_api.go index 76f6cd3..a296ee8 100644 --- a/proxy/proxymanager_api.go +++ b/proxy/proxymanager_api.go @@ -28,6 +28,7 @@ func addApiHandlers(pm *ProxyManager) { apiGroup.POST("/models/unload/*model", pm.apiUnloadSingleModelHandler) apiGroup.GET("/events", pm.apiSendEvents) apiGroup.GET("/metrics", pm.apiGetMetrics) + apiGroup.GET("/version", pm.apiGetVersion) } } @@ -227,3 +228,11 @@ func (pm *ProxyManager) apiUnloadSingleModelHandler(c *gin.Context) { c.String(http.StatusOK, "OK") } } + +func (pm *ProxyManager) apiGetVersion(c *gin.Context) { + c.JSON(http.StatusOK, map[string]string{ + "version": pm.version, + "commit": pm.commit, + "build_date": pm.buildDate, + }) +} diff --git a/proxy/proxymanager_test.go b/proxy/proxymanager_test.go index bbe5e93..c2f41f4 100644 --- a/proxy/proxymanager_test.go +++ b/proxy/proxymanager_test.go @@ -1147,3 +1147,41 @@ func TestProxyManager_ProxiedStreamingEndpointReturnsNoBufferingHeader(t *testin assert.Equal(t, "no", rec.Header().Get("X-Accel-Buffering")) assert.Contains(t, rec.Header().Get("Content-Type"), "text/event-stream") } + +func TestProxyManager_ApiGetVersion(t *testing.T) { + config := config.AddDefaultGroupToConfig(config.Config{ + HealthCheckTimeout: 15, + Models: map[string]config.ModelConfig{ + "model1": getTestSimpleResponderConfig("model1"), + }, + LogLevel: "error", + }) + + // Version test map + versionTest := map[string]string{ + "build_date": "1970-01-01T00:00:00Z", + "commit": "cc915ddb6f04a42d9cd1f524e1d46ec6ed069fdc", + "version": "v001", + } + + proxy := New(config) + proxy.SetVersion(versionTest["build_date"], versionTest["commit"], versionTest["version"]) + defer proxy.StopProcesses(StopWaitForInflightRequest) + + req := httptest.NewRequest("GET", "/api/version", nil) + w := CreateTestResponseRecorder() + + proxy.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + // Ensure json response + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) + + // Check for attributes + response := map[string]string{} + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &response)) + for key, value := range versionTest { + assert.Equal(t, value, response[key], "%s value %s should match response %s", key, value, response[key]) + } +} diff --git a/ui/src/components/ConnectionStatus.tsx b/ui/src/components/ConnectionStatus.tsx index 52285b6..e22c5f3 100644 --- a/ui/src/components/ConnectionStatus.tsx +++ b/ui/src/components/ConnectionStatus.tsx @@ -2,7 +2,7 @@ import { useAPI } from "../contexts/APIProvider"; import { useMemo } from "react"; const ConnectionStatusIcon = () => { - const { connectionStatus } = useAPI(); + const { connectionStatus, versionInfo } = useAPI(); const eventStatusColor = useMemo(() => { switch (connectionStatus) { @@ -17,7 +17,7 @@ const ConnectionStatusIcon = () => { }, [connectionStatus]); return ( -
+
); diff --git a/ui/src/contexts/APIProvider.tsx b/ui/src/contexts/APIProvider.tsx index b8708b9..3740a1f 100644 --- a/ui/src/contexts/APIProvider.tsx +++ b/ui/src/contexts/APIProvider.tsx @@ -23,6 +23,7 @@ interface APIProviderType { upstreamLogs: string; metrics: Metrics[]; connectionStatus: ConnectionState; + versionInfo: VersionInfo; } interface Metrics { @@ -41,11 +42,18 @@ interface LogData { source: "upstream" | "proxy"; data: string; } + interface APIEventEnvelope { type: "modelStatus" | "logData" | "metrics"; data: string; } +interface VersionInfo { + build_date: string; + commit: string; + version: string; +} + const APIContext = createContext(undefined); type APIProviderProps = { children: ReactNode; @@ -59,6 +67,11 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider const [upstreamLogs, setUpstreamLogs] = useState(""); const [metrics, setMetrics] = useState([]); const [connectionStatus, setConnectionState] = useState("disconnected"); + const [versionInfo, setVersionInfo] = useState({ + build_date: "unknown", + commit: "unknown", + version: "unknown" + }); //const apiEventSource = useRef(null); const [models, setModels] = useState([]); @@ -152,6 +165,26 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider connect(); }, []); + useEffect(() => { + // fetch version + const fetchVersion = async () => { + try { + const response = await fetch("/api/version"); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data: VersionInfo = await response.json(); + setVersionInfo(data); + } catch (error) { + console.error(error); + } + }; + + if (connectionStatus === 'connected') { + fetchVersion(); + } + }, [connectionStatus]); + useEffect(() => { if (autoStartAPIEvents) { enableAPIEvents(true); @@ -230,8 +263,9 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider upstreamLogs, metrics, connectionStatus, + versionInfo, }), - [models, listModels, unloadAllModels, loadModel, enableAPIEvents, proxyLogs, upstreamLogs, metrics] + [models, listModels, unloadAllModels, unloadSingleModel, loadModel, enableAPIEvents, proxyLogs, upstreamLogs, metrics, connectionStatus, versionInfo] ); return {children};