Skip to content

Commit 5719894

Browse files
Alexey Panfilovclaude
andcommitted
feat(deploy): bake config.yaml into image + auto-migrate MCP to DB
Production deploys no longer need to mount a config directory — config.yaml is now a Docker image artifact, supplying bootstrap defaults only. Secrets still come from env vars; dynamic settings (routing, prompts, slot models, MCP servers, feature flags) live in kv_settings. First-boot MCP migration: when the DB key cfg.mcp.servers is empty and config/mcp.json exists, the bot writes the file contents to the DB. Next restart can run with that mount removed too. Repo config/config.yaml is updated to match the trimmed deploy version (7 named slots, no redundant base_urls, no ollama inference slot) so the baked defaults reflect the current production shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 75adbb3 commit 5719894

3 files changed

Lines changed: 71 additions & 72 deletions

File tree

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,10 @@ RUN apk add --no-cache ca-certificates tzdata
4040
WORKDIR /app
4141

4242
COPY --from=builder /app/bin/agent .
43+
# Bake the default config.yaml into the image. Production deploys no longer
44+
# need to mount a config directory — secrets come from env, dynamic settings
45+
# live in kv_settings, and this file supplies the bootstrap defaults. For dev
46+
# you can still mount over it via volumes.
47+
COPY --from=builder /app/config/config.yaml ./config/config.yaml
4348

4449
ENTRYPOINT ["./agent"]

cmd/agent/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"log/slog"
67
"os"
78
"os/signal"
@@ -231,12 +232,15 @@ func main() {
231232

232233
// Init MCP client. DB (kv_settings) wins over the legacy mcp.json file —
233234
// the admin UI writes there, so any user-edited list is authoritative.
235+
// On first boot with an empty DB, the file is auto-migrated so the next
236+
// restart can run with the config.yaml volume unmounted.
234237
var mcpClient *mcp.Client
235238
var mcpServers map[string]config.MCPServerConfig
236239
var mcpSource string
237-
if ss, ok := s.(llm.SettingsStore); ok {
240+
settingsStoreEarly, _ := s.(llm.SettingsStore)
241+
if settingsStoreEarly != nil {
238242
mcpCtx, mcpCancel := context.WithTimeout(context.Background(), 3*time.Second)
239-
if dbServers, found, dbErr := adminapi.LoadMCPServersFromSettings(mcpCtx, ss); dbErr != nil {
243+
if dbServers, found, dbErr := adminapi.LoadMCPServersFromSettings(mcpCtx, settingsStoreEarly); dbErr != nil {
240244
logger.Warn("failed to load MCP servers from DB; falling back to file", "err", dbErr)
241245
} else if found {
242246
mcpServers = dbServers
@@ -252,6 +256,19 @@ func main() {
252256
mcpServers = fileServers
253257
if fileServers != nil {
254258
mcpSource = "file"
259+
// Auto-migrate: write the file contents to kv_settings so future
260+
// boots don't depend on the file being present.
261+
if settingsStoreEarly != nil {
262+
migCtx, migCancel := context.WithTimeout(context.Background(), 3*time.Second)
263+
if data, mErr := json.Marshal(fileServers); mErr == nil {
264+
if pErr := settingsStoreEarly.PutSetting(migCtx, adminapi.SettingKeyMCPServers, string(data)); pErr != nil {
265+
logger.Warn("MCP auto-migration failed", "err", pErr)
266+
} else {
267+
logger.Info("MCP config auto-migrated file → DB", "servers", len(fileServers))
268+
}
269+
}
270+
migCancel()
271+
}
255272
}
256273
}
257274
logger.Info("MCP config loaded", "servers", len(mcpServers), "source", mcpSource)

config/config.yaml

Lines changed: 47 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,105 +4,81 @@ telegram:
44
- ${TELEGRAM_OWNER_CHAT_ID}
55
owner_chat_id: ${TELEGRAM_OWNER_CHAT_ID}
66

7-
# Each entry's KEY is its routing name (referenced from `routing.*` below).
8-
# Each entry's `provider` field selects the backend implementation.
9-
# Known providers: openrouter, gemini, ollama, claude-bridge, local, hf-tei, openai.
10-
# The key "embedding" is reserved for the MCP embedding provider.
7+
# Each entry's KEY is its routing name. `provider` field selects the backend
8+
# TYPE and the initial credentials; at runtime, the admin UI can swap both
9+
# the provider type AND the model for any slot (via BuildBackendFactories).
10+
#
11+
# base_url is omitted for openrouter/gemini/claude-bridge/ollama — those
12+
# have built-in defaults (llm.ApplyProviderDefaults). Override via env vars:
13+
# OPENROUTER_BASE_URL, CLAUDE_BRIDGE_URL, OLLAMA_BASE_URL.
1114
models:
12-
# --- OpenRouter slots — one per role so each can be reassigned independently
13-
# from the admin UI. Initial models are suggestions; override via UI. ---
14-
simple-or:
15+
# --- One slot per routing role. Admin UI picks provider+model per slot. ---
16+
simple:
1517
provider: openrouter
16-
model: qwen/qwen3.5-flash-02-23 # V T R, 1M ctx, $0.065/$0.26
18+
model: qwen/qwen3.5-flash-02-23
1719
api_key: ${OPENROUTER_API_KEY}
1820
max_tokens: 2048
19-
base_url: https://openrouter.ai/api/v1
20-
21-
default-or:
21+
default:
2222
provider: openrouter
23-
model: qwen/qwen3.5-flash-02-23 # same as simple initially; diverge in UI
23+
model: qwen/qwen3.5-flash-02-23
2424
api_key: ${OPENROUTER_API_KEY}
2525
max_tokens: 4096
26-
base_url: https://openrouter.ai/api/v1
27-
28-
complex-or: # alt to claude-bridge if bridge is off
29-
provider: openrouter
30-
model: qwen/qwen3-235b-a22b-thinking-2507
31-
api_key: ${OPENROUTER_API_KEY}
32-
max_tokens: 8192
33-
base_url: https://openrouter.ai/api/v1
34-
35-
compaction-or:
26+
complex:
27+
provider: claude-bridge
28+
api_key: ${CLAUDE_BRIDGE_TOKEN}
29+
max_tokens: 120
30+
classifier:
31+
provider: gemini
32+
model: gemini-2.5-flash-lite
33+
api_key: ${GEMINI_API_KEY}
34+
max_tokens: 64
35+
compaction:
3636
provider: openrouter
37-
model: qwen/qwen3.5-9b # min workable, 262k ctx, $0.10/$0.15
37+
model: qwen/qwen3.5-9b
3838
api_key: ${OPENROUTER_API_KEY}
3939
max_tokens: 2048
40-
base_url: https://openrouter.ai/api/v1
41-
42-
# --- Gemini (direct Google API) — fallback + multimodal/transcription. ---
43-
gemini-flash-lite:
40+
fallback:
4441
provider: gemini
45-
model: gemini-3.1-flash-lite-preview
42+
model: gemini-2.5-flash-lite
4643
api_key: ${GEMINI_API_KEY}
4744
max_tokens: 2048
48-
base_url: https://generativelanguage.googleapis.com/v1beta/openai/
49-
gemini-flash:
45+
multimodal:
5046
provider: gemini
51-
model: gemini-3-flash-preview
47+
model: gemini-2.5-flash
5248
api_key: ${GEMINI_API_KEY}
5349
max_tokens: 4096
54-
base_url: https://generativelanguage.googleapis.com/v1beta/openai/
5550

56-
# --- MCP embedding (hf-tei self-hosted). ---
51+
# --- Embedding (hf-tei in memory-embeddings container). Not an LLM slot. ---
5752
embedding:
5853
provider: hf-tei
59-
base_url: https://embed.dzarlax.dev # or http://embed-service:8080 in Docker
60-
api_key: ${EMBED_API_KEY} # format: "user:password" for Basic Auth
61-
62-
# --- Local Ollama — cheap fast classifier on host GPU. ---
63-
classifier:
64-
provider: ollama
65-
model: qwen3:0.6b
66-
base_url: http://host.docker.internal:11434
67-
max_tokens: 64
68-
no_think: true
69-
70-
# --- Claude via host-side bridge (wraps `claude -p` CLI). ---
71-
claude-bridge:
72-
provider: claude-bridge
73-
base_url: http://host.docker.internal:9900
74-
api_key: ${CLAUDE_BRIDGE_TOKEN}
75-
max_tokens: 120
54+
base_url: http://memory-embeddings:80
7655

7756
routing:
78-
simple: simple-or
79-
default: default-or
80-
complex: claude-bridge # via Anthropic Max subscription (host bridge)
81-
compaction: compaction-or
82-
fallback: gemini-flash-lite # DIRECT Google — different vendor, survives OR outage
83-
multimodal: gemini-flash # DIRECT Google — native input_audio for voice
84-
classifier: classifier # local Ollama
57+
simple: simple
58+
default: default
59+
complex: complex
60+
compaction: compaction
61+
fallback: fallback
62+
multimodal: multimodal
63+
classifier: classifier
8564
classifier_timeout: 15
86-
classifier_min_length: 0 # 0 = always; >0 = min chars; <0 = disabled
65+
classifier_min_length: 0
8766

8867
tool_filter:
8968
top_k: 20
9069

9170
# Web search — gives LLM access to real-time web results.
9271
# provider: "tavily" (free 1000/mo, LLM-ready answer+sources) or "ollama" (Ollama Cloud).
93-
# Get Tavily key at app.tavily.com (no card required for free tier).
9472
web_search:
9573
enabled: true
9674
provider: tavily
9775
api_key: ${TAVILY_API_KEY}
9876

9977
# Web fetch — extract main article text from a URL.
100-
# Primary: HTTP GET + go-readability (no cost, no API key).
101-
# Fallback: headless Chrome via CDP (for JS-heavy / bot-protected sites).
102-
# Leave cdp_url empty to disable fallback.
78+
# Primary: HTTP GET + go-readability. Fallback: shared infra-chrome (CDP).
10379
web_fetch:
10480
enabled: true
105-
cdp_url: ${WEB_FETCH_CDP_URL:-}
81+
cdp_url: http://infra-chrome:9222
10682

10783
# Filesystem access — gives the assistant read/write access to a local directory.
10884
# Mount the directory into the container via volumes in docker-compose.yml.
@@ -122,14 +98,15 @@ voice_api:
12298
token: ${VOICE_API_TOKEN}
12399
chat_id: 9999
124100

125-
# Admin web UI — browse OpenRouter models, assign to slots, edit routing roles.
126-
# Auth priority: authentik forward-auth (if trust_forward_auth) → cookie → bearer.
127-
# When running behind Traefik with an Authentik middleware, set trust_forward_auth: true
128-
# and let the middleware handle access control.
101+
# Admin web UI — behind Traefik + Authentik.
129102
admin_api:
130-
enabled: ${ADMIN_API_ENABLED:-false}
103+
enabled: true
131104
listen: ":8087"
132105
token: ${ADMIN_API_TOKEN}
133-
trust_forward_auth: ${ADMIN_TRUST_FORWARD_AUTH:-false}
106+
trust_forward_auth: true
134107
forward_auth_header: X-authentik-username
135-
base_url: ${ADMIN_API_BASE_URL}
108+
base_url: https://assistant.dzarlax.dev
109+
110+
# Artificial Analysis API — overlays Intelligence Index scores onto model capabilities.
111+
# Register at artificialanalysis.ai → API Keys. Free tier: 1000 req/day.
112+
artificial_analysis_api_key: ${AA_API_KEY}

0 commit comments

Comments
 (0)