From 77b2532adcf2f43ab4ed83b2e2ab6a0881fd42e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey=20=28they/them=29?= Date: Tue, 17 Mar 2026 17:03:19 -0700 Subject: [PATCH 1/3] feat(glyph): declarative Open WebUI model config via API sync Add a oneshot systemd service (open-webui-model-sync) that syncs model configuration with Open WebUI via its API after deployment. Models listed in the `models` attrset are activated with full config: capabilities, tool servers (MCPJungle), default features (web search, code interpreter), and builtin tools. All unlisted models from the API provider are automatically deactivated. The sync service: - Runs 10s after activation via a systemd timer - Re-triggers when the model config hash changes - Waits for Open WebUI to be ready before syncing - Uses POST /api/v1/models/model/update for active models - Uses POST /api/v1/models/model/toggle for deactivation - Checks current state before toggling to ensure idempotency Requires an Open WebUI API key in open-webui-api-key.age. Co-Authored-By: Claude Opus 4.6 --- hosts/glyph/services/open-webui.nix | 130 +++++++++++++++++++++++++++- lib/secrets/glyph.nix | 1 + 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/hosts/glyph/services/open-webui.nix b/hosts/glyph/services/open-webui.nix index 1e1223ef..750d6b10 100644 --- a/hosts/glyph/services/open-webui.nix +++ b/hosts/glyph/services/open-webui.nix @@ -1,19 +1,145 @@ { config, inputs, + lib, pkgs, ... -}: { +}: let + port = 8888; + baseUrl = "http://127.0.0.1:${toString port}"; + + # Shared model defaults + defaultCapabilities = { + file_context = true; + vision = true; + file_upload = true; + web_search = true; + image_generation = true; + code_interpreter = true; + citations = true; + status_updates = true; + builtin_tools = true; + }; + + defaultBuiltinTools = { + time = true; + memory = true; + chats = true; + notes = true; + knowledge = true; + channels = true; + web_search = true; + image_generation = true; + code_interpreter = true; + }; + + defaultMeta = { + capabilities = defaultCapabilities; + toolIds = ["server:mcp:glyph"]; + defaultFeatureIds = ["web_search" "code_interpreter"]; + builtinTools = defaultBuiltinTools; + }; + + # Active models — listed models are enabled with full config. + # All other models from the API provider are deactivated automatically. + models = { + "claude-sonnet-4-6" = {}; + "claude-opus-4-6" = {}; + "claude-haiku-4-5-20251001" = {}; + }; + + modelIds = builtins.toJSON (builtins.attrNames models); +in { age.secrets.open-webui-env.file = ./../secrets/open-webui-env.age; + age.secrets.open-webui-api-key = { + file = ./../secrets/open-webui-api-key.age; + mode = "440"; + }; systemd.services.open-webui.restartTriggers = [config.age.secrets.open-webui-env.file]; + # Sync model configuration after open-webui starts + systemd.timers.open-webui-model-sync = { + description = "Trigger Open WebUI model sync"; + wantedBy = ["timers.target"]; + restartTriggers = [(builtins.hashString "sha256" (builtins.toJSON models))]; + timerConfig.OnActiveSec = "10s"; + }; + + systemd.services.open-webui-model-sync = { + description = "Sync model configuration with Open WebUI"; + after = ["open-webui.service"]; + requires = ["open-webui.service"]; + restartIfChanged = false; + path = [pkgs.curl pkgs.jq]; + script = let + mkModelForm = id: attrs: + builtins.toJSON { + inherit id; + is_active = true; + name = attrs.name or id; + meta = defaultMeta // (attrs.meta or {}); + params = attrs.params or {}; + }; + + mkModelUpdate = id: attrs: let + form = mkModelForm id attrs; + in '' + update_model "${id}" '${form}' & + ''; + in '' + API_KEY=$(cat ${config.age.secrets.open-webui-api-key.path}) + ACTIVE_IDS='${modelIds}' + + update_model() { + local id=$1 form=$2 + echo "Configuring $id..." + http_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "$form" \ + "${baseUrl}/api/v1/models/model/update") + + if [ "$http_code" = "200" ]; then + echo "$id: updated." + else + echo "ERROR: failed to update $id (HTTP $http_code)" + fi + } + + # Wait for open-webui to be ready + for i in $(seq 1 30); do + if curl -sf "${baseUrl}/api/models" -H "Authorization: Bearer $API_KEY" >/dev/null 2>&1; then + break + fi + echo "Waiting for Open WebUI (attempt $i/30)..." + sleep 2 + done + + # Activate and configure listed models + ${lib.concatStringsSep "\n" (lib.mapAttrsToList mkModelUpdate models)} + wait + + # Deactivate all unlisted models that are currently active + curl -sf -H "Authorization: Bearer $API_KEY" \ + "${baseUrl}/api/v1/models/list" \ + | jq -r '.data[] | select(.is_active == true) | .id' \ + | while read -r id; do + if ! echo "$ACTIVE_IDS" | jq -e --arg id "$id" 'index($id)' >/dev/null 2>&1; then + curl -sf -X POST -H "Authorization: Bearer $API_KEY" \ + "${baseUrl}/api/v1/models/model/toggle?id=$id" >/dev/null 2>&1 + echo "$id: deactivated." + fi + done + ''; + }; + services.open-webui = { enable = true; package = pkgs.open-webui.overridePythonAttrs (old: { dependencies = old.dependencies ++ old.optional-dependencies.postgres; }); - port = 8888; + inherit port; host = "0.0.0.0"; environmentFile = config.age.secrets.open-webui-env.path; environment = { diff --git a/lib/secrets/glyph.nix b/lib/secrets/glyph.nix index 135a9eec..20f33a62 100644 --- a/lib/secrets/glyph.nix +++ b/lib/secrets/glyph.nix @@ -7,6 +7,7 @@ in { "hosts/glyph/secrets/kagi-api-key.age".publicKeys = keys; "hosts/glyph/secrets/context7-api-key.age".publicKeys = keys; "hosts/glyph/secrets/open-terminal-env.age".publicKeys = keys; + "hosts/glyph/secrets/open-webui-api-key.age".publicKeys = keys; "hosts/glyph/secrets/open-webui-env.age".publicKeys = keys; "hosts/glyph/secrets/graphite-auth-token.age".publicKeys = keys; "hosts/glyph/secrets/attic-credentials.age".publicKeys = keys; From 4b5ca71d7230d98c59d113ddd74b67d3e6834e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey=20=28they/them=29?= Date: Tue, 17 Mar 2026 17:05:32 -0700 Subject: [PATCH 2/3] add api key secret --- hosts/glyph/secrets/open-webui-api-key.age | Bin 0 -> 358 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hosts/glyph/secrets/open-webui-api-key.age diff --git a/hosts/glyph/secrets/open-webui-api-key.age b/hosts/glyph/secrets/open-webui-api-key.age new file mode 100644 index 0000000000000000000000000000000000000000..e5aba1f57906ab40d9e9c149924ca34ab00f4f8c GIT binary patch literal 358 zcmZ9_JyU{U007|b+PJRPMs^NAPzZ-UQ1DfhF989Y5)4HHc>xvgnrdllYwND-x~`#y z?wV?Hc4=zp8XIfr+S_~nz;odC@SbnObQ})?KNCPVJIN#X4jM&$yyp-knJ=MGg&1Ve zAPkL;Tqs3AYSm3j6g9ax{*ZwO&M2OQ8pi|Ejp%^n5FE$RGHLqFtB7jgMV!G(Ja>)AE$pv|QzP#e!tJKG9{(iA>D?OH#z2(F1 pulN?vU(Ti-xr9B3>|G{vy}7?=+z6j*%b%y5M^#oYPgl0T{{T^)eHZ`$ literal 0 HcmV?d00001 From f0ef732178786b447eb07f0f9c43c15d5c5a9a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey=20=28they/them=29?= Date: Tue, 17 Mar 2026 17:21:09 -0700 Subject: [PATCH 3/3] feat(glyph): enable native function calling for agentic search Set params.function_calling to "native" for all active models, enabling agentic web search where the model uses native tool calling to generate search queries and process results. Co-Authored-By: Claude Opus 4.6 --- hosts/glyph/services/open-webui.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/open-webui.nix b/hosts/glyph/services/open-webui.nix index 750d6b10..eae71627 100644 --- a/hosts/glyph/services/open-webui.nix +++ b/hosts/glyph/services/open-webui.nix @@ -79,7 +79,7 @@ in { is_active = true; name = attrs.name or id; meta = defaultMeta // (attrs.meta or {}); - params = attrs.params or {}; + params = {function_calling = "native";} // (attrs.params or {}); }; mkModelUpdate = id: attrs: let