diff --git a/config_embed.go b/config_embed.go new file mode 100644 index 000000000..b158e9449 --- /dev/null +++ b/config_embed.go @@ -0,0 +1,13 @@ +package main + +import ( + _ "embed" +) + +//go:embed config.example.yaml +var configExampleYAML []byte + +// GetConfigExampleYAML returns the embedded example config file +func GetConfigExampleYAML() []byte { + return configExampleYAML +} diff --git a/llama-swap.go b/llama-swap.go index 9706e07d1..1c68a25ce 100644 --- a/llama-swap.go +++ b/llama-swap.go @@ -97,6 +97,8 @@ func main() { currentPM.Shutdown() newPM := proxy.New(conf) newPM.SetVersion(date, commit, version) + newPM.SetConfigPath(*configPath) + newPM.SetConfigExample(GetConfigExampleYAML()) srv.Handler = newPM fmt.Println("Configuration Reloaded") @@ -114,6 +116,8 @@ func main() { } newPM := proxy.New(conf) newPM.SetVersion(date, commit, version) + newPM.SetConfigPath(*configPath) + newPM.SetConfigExample(GetConfigExampleYAML()) srv.Handler = newPM } } @@ -121,13 +125,15 @@ func main() { // load the initial proxy manager reloadProxyManager() debouncedReload := debounce(time.Second, reloadProxyManager) - if *watchConfig { - defer event.On(func(e proxy.ConfigFileChangedEvent) { - if e.ReloadingState == proxy.ReloadingStateStart { - debouncedReload() - } - })() + // Always listen for API-triggered config changes + defer event.On(func(e proxy.ConfigFileChangedEvent) { + if e.ReloadingState == proxy.ReloadingStateStart { + debouncedReload() + } + })() + + if *watchConfig { fmt.Println("Watching Configuration for changes") go func() { absConfigPath, err := filepath.Abs(*configPath) diff --git a/proxy/proxymanager.go b/proxy/proxymanager.go index 5a016bc50..c33c9f960 100644 --- a/proxy/proxymanager.go +++ b/proxy/proxymanager.go @@ -52,6 +52,12 @@ type ProxyManager struct { commit string version string + // config file path for editing + configPath string + + // embedded example config + configExample []byte + // peer proxy see: #296, #433 peerProxy *PeerProxy } @@ -966,3 +972,15 @@ func (pm *ProxyManager) SetVersion(buildDate string, commit string, version stri pm.commit = commit pm.version = version } + +func (pm *ProxyManager) SetConfigPath(configPath string) { + pm.Lock() + defer pm.Unlock() + pm.configPath = configPath +} + +func (pm *ProxyManager) SetConfigExample(configExample []byte) { + pm.Lock() + defer pm.Unlock() + pm.configExample = configExample +} diff --git a/proxy/proxymanager_api.go b/proxy/proxymanager_api.go index fe4326d0f..050581936 100644 --- a/proxy/proxymanager_api.go +++ b/proxy/proxymanager_api.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" + "os" "sort" "strings" @@ -31,6 +33,9 @@ func addApiHandlers(pm *ProxyManager) { apiGroup.GET("/events", pm.apiSendEvents) apiGroup.GET("/metrics", pm.apiGetMetrics) apiGroup.GET("/version", pm.apiGetVersion) + apiGroup.GET("/config/current", pm.apiGetCurrentConfig) + apiGroup.GET("/config/example", pm.apiGetExampleConfig) + apiGroup.POST("/config", pm.apiUpdateConfig) } } @@ -250,3 +255,65 @@ func (pm *ProxyManager) apiGetVersion(c *gin.Context) { "build_date": pm.buildDate, }) } + +func (pm *ProxyManager) apiGetCurrentConfig(c *gin.Context) { + pm.Lock() + configPath := pm.configPath + pm.Unlock() + + if configPath == "" { + pm.sendErrorResponse(c, http.StatusNotFound, "Config file path not set") + return + } + + data, err := os.ReadFile(configPath) + if err != nil { + pm.sendErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to read config file: %v", err)) + return + } + + c.Data(http.StatusOK, "text/yaml; charset=utf-8", data) +} + +func (pm *ProxyManager) apiGetExampleConfig(c *gin.Context) { + pm.Lock() + data := pm.configExample + pm.Unlock() + + if data == nil { + pm.sendErrorResponse(c, http.StatusInternalServerError, "Example config not available") + return + } + + c.Data(http.StatusOK, "text/yaml; charset=utf-8", data) +} + +func (pm *ProxyManager) apiUpdateConfig(c *gin.Context) { + pm.Lock() + configPath := pm.configPath + pm.Unlock() + + if configPath == "" { + pm.sendErrorResponse(c, http.StatusBadRequest, "Config file path not set") + return + } + + body, err := io.ReadAll(c.Request.Body) + if err != nil { + pm.sendErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Failed to read request body: %v", err)) + return + } + + // Write to config file + if err := os.WriteFile(configPath, body, 0644); err != nil { + pm.sendErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to write config file: %v", err)) + return + } + + // Trigger config reload event + event.Emit(ConfigFileChangedEvent{ + ReloadingState: ReloadingStateStart, + }) + + c.JSON(http.StatusOK, gin.H{"message": "Config updated successfully. Reloading..."}) +} diff --git a/test-config.yaml b/test-config.yaml new file mode 100644 index 000000000..15fd5784d --- /dev/null +++ b/test-config.yaml @@ -0,0 +1,264 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mostlygeek/llama-swap/refs/heads/main/config-schema.json +# +# llama-swap configuration for 16GB VRAM AMD Radeon RX 6800 XT (gfx1030) +# Optimized for headless system with no display overhead +# ------------------------------------- + +healthCheckTimeout: 300 +logLevel: info +logTimeFormat: "rfc3339" +logToStdout: "proxy" +metricsMaxInMemory: 1000 +startPort: 10001 +sendLoadingState: false +includeAliasesInList: false + +macros: + "latest-llama": > + /home/svc-gpgpu/.local/bin/llama-server + --port ${PORT} --host 0.0.0.0 -b 512 -ub 32 -np 1 + "default_ctx": 4096 + "rocm_device": "0" + +models: + # ======================================== + # GENERAL PURPOSE MODELS + # ======================================== + + "qwen3:14b-q5_k_m-32768": + cmd: | + ${latest-llama} + -hf Qwen/Qwen3-14B-GGUF:q5_k_m + --ctx-size 32768 + -fa auto + -ctv q8_0 + -ctk q8_0 + -ngl 99 + --jinja + --mmap + -b 512 + name: "qwen3:14b-q5_k_m-32768" + description: "VRAM: 12505 MiB" + ttl: 600 + + "qwen3:8b-q5_k_m-40960": + cmd: | + ${latest-llama} + -hf Qwen/Qwen3-8B-GGUF:q5_k_m + --ctx-size 40960 + -fa auto + -ctv q8_0 + -ctk q8_0 + -ngl 99 + --jinja + --mmap + -b 512 + name: "qwen3:8b-q5_k_m-40960" + description: "VRAM: 8491 MiB" + ttl: 600 + + "qwen3:8b-q8_0-32768": + cmd: | + ${latest-llama} + -hf Qwen/Qwen3-8B-GGUF:q8_0 + --ctx-size 32768 + -fa auto + -ctv q8_0 + -ctk q8_0 + -ngl 99 + --jinja + --mmap + -b 512 + name: "qwen3:8b-q8_0-32768" + description: "VRAM: 10381 MiB" + ttl: 600 + + "ministral-3:14b-instruct-q5_k_m-20480-vision": + cmd: | + ${latest-llama} + -hf mistralai/Ministral-3-14B-Instruct-2512-GGUF:q5_k_m + --ctx-size 20480 + -fa off + -ngl 99 + --mmap + --jinja + --mmproj-auto + name: "ministral-3:14b-instruct-q5_k_m-20480-vision" + description: "VRAM: 13184 MiB" + ttl: 600 + + "ministral-3:14b-reasoning-q5_k_m-20480-vision": + cmd: | + ${latest-llama} + -hf mistralai/Ministral-3-14B-Reasoning-2512-GGUF:q5_k_m + --ctx-size 20480 + -fa off + -ngl 99 + --mmap + --jinja + --mmproj-auto + name: "ministral-3:14b-reasoning-q5_k_m-20480-vision" + description: "VRAM: 13184 MiB" + ttl: 600 + + "ministral-3:14b-instruct-q5_k_m-32768": + cmd: | + ${latest-llama} + -hf mistralai/Ministral-3-14B-Instruct-2512-GGUF:q5_k_m + --ctx-size 32768 + -fa off + -ngl 99 + --mmap + --jinja + --no-mmproj + name: "ministral-3:14b-instruct-q5_k_m-32768" + description: "VRAM: 14224 MiB" + ttl: 600 + + "ministral-3:14b-reasoning-q5_k_m-32768": + cmd: | + ${latest-llama} + -hf mistralai/Ministral-3-14B-Reasoning-2512-GGUF:q5_k_m + --ctx-size 32768 + -fa off + -ngl 99 + --mmap + --jinja + --no-mmproj + name: "ministral-3:14b-reasoning-q5_k_m-32768" + description: "VRAM: 14224 MiB" + ttl: 600 + + # ======================================== + # UTILITY MODELS (General Purpose) + # ======================================== + + "embeddinggemma:300m": + cmd: | + ${latest-llama} + -hf gaianet/embeddinggemma-300m-GGUF + --ctx-size 2048 + -fa off + -ngl 99 + --embeddings + --pooling mean + -b 1024 + -ub 1024 + name: "embeddinggemma:300m" + description: "VRAM: 512 MiB" + ttl: 3600 + + "bge-reranker-v2-m3": + cmd: | + ${latest-llama} + -hf Felladrin/bge-reranker-v2-m3-Q8_0-GGUF + --ctx-size 8192 + -ngl 99 + --mmap + --rerank + --embedding + --pooling rank + -b 8192 + -ub 8192 + name: "bge-reranker-v2-m3" + description: "VRAM: 1077 MiB" + ttl: 3600 + + # ======================================== + # CODING MODELS + # ======================================== + + "qwen2.5-coder:14b-q5_k_m-32768": + cmd: | + ${latest-llama} + -hf Qwen/Qwen2.5-Coder-14B-Instruct-GGUF:q5_k_m + --ctx-size 32768 + -fa auto + -ctv q8_0 + -ctk q8_0 + -ngl 99 + --jinja + --mmap + -b 512 + name: "qwen2.5-coder:14b-q5_k_m-32768" + description: "VRAM: ~12500 MiB" + ttl: 600 + + "qwen2.5-coder:1.5b-q4_k_m-autocomplete": + cmd: | + ${latest-llama} + -hf Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF:q4_k_m + --ctx-size 2048 + -fa off + -ngl 99 + -b 128 + -ub 32 + --mmap + --no-warmup + name: "qwen2.5-coder:1.5b-q4_k_m-autocomplete" + description: "VRAM: ~1000 MiB" + ttl: 3600 + + # ======================================== + # PERSISTENT CPU MODEL + # ======================================== + + "qwen3:1.7b-cpu-json": + cmd: | + ${latest-llama} + -hf unsloth/Qwen3-1.7B-GGUF:Q4_K_M + --ctx-size 8192 + -fa off + -ngl 0 + -b 512 + --jinja + --mmap + name: "qwen3:1.7b-cpu-json" + description: "CPU-only - permanent RAM resident for tags/titles/queries" + ttl: 0 + +# ======================================== +# GROUPS CONFIGURATION +# ======================================== + +groups: + # General purpose models can coexist with utility models + # When loaded, they prevent coding group from running + "general-purpose": + swap: false # All models in group can run simultaneously + exclusive: true # Unloads other exclusive groups when active + members: + - "qwen3:14b-q5_k_m-32768" + - "qwen3:8b-q5_k_m-40960" + - "qwen3:8b-q8_0-32768" + - "ministral-3:14b-instruct-q5_k_m-20480-vision" + - "ministral-3:14b-reasoning-q5_k_m-20480-vision" + - "ministral-3:14b-instruct-q5_k_m-32768" + - "ministral-3:14b-reasoning-q5_k_m-32768" + - "bge-reranker-v2-m3" + + # Coding models can coexist with each other + # When loaded, they prevent general-purpose group from running + "coding": + swap: false # Both coder models can run simultaneously + exclusive: true # Unloads other exclusive groups when active + members: + - "qwen2.5-coder:14b-q5_k_m-32768" + - "qwen2.5-coder:1.5b-q4_k_m-autocomplete" + + # CPU-based persistent model - never unloaded, doesn't interfere + "persistent-cpu": + swap: false # No swapping (only one model anyway) + exclusive: false # Doesn't unload other groups + persistent: true # Other groups cannot unload this + members: + - "qwen3:1.7b-cpu-json" + +# ======================================== +# STARTUP HOOKS +# ======================================== + +hooks: + on_startup: + preload: + - "qwen3:1.7b-cpu-json" diff --git a/ui-svelte/package-lock.json b/ui-svelte/package-lock.json index 93150075d..57723b8e7 100644 --- a/ui-svelte/package-lock.json +++ b/ui-svelte/package-lock.json @@ -8,6 +8,12 @@ "name": "ui-svelte", "version": "0.0.0", "dependencies": { + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.12", + "codemirror": "^6.0.2", + "js-yaml": "^4.1.1", "svelte-spa-router": "^4.0.1" }, "devDependencies": { @@ -21,6 +27,102 @@ "vite": "^6.3.5" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz", + "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz", + "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.12", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.12.tgz", + "integrity": "sha512-f+/VsHVn/kOA9lltk/GFzuYwVVAKmOnNjxbrhkk3tPHntFqjWeI2TbIXx006YkBkqC10wZ4NsnWXCQiFPeAISQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -513,6 +615,47 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz", + "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", @@ -879,7 +1022,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -1206,7 +1348,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1214,6 +1355,12 @@ "node": ">=0.4.0" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -1260,6 +1407,27 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1438,6 +1606,18 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1775,7 +1955,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1903,13 +2082,18 @@ "node": ">=0.10.0" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/svelte": { "version": "5.48.5", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.48.5.tgz", "integrity": "sha512-NB3o70OxfmnE5UPyLr8uH3IV02Q43qJVAuWigYmsSOYsS0s/rHxP0TF81blG0onF/xkhNvZw4G8NfzIX+By5ZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2011,7 +2195,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2026,7 +2209,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2116,6 +2298,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", diff --git a/ui-svelte/package.json b/ui-svelte/package.json index 532967972..fbe35b9db 100644 --- a/ui-svelte/package.json +++ b/ui-svelte/package.json @@ -20,6 +20,12 @@ "vite": "^6.3.5" }, "dependencies": { + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.12", + "codemirror": "^6.0.2", + "js-yaml": "^4.1.1", "svelte-spa-router": "^4.0.1" } } diff --git a/ui-svelte/src/App.svelte b/ui-svelte/src/App.svelte index 69216703e..2e4ab9d11 100644 --- a/ui-svelte/src/App.svelte +++ b/ui-svelte/src/App.svelte @@ -5,6 +5,7 @@ import LogViewer from "./routes/LogViewer.svelte"; import Models from "./routes/Models.svelte"; import Activity from "./routes/Activity.svelte"; + import Config from "./routes/Config.svelte"; import { enableAPIEvents } from "./stores/api"; import { initScreenWidth, isDarkMode, appTitle, connectionState } from "./stores/theme"; @@ -12,6 +13,7 @@ "/": Models, "/logs": LogViewer, "/activity": Activity, + "/config": Config, "*": Models, }; diff --git a/ui-svelte/src/components/Header.svelte b/ui-svelte/src/components/Header.svelte index 73c66874b..4c7553ee2 100644 --- a/ui-svelte/src/components/Header.svelte +++ b/ui-svelte/src/components/Header.svelte @@ -68,6 +68,14 @@ > Logs + + Config + + + + + + + {#if validationError} +
+ Validation Error: {validationError} +
+ {/if} + + {#if error} +
+ {error} +
+ {/if} + + {#if loading} +
+
Loading configuration...
+
+ {:else} +
+ +
+

Current Config (Editable)

+
+
+ + +
+

Example Config (Reference)

+
+
+
+ {/if} + diff --git a/ui/package-lock.json b/ui/package-lock.json index c88133e7d..097d3bcca 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,6 +8,10 @@ "name": "ui", "version": "0.0.0", "dependencies": { + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/state": "^6.4.1", + "codemirror": "^6.0.1", + "js-yaml": "^4.1.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", @@ -17,6 +21,7 @@ "devDependencies": { "@eslint/js": "^9.25.0", "@tailwindcss/vite": "^4.1.8", + "@types/js-yaml": "^4.0.9", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", @@ -75,7 +80,6 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -327,6 +331,94 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz", + "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz", + "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.12", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.12.tgz", + "integrity": "sha512-f+/VsHVn/kOA9lltk/GFzuYwVVAKmOnNjxbrhkk3tPHntFqjWeI2TbIXx006YkBkqC10wZ4NsnWXCQiFPeAISQ==", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -1041,6 +1133,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz", + "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1755,6 +1883,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1768,7 +1902,6 @@ "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1829,7 +1962,6 @@ "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", @@ -2081,7 +2213,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2136,7 +2267,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { @@ -2190,7 +2320,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -2262,6 +2391,20 @@ "node": ">=18" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2309,6 +2452,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2457,7 +2605,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2927,7 +3074,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3567,7 +3713,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3577,7 +3722,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -3812,6 +3956,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3907,7 +4056,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3960,7 +4108,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4039,7 +4186,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4130,7 +4276,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4138,6 +4283,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index d8a5e0905..07cc38ea6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/state": "^6.4.1", + "codemirror": "^6.0.1", + "js-yaml": "^4.1.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", @@ -19,6 +23,7 @@ "devDependencies": { "@eslint/js": "^9.25.0", "@tailwindcss/vite": "^4.1.8", + "@types/js-yaml": "^4.0.9", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1",