Skip to content

Commit 77b2532

Browse files
stackptrclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 32f6914 commit 77b2532

2 files changed

Lines changed: 129 additions & 2 deletions

File tree

‎hosts/glyph/services/open-webui.nix‎

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,145 @@
11
{
22
config,
33
inputs,
4+
lib,
45
pkgs,
56
...
6-
}: {
7+
}: let
8+
port = 8888;
9+
baseUrl = "http://127.0.0.1:${toString port}";
10+
11+
# Shared model defaults
12+
defaultCapabilities = {
13+
file_context = true;
14+
vision = true;
15+
file_upload = true;
16+
web_search = true;
17+
image_generation = true;
18+
code_interpreter = true;
19+
citations = true;
20+
status_updates = true;
21+
builtin_tools = true;
22+
};
23+
24+
defaultBuiltinTools = {
25+
time = true;
26+
memory = true;
27+
chats = true;
28+
notes = true;
29+
knowledge = true;
30+
channels = true;
31+
web_search = true;
32+
image_generation = true;
33+
code_interpreter = true;
34+
};
35+
36+
defaultMeta = {
37+
capabilities = defaultCapabilities;
38+
toolIds = ["server:mcp:glyph"];
39+
defaultFeatureIds = ["web_search" "code_interpreter"];
40+
builtinTools = defaultBuiltinTools;
41+
};
42+
43+
# Active models — listed models are enabled with full config.
44+
# All other models from the API provider are deactivated automatically.
45+
models = {
46+
"claude-sonnet-4-6" = {};
47+
"claude-opus-4-6" = {};
48+
"claude-haiku-4-5-20251001" = {};
49+
};
50+
51+
modelIds = builtins.toJSON (builtins.attrNames models);
52+
in {
753
age.secrets.open-webui-env.file = ./../secrets/open-webui-env.age;
54+
age.secrets.open-webui-api-key = {
55+
file = ./../secrets/open-webui-api-key.age;
56+
mode = "440";
57+
};
858

959
systemd.services.open-webui.restartTriggers = [config.age.secrets.open-webui-env.file];
1060

61+
# Sync model configuration after open-webui starts
62+
systemd.timers.open-webui-model-sync = {
63+
description = "Trigger Open WebUI model sync";
64+
wantedBy = ["timers.target"];
65+
restartTriggers = [(builtins.hashString "sha256" (builtins.toJSON models))];
66+
timerConfig.OnActiveSec = "10s";
67+
};
68+
69+
systemd.services.open-webui-model-sync = {
70+
description = "Sync model configuration with Open WebUI";
71+
after = ["open-webui.service"];
72+
requires = ["open-webui.service"];
73+
restartIfChanged = false;
74+
path = [pkgs.curl pkgs.jq];
75+
script = let
76+
mkModelForm = id: attrs:
77+
builtins.toJSON {
78+
inherit id;
79+
is_active = true;
80+
name = attrs.name or id;
81+
meta = defaultMeta // (attrs.meta or {});
82+
params = attrs.params or {};
83+
};
84+
85+
mkModelUpdate = id: attrs: let
86+
form = mkModelForm id attrs;
87+
in ''
88+
update_model "${id}" '${form}' &
89+
'';
90+
in ''
91+
API_KEY=$(cat ${config.age.secrets.open-webui-api-key.path})
92+
ACTIVE_IDS='${modelIds}'
93+
94+
update_model() {
95+
local id=$1 form=$2
96+
echo "Configuring $id..."
97+
http_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
98+
-H "Authorization: Bearer $API_KEY" \
99+
-H "Content-Type: application/json" \
100+
-d "$form" \
101+
"${baseUrl}/api/v1/models/model/update")
102+
103+
if [ "$http_code" = "200" ]; then
104+
echo "$id: updated."
105+
else
106+
echo "ERROR: failed to update $id (HTTP $http_code)"
107+
fi
108+
}
109+
110+
# Wait for open-webui to be ready
111+
for i in $(seq 1 30); do
112+
if curl -sf "${baseUrl}/api/models" -H "Authorization: Bearer $API_KEY" >/dev/null 2>&1; then
113+
break
114+
fi
115+
echo "Waiting for Open WebUI (attempt $i/30)..."
116+
sleep 2
117+
done
118+
119+
# Activate and configure listed models
120+
${lib.concatStringsSep "\n" (lib.mapAttrsToList mkModelUpdate models)}
121+
wait
122+
123+
# Deactivate all unlisted models that are currently active
124+
curl -sf -H "Authorization: Bearer $API_KEY" \
125+
"${baseUrl}/api/v1/models/list" \
126+
| jq -r '.data[] | select(.is_active == true) | .id' \
127+
| while read -r id; do
128+
if ! echo "$ACTIVE_IDS" | jq -e --arg id "$id" 'index($id)' >/dev/null 2>&1; then
129+
curl -sf -X POST -H "Authorization: Bearer $API_KEY" \
130+
"${baseUrl}/api/v1/models/model/toggle?id=$id" >/dev/null 2>&1
131+
echo "$id: deactivated."
132+
fi
133+
done
134+
'';
135+
};
136+
11137
services.open-webui = {
12138
enable = true;
13139
package = pkgs.open-webui.overridePythonAttrs (old: {
14140
dependencies = old.dependencies ++ old.optional-dependencies.postgres;
15141
});
16-
port = 8888;
142+
inherit port;
17143
host = "0.0.0.0";
18144
environmentFile = config.age.secrets.open-webui-env.path;
19145
environment = {

‎lib/secrets/glyph.nix‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ in {
77
"hosts/glyph/secrets/kagi-api-key.age".publicKeys = keys;
88
"hosts/glyph/secrets/context7-api-key.age".publicKeys = keys;
99
"hosts/glyph/secrets/open-terminal-env.age".publicKeys = keys;
10+
"hosts/glyph/secrets/open-webui-api-key.age".publicKeys = keys;
1011
"hosts/glyph/secrets/open-webui-env.age".publicKeys = keys;
1112
"hosts/glyph/secrets/graphite-auth-token.age".publicKeys = keys;
1213
"hosts/glyph/secrets/attic-credentials.age".publicKeys = keys;

0 commit comments

Comments
 (0)