@fugood/buttress-server 2.25.0-beta.9 → 2.25.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +506 -35
  2. package/config/function-samples/README.md +37 -0
  3. package/config/function-samples/_auth.ts +48 -0
  4. package/config/function-samples/host-info.ts +25 -0
  5. package/config/function-samples/summarize-text.ts +55 -0
  6. package/config/function-samples/text-to-speech.ts +51 -0
  7. package/config/function-samples/transcribe-media.ts +65 -0
  8. package/config/sample.toml +25 -0
  9. package/lib/autodiscover/index.d.ts +20 -0
  10. package/lib/autodiscover/sign.d.ts +10 -0
  11. package/lib/autodiscover/types.d.ts +32 -0
  12. package/lib/autodiscover/udp.d.ts +22 -0
  13. package/lib/cli.d.ts +3 -0
  14. package/lib/functions/auth.d.ts +28 -0
  15. package/lib/functions/config.d.ts +20 -0
  16. package/lib/functions/constants.d.ts +17 -0
  17. package/lib/functions/executor.d.ts +27 -0
  18. package/lib/functions/files.d.ts +29 -0
  19. package/lib/functions/index.d.ts +50 -0
  20. package/lib/functions/libs.d.ts +10 -0
  21. package/lib/functions/loader.d.ts +48 -0
  22. package/lib/functions/mcp.d.ts +35 -0
  23. package/lib/functions/registry.d.ts +34 -0
  24. package/lib/functions/scaffold.d.ts +18 -0
  25. package/lib/functions/status.d.ts +119 -0
  26. package/lib/functions/templates.d.ts +14 -0
  27. package/lib/functions/transpile.d.ts +23 -0
  28. package/lib/functions/types.d.ts +196 -0
  29. package/lib/functions/uploads.d.ts +42 -0
  30. package/lib/functions/watcher.d.ts +34 -0
  31. package/lib/index.d.ts +36 -0
  32. package/lib/index.mjs +1539 -64
  33. package/lib/package.d.ts +7 -0
  34. package/lib/routes/anthropic-messages.d.ts +55 -0
  35. package/lib/routes/file.d.ts +10 -0
  36. package/lib/routes/functions.check.d.ts +13 -0
  37. package/lib/routes/functions.d.ts +16 -0
  38. package/lib/routes/generator-cache.d.ts +42 -0
  39. package/lib/routes/index.d.ts +6 -0
  40. package/lib/routes/info.check.d.ts +1 -0
  41. package/lib/routes/info.d.ts +4 -0
  42. package/lib/routes/llm-shared.d.ts +57 -0
  43. package/lib/routes/openai-compat.d.ts +17 -0
  44. package/lib/routes/status.d.ts +4 -0
  45. package/lib/routes/stt-shared.d.ts +36 -0
  46. package/lib/routes/tts-shared.d.ts +36 -0
  47. package/lib/services/common.d.ts +29 -0
  48. package/lib/services/create-llm-service.d.ts +24 -0
  49. package/lib/services/create-onnx-init-context.d.ts +10 -0
  50. package/lib/services/ggml-llm.d.ts +5 -0
  51. package/lib/services/ggml-stt.d.ts +26 -0
  52. package/lib/services/index.d.ts +39 -0
  53. package/lib/services/mlx-llm.d.ts +5 -0
  54. package/lib/services/onnx-stt.d.ts +77 -0
  55. package/lib/services/onnx-tts.d.ts +48 -0
  56. package/lib/types.d.ts +181 -0
  57. package/lib/utils/SessionFileManager.d.ts +16 -0
  58. package/lib/utils/buttressAuth.d.ts +25 -0
  59. package/lib/utils/config.d.ts +21 -0
  60. package/lib/utils/functionsAuthGuard.d.ts +12 -0
  61. package/lib/utils/httpAuthGuard.d.ts +1 -0
  62. package/lib/utils/net.d.ts +6 -0
  63. package/lib/utils/router.d.ts +2 -0
  64. package/lib/utils/serialize.d.ts +2 -0
  65. package/lib/utils/serverCaps.d.ts +4 -0
  66. package/lib/utils/sessionGuard.d.ts +33 -0
  67. package/lib/utils/test-caps.d.ts +61 -0
  68. package/lib/utils/workspaceState.d.ts +21 -0
  69. package/package.json +21 -8
  70. package/public/status.html +278 -1
  71. package/lib/chunk-C8PTHxhX.mjs +0 -2
  72. package/lib/index.d.mts +0 -370
package/README.md CHANGED
@@ -20,73 +20,400 @@ npx bricks-buttress --config ./config.toml
20
20
  npx bricks-buttress
21
21
  ```
22
22
 
23
+ ## Workspace Binding (`bricks buttress`)
24
+
25
+ By default, a buttress-server runs in **public mode**: any client on the LAN can connect, no auth required. To restrict access to a single BRICKS workspace and enable workspace-scoped JWT auth, **bind** the server with the `bricks buttress` CLI commands. Once bound, the server only accepts WebSocket / file-transfer requests carrying a valid access token signed by that workspace's issuer.
26
+
27
+ The `bricks` CLI is the tool that performs the binding and writes the local state file. Install it first — see the [bricks-cli docs](https://docs.bricks.tools/cli) — then `bricks auth login` with the workspace owner's account before running the commands below.
28
+
29
+ ### Bind a server to a workspace
30
+
31
+ ```bash
32
+ # Pair the local machine's buttress-server with the workspace of the current bricks-cli profile
33
+ bricks buttress bind
34
+
35
+ # Override the auto-detected server id, give it a friendly name, or write to a custom state dir
36
+ bricks buttress bind --server-id buttress-mac-studio --name "Studio LLM" --state-dir /etc/buttress
37
+
38
+ # For headless/remote setups: emit state.json to stdout instead of writing to disk
39
+ bricks buttress bind --print > /etc/buttress/state.json
40
+ ```
41
+
42
+ The state file (`~/.bricks-cli/buttress/state.json` by default, or `$BRICKS_BUTTRESS_STATE_DIR`) stores:
43
+
44
+ - `workspace.id` / `workspace.name` — which workspace this server belongs to
45
+ - `workspace.serverId` — the server's stable id (defaults to `buttress-<machineId>`)
46
+ - `workspace.issuerPublicKey` + `workspace.kid` — Ed25519 SPKI used to verify access tokens
47
+
48
+ **Restart `bricks-buttress` after binding** for the change to take effect — the state file is read once at startup.
49
+
50
+ ### Inspect bindings
51
+
52
+ ```bash
53
+ # Show local state.json + the workspace-side bound list
54
+ bricks buttress status
55
+
56
+ # Same, JSON-formatted
57
+ bricks buttress status --json
58
+ ```
59
+
60
+ ### Discover servers on the LAN
61
+
62
+ ```bash
63
+ # UDP scan + HTTP /buttress/info verification (3s timeout by default)
64
+ bricks buttress scan
65
+
66
+ # UDP only (skip the /buttress/info round-trip)
67
+ bricks buttress scan --udp-only
68
+
69
+ # Machine-readable
70
+ bricks buttress scan --json
71
+ ```
72
+
73
+ `scan` lists every buttress-server visible on the LAN, including unbound (public) ones, with their version, auth state (`open` vs `JWT required` + kid), bound workspace, and per-generator hardware caps (`score`, GPU, usable memory). Servers whose workspace matches your current `bricks-cli` profile are highlighted; this is purely a discovery command and does not mint any tokens.
74
+
75
+ ### Unbind
76
+
77
+ ```bash
78
+ # Remove the binding from the workspace and delete the local state.json
79
+ bricks buttress unbind
80
+
81
+ # Keep the local state file (useful if you only want to revoke server-side)
82
+ bricks buttress unbind --keep-local
83
+ ```
84
+
85
+ After unbinding, restart the server to return it to public mode.
86
+
87
+ ### Issue a long-lived access token
88
+
89
+ For headless callers (CI, ctor agents) that already hold a workspace token, mint a long-lived buttress access token instead of relying on a per-launcher session token:
90
+
91
+ ```bash
92
+ # Default 30-day TTL
93
+ bricks buttress issue-token
94
+
95
+ # Custom TTL (seconds), JSON output for scripting
96
+ bricks buttress issue-token --ttl 3600 --json
97
+ ```
98
+
99
+ The token claims `{ k: 'ba', w_id, st: 'ws', sid, jti, exp }` and any buttress-server bound to the same workspace will accept it.
100
+
23
101
  ## Configuration
24
102
 
25
- Configuration can be provided via:
26
- - `--config` / `-c` flag with TOML file path
103
+ Configuration is loaded from a TOML file passed via `--config` / `-c`. Every top-level table is optional — missing sections fall back to defaults. See `config/sample.toml` for an end-to-end example.
27
104
 
28
- ### Configuration Format (TOML)
105
+ ### Top-level sections
106
+
107
+ | Section | Purpose |
108
+ | ------------------------- | -------------------------------------------------------------------------------------------------- |
109
+ | `[env]` | Environment variables exported into the process **only if not already set** |
110
+ | `[server]` | HTTP/RPC listener (port, log level, body limits) |
111
+ | `[runtime]` | Global defaults shared by every generator (most `[generators.model]` keys may live here too) |
112
+ | `[runtime.session_cache]` | KV-cache reuse store — see [Session State Cache](#session-state-cache) |
113
+ | `[autodiscover]` | LAN UDP / HTTP / mDNS discovery toggles |
114
+ | `[openai_compat]` | Enable `/oai-compat/v1/*` — see [Compatibility Endpoints](#compatibility-endpoints-experimental) |
115
+ | `[anthropic_messages]` | Enable `/anthropic-messages` — see [Compatibility Endpoints](#compatibility-endpoints-experimental)|
116
+ | `[functions]` | Enable local functions — see [Local Functions](#local-functions-experimental) |
117
+ | `[[generators]]` | Array of generator instances — one entry per loaded model |
118
+
119
+ ### `[env]`
29
120
 
30
121
  ```toml
31
- # Environment variables (only set if not already defined in system)
32
122
  [env]
33
- HF_TOKEN = "your_huggingface_token_here"
123
+ HUGGINGFACE_TOKEN = "hf_xxx" # ggml backends read this; HF_TOKEN is not picked up automatically
34
124
  CUDA_VISIBLE_DEVICES = "0"
125
+ ```
35
126
 
36
- [server]
37
- port = 2080
38
- log_level = "info"
127
+ Values here are exported only when the variable isn't already set in the process — see [Environment Variable Priority](#environment-variable-priority). For HuggingFace auth across all backends, `[runtime] huggingface_token = "hf_xxx"` works regardless of variable name.
128
+
129
+ ### `[server]`
130
+
131
+ | Key | Type | Default |
132
+ | ----------------- | -------------- | ---------------------------------------------------------------------- |
133
+ | `id` | string | `buttress-<machineId>` — stable id used for autodiscover / binding |
134
+ | `name` | string | `Buttress Server (<short id>)` — display name |
135
+ | `port` | number | `2080` (overridden by `--port`) |
136
+ | `log_level` | `"debug"`/`"info"`/`"warn"`/`"error"` | unset |
137
+ | `max_body_size` | string\|number | `"50MB"` — e.g. `"100MB"`, `"1GB"`, or raw bytes |
138
+ | `session_timeout` | string\|number | `60000` ms — accepts ms numbers or duration strings (`"30s"`) |
139
+ | `temp_file_dir` | string | `$TMPDIR/.buttress` |
140
+
141
+ ### `[runtime]` — global generator defaults
142
+
143
+ Most ggml-llm `[generators.model]` keys can also live in `[runtime]` as defaults. Per-generator values win; otherwise the runtime default applies.
144
+
145
+ | Key | Type | Notes |
146
+ | ---------------------------- | ----------------------------- | ---------------------------------------------------------------------- |
147
+ | `cache_dir` | string | Model + metadata cache root (default `~/.buttress/models`) |
148
+ | `huggingface_token` | string | Falls back to `$HUGGINGFACE_TOKEN` |
149
+ | `http_headers` | table | Extra headers attached to HF / HTTP downloads |
150
+ | `context_release_delay_ms` | number | Idle time before unloading a context (default `10000`; `0` = immediate)|
151
+ | `prefer_variants` | string[] | Override variant probe order (ggml backends) |
152
+ | `n_threads` | number | CPU thread count |
153
+ | `n_ctx` | number | Context window (per-model value wins; auto-capped at training context) |
154
+ | `n_gpu_layers` | number\|`"auto"` | Layers offloaded to GPU (default `"auto"`) |
155
+ | `n_batch` / `n_ubatch` | number | Prompt batch / micro-batch size. **Note:** `n_batch` has a model-level default of `512` that shadows the runtime value unless `[generators.model] n_batch` is set explicitly. |
156
+ | `n_parallel` | number | Parallel sequences (default `4`) |
157
+ | `n_cpu_moe` | number | MoE expert layers offloaded to CPU |
158
+ | `flash_attn_type` | `"on"` / `"off"` / `"auto"` | When a GPU backend is selected, defaults to `"auto"`; on CPU, defaults to `"off"`. Explicit `"on"` / `"off"` / `"auto"` overrides. |
159
+ | `cache_type_k`, `cache_type_v` | string | KV-cache dtype (`f16`, `f32`, `q8_0`, `q4_0`, …) |
160
+ | `kv_unified` | boolean | Use a unified KV cache across sequences |
161
+ | `swa_full` | boolean | Materialize full attention even for sliding-window layers |
162
+ | `ctx_shift` | boolean | Allow llama.cpp's rolling context shift |
163
+ | `use_mmap`, `use_mlock` | boolean | Memory-mapping / locking |
164
+ | `no_extra_bufts` | boolean | Disable extra compute buffer types |
165
+ | `cpu_mask`, `cpu_strict` | string / boolean | CPU affinity (advanced) |
166
+ | `devices` | string[] | Restrict to specific GGML devices |
167
+ | Speculative keys | various | `speculative`, `spec_type`, `spec_draft_n_max/n_min/p_min/p_split`, plus draft-model GPU/cache settings |
168
+
169
+ ### `[autodiscover]`
170
+
171
+ Set `autodiscover = true` for defaults, `false` (or omit) to disable, or a table for fine control:
39
172
 
40
- [runtime]
41
- cache_dir = "~/.buttress/models"
173
+ ```toml
174
+ [autodiscover]
175
+ udp.port = 8089
176
+ udp.announcements = { enabled = true, interval = 5000 }
177
+ udp.requests = { enabled = true, responseDelay = 100 }
178
+ http.enabled = true
179
+ http.path = "/buttress/info"
180
+ http.cors = true
181
+ # mdns.enabled = false # Bonjour/Avahi advertisement (optional)
182
+ ```
42
183
 
43
- # Session state cache for ggml-llm (saves KV cache to disk for prompt reuse)
44
- [runtime.session_cache]
45
- enabled = true
46
- max_size_bytes = "10GB" # Supports string (e.g., "10GB", "500MB") or number
47
- max_entries = 1000
184
+ ### `[[generators]]`
185
+
186
+ Every generator entry has a `type`, an optional `[generators.backend]` table, and a `[generators.model]` table:
187
+
188
+ ```toml
189
+ [[generators]]
190
+ type = "ggml-llm" # or "ggml-stt" / "mlx-llm"
48
191
 
49
- # GGML LLM generator
192
+ [generators.backend]
193
+ # (see per-type sections below)
194
+
195
+ [generators.model]
196
+ repo_id = "..."
197
+ # (see per-type sections below)
198
+ ```
199
+
200
+ #### Common `[generators.model]` keys
201
+
202
+ Shared by **all** generator types:
203
+
204
+ | Key | Type | Notes |
205
+ | ------------------------- | --------- | -------------------------------------------------------------------------------- |
206
+ | `repo_id` *(required)* | string | HuggingFace repo (`org/repo`) |
207
+ | `revision` | string | Default `"main"` |
208
+ | `download` | boolean | Pre-download at server startup (default `false`) |
209
+
210
+ Additional keys honored by **ggml-llm** and **ggml-stt** (mlx-llm gets quantization from the repo itself and does not use these):
211
+
212
+ | Key | Type | Notes |
213
+ | ------------------------- | --------- | -------------------------------------------------------------------------------- |
214
+ | `filename` | string | Pin a specific artifact in the repo |
215
+ | `url` | string | Direct download URL (skips manifest lookup) |
216
+ | `quantization` | string | Preferred quant tag — e.g. `q4_0`, `q8_0`, `mxfp4` |
217
+ | `preferred_quantizations` | string[] | Ordered fallback list when `quantization` doesn't match (alias: `quantizations`) |
218
+ | `allow_local_file` | boolean | Required to use `local_path` / `mmproj_local_path` |
219
+ | `local_path` | string | Use a local file as the load path. Repo metadata is still resolved from HF, so `repo_id` is still required. |
220
+ | `api_base`, `base_url` | string | Override HF API / blob hosts (mirrors / proxies) |
221
+
222
+ ---
223
+
224
+ ### `ggml-llm` (llama.cpp via `@fugood/llama.node`)
225
+
226
+ Loads a GGUF LLM. Runtime keys above can be overridden per-generator under `[generators.model]`; `[generators.backend]` only controls backend selection and resource planning.
227
+
228
+ **`[generators.backend]`**
229
+
230
+ | Key | Type | Default | Notes |
231
+ | --------------------- | -------- | --------------------------------------------- | ---------------------------------------------------------------- |
232
+ | `variant` | string | auto | Force `cuda` / `vulkan` / `snapdragon` / `default` |
233
+ | `variant_preference` | string[] | `["cuda","vulkan","snapdragon","default"]` | Probe order when `variant` is unset |
234
+ | `gpu_memory_fraction` | number | `0.85` | Max GPU fraction the hardware guardrails may plan against |
235
+ | `cpu_memory_fraction` | number | `0.5` | Max RAM fraction for CPU-side buffers |
236
+
237
+ **`[generators.model]`** — in addition to the common keys above:
238
+
239
+ | Key | Type | Notes |
240
+ | ----------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------- |
241
+ | `n_ctx` | number | Context window. Auto-capped at the model's training context. |
242
+ | `n_gpu_layers` | number\|`"auto"` | Layers offloaded to GPU (default `"auto"`) |
243
+ | `n_batch` | number | Prompt batch size (default `512`) |
244
+ | `n_ubatch`, `n_threads`, `n_parallel`, `n_cpu_moe` | number | Same semantics as the `[runtime]` defaults |
245
+ | `flash_attn_type`, `cache_type_k`, `cache_type_v`, `kv_unified`, `swa_full`, `ctx_shift`, `use_mmap`, `use_mlock`, `no_extra_bufts`, `cpu_mask`, `cpu_strict`, `devices` | various | Per-model overrides for the `[runtime]` defaults |
246
+
247
+ **Multimodal (mtmd)** — auto-downloads the matching `mmproj-*.gguf` from the same repo and calls `initMultimodal`:
248
+
249
+ | Key | Type | Notes |
250
+ | ------------------------- | ------- | ------------------------------------------------------------------ |
251
+ | `enable_mtmd` | boolean | Default `false` |
252
+ | `mmproj_filename` | string | Pin a specific projector file |
253
+ | `mmproj_url` | string | Direct URL override |
254
+ | `mmproj_local_path` | string | Local projector (requires `allow_local_file = true`) |
255
+ | `mmproj_use_gpu` | boolean | `null` = auto (true when `n_gpu_layers > 0`) |
256
+ | `mmproj_image_min_tokens` | number | Min visual tokens (dynamic-resolution models; `-1` = unset) |
257
+ | `mmproj_image_max_tokens` | number | Max visual tokens (`-1` = unset) |
258
+
259
+ **Speculative decoding**
260
+
261
+ `model_draft` may be a direct URL/path string or a table with the same
262
+ `repo_id`/`filename`/`url`/`local_path` model keys described above. Local draft paths require
263
+ `allow_local_file = true`. When `download = true`, Buttress pre-downloads both target and draft
264
+ models and includes both in its memory plan.
265
+
266
+ | Key | Type | Notes |
267
+ | -------------------------- | --------------- | ------------------------------------------------------------ |
268
+ | `model_draft` | string \| table | Optional separate GGUF draft model |
269
+ | `speculative` | bool \| string \| table | Enable speculative decoding and optionally select a strategy |
270
+ | `spec_type` | string | Strategy, such as `"draft-mtp"` |
271
+ | `spec_draft_n_max` | int | Max drafted tokens per step |
272
+ | `spec_draft_n_min` | int | Min drafted tokens |
273
+ | `spec_draft_p_min` | number | Min acceptance probability |
274
+ | `spec_draft_p_split` | number | Split threshold |
275
+ | `spec_draft_n_gpu_layers` | int | Draft-model layers offloaded to GPU (`-1` auto, `-2` all) |
276
+ | `spec_draft_cache_type_k` | string | Draft-model K-cache dtype |
277
+ | `spec_draft_cache_type_v` | string | Draft-model V-cache dtype |
278
+
279
+ **Example**
280
+
281
+ ```toml
50
282
  [[generators]]
51
283
  type = "ggml-llm"
52
284
  [generators.backend]
53
285
  variant_preference = ["cuda", "vulkan", "default"]
286
+ gpu_memory_fraction = 0.95
54
287
  [generators.model]
55
288
  repo_id = "ggml-org/gpt-oss-20b-GGUF"
56
289
  quantization = "mxfp4"
57
290
  n_ctx = 12800
291
+ download = true
292
+ ```
293
+
294
+ A separate draft model can be configured inline:
295
+
296
+ ```toml
297
+ [generators.model]
298
+ repo_id = "org/target-model-GGUF"
299
+ model_draft = { repo_id = "org/draft-model-GGUF", filename = "draft-q8_0.gguf" }
300
+ speculative = { type = "draft-mtp" }
301
+ spec_draft_n_max = 4
302
+ spec_draft_n_gpu_layers = -1
303
+ spec_draft_cache_type_k = "f16"
304
+ spec_draft_cache_type_v = "f16"
305
+ ```
306
+
307
+ ---
308
+
309
+ ### `ggml-stt` (whisper.cpp via `@fugood/whisper.node`)
310
+
311
+ Loads a Whisper GGML model for speech-to-text.
58
312
 
59
- # GGML STT (Speech-to-Text) generator
313
+ **`[generators.backend]`**
314
+
315
+ | Key | Type | Default | Notes |
316
+ | --------------------- | -------- | ----------------------------- | ---------------------------------- |
317
+ | `variant` | string | auto | `cuda` / `vulkan` / `default` |
318
+ | `variant_preference` | string[] | `["cuda","vulkan","default"]` | Probe order |
319
+ | `gpu_memory_fraction` | number | `0.85` | |
320
+ | `cpu_memory_fraction` | number | `0.5` | |
321
+
322
+ **`[generators.model]`** — common keys plus:
323
+
324
+ | Key | Type | Default | Notes |
325
+ | ------------------------- | ----------------------------- | -------------------------------- | ---------------------------------------------------- |
326
+ | `repo_id` | string | `"BricksDisplay/whisper-ggml"` | Defaulted (unlike ggml-llm) |
327
+ | `preferred_quantizations` | string[] | `["q8_0", <no-quant>, "q5_1"]` | Default fallback chain |
328
+ | `use_gpu` | boolean | `true` | Force-disable GPU even when available |
329
+ | `use_flash_attn` | `"on"` / `"off"` / `"auto"` / boolean | `"auto"` | `"auto"` enables flash-attn when GPU is in use. `true`/`false` are accepted as shortcuts for `"on"`/`"off"`. |
330
+
331
+ **Runtime extras** — under `[runtime]` for ggml-stt only:
332
+
333
+ | Key | Type | Notes |
334
+ | ------------- | ------ | ------------------------------------------- |
335
+ | `max_threads` | number | Caps the whisper.cpp thread count |
336
+
337
+ **Example**
338
+
339
+ ```toml
60
340
  [[generators]]
61
341
  type = "ggml-stt"
62
342
  [generators.backend]
63
- variant_preference = ["coreml", "default"]
343
+ variant_preference = ["cuda", "vulkan", "default"]
64
344
  [generators.model]
65
345
  repo_id = "BricksDisplay/whisper-ggml"
66
- filename = "ggml-small.bin"
346
+ filename = "ggml-large-v3-turbo-q8_0.bin"
347
+ use_gpu = true
348
+ use_flash_attn = "on"
349
+ download = true
350
+ ```
351
+
352
+ ---
353
+
354
+ ### `mlx-llm` (Apple Silicon, Python `mlx-lm` / `mlx-vlm` bridge)
355
+
356
+ Loads an MLX-format model on Apple Silicon. On first use, the backend creates a virtualenv at `{cache_dir}/mlx-env` and installs `mlx_lm_package`, `mlx_vlm_package`, plus `torch` and `torchvision` (required by some VLM processors). If an existing venv already has `mlx_vlm` and `torch` importable, the install step is skipped. There is no `[generators.backend]` section.
357
+
358
+ **`[generators.model]`** — common `repo_id` / `revision` / `download` plus:
359
+
360
+ | Key | Type | Default | Notes |
361
+ | ------------------ | ------------------- | --------- | ----------------------------------------------------------------------------- |
362
+ | `adapter_path` | string | — | Local LoRA adapter directory |
363
+ | `vlm` | `"auto"` / boolean | `"auto"` | Force VLM (`true`) vs text-only (`false`); `"auto"` infers from the repo |
364
+ | `tokenizer_config` | table | — | Forwarded to `mlx_lm.load(..., tokenizer_config=...)` |
365
+ | `model_config` | table | — | Forwarded to `mlx_lm.load(..., model_config=...)` |
366
+
367
+ `quantization`, `filename`, and `preferred_quantizations` are **not** used — the MLX repo itself determines the quantization.
368
+
369
+ **Runtime extras** — under `[runtime]` for mlx-llm:
370
+
371
+ | Key | Type | Default | Notes |
372
+ | ------------------- | ------ | ----------------------------- | -------------------------------------------------------------------- |
373
+ | `mlx_env_dir` | string | `{cache_dir}/mlx-env` | Location of the auto-managed Python venv |
374
+ | `mlx_lm_package` | string | `"mlx-lm==0.31.1"` | pip spec used when provisioning the venv |
375
+ | `mlx_vlm_package` | string | `"mlx-vlm==0.4.0"` | pip spec used when provisioning the venv |
376
+ | `session_cache.*` | table | enabled, `5GB`, 100 entries | Separate cache from ggml-llm (lives in `{cache_dir}/mlx-session-cache`) |
377
+
378
+ **Example**
379
+
380
+ ```toml
381
+ [[generators]]
382
+ type = "mlx-llm"
383
+ [generators.model]
384
+ repo_id = "mlx-community/Qwen2.5-VL-3B-Instruct-4bit"
385
+ vlm = true
386
+ download = true
67
387
  ```
68
388
 
69
389
  ### Programmatic Usage
70
390
 
391
+ `startServer` takes a processed config — run raw (TOML-shaped) input through `processConfig` first. Optional endpoint families are enabled with explicit flags, mirroring what the CLI resolves from the TOML and environment.
392
+
71
393
  ```javascript
72
- import { startServer } from '@fugood/buttress-server'
394
+ import { startServer, processConfig } from '@fugood/buttress-server'
395
+ import { resolveFunctionsConfig } from '@fugood/buttress-server'
396
+
397
+ const config = processConfig({
398
+ server: { port: 3000 },
399
+ runtime: { cache_dir: './.buttress-cache' },
400
+ generators: [
401
+ {
402
+ type: 'ggml-llm',
403
+ model: {
404
+ repo_id: 'ggml-org/gemma-3-270m-qat-GGUF',
405
+ quantization: 'mxfp4',
406
+ },
407
+ },
408
+ ],
409
+ })
73
410
 
74
411
  startServer({
75
- port: 3000,
76
- defaultConfig: {
77
- runtime: {
78
- cache_dir: './.buttress-cache'
79
- },
80
- generators: [
81
- {
82
- type: 'ggml-llm',
83
- model: {
84
- repo_id: 'ggml-org/gemma-3-270m-qat-GGUF',
85
- quantization: 'mxfp4',
86
- }
87
- }
88
- ]
89
- }
412
+ config,
413
+ enableOpenAICompat: false,
414
+ enableAnthropicMessages: false,
415
+ // Omit (or pass null) to leave local functions off.
416
+ functions: resolveFunctionsConfig(config.global, { configDir: process.cwd() }),
90
417
  })
91
418
  .then(({ port }) => {
92
419
  console.log(`Server running on port ${port}`)
@@ -94,6 +421,8 @@ startServer({
94
421
  .catch(console.error)
95
422
  ```
96
423
 
424
+ Note that the `[env]` table is applied by the CLI entrypoint only — a programmatic caller sets `process.env` itself.
425
+
97
426
  ### Environment Variable Priority
98
427
 
99
428
  Environment variables can be set in the `[env]` section of the TOML config. These values will **only be applied if the environment variable is not already set** in the system. This allows:
@@ -176,6 +505,148 @@ enabled = true
176
505
  | `/oai-compat/v1/*` | `[openai_compat] enabled = true` |
177
506
  | `/anthropic-messages` | `[anthropic_messages] enabled = true` |
178
507
 
508
+ ## Local Functions (Experimental)
509
+
510
+ Local functions are `.ts`/`.js` files you drop into a directory on the server. Each one becomes an MCP tool **and** an HTTP endpoint, so an agent (or any HTTP client) can run server-side work — shelling out to `ffmpeg`, calling this server's own LLM/STT generators — without you writing a service.
511
+
512
+ > **Experimental.** This entire surface — the endpoints, the function file contract, the `context` API, custom `_auth`, and the `[functions]` config keys — may change between releases without a deprecation window. The server says so at startup. Pin your `@fugood/bricks-buttress` version if you build on it, and re-check this section after upgrades.
513
+
514
+ ```toml
515
+ [functions]
516
+ enabled = true
517
+ dir = "./functions" # relative paths resolve against this config file
518
+ # allow_unauthenticated = false # see Security below
519
+ # default_timeout = "5m" # per-call deadline; override per function via meta.timeout
520
+ # hot_reload = false # watch the dir and reload eagerly on change (see below)
521
+
522
+ [functions.config] # optional; handed to functions as `context.config`
523
+ # api_base = "https://example.internal"
524
+ ```
525
+
526
+ `ENABLE_FUNCTIONS_ENDPOINT=1` and `BUTTRESS_FUNCTIONS_DIR=<dir>` are equivalent to the first two keys.
527
+
528
+ On startup the server creates the directory if needed and writes `buttress-functions.d.ts` (ambient types, refreshed every start), plus a `tsconfig.json` and a commented `_example.ts` when the directory holds no functions yet.
529
+
530
+ Ready-to-copy examples — a no-prerequisite starter, LLM summarization, ffmpeg + STT transcription, TTS with a downloadable result, and a custom auth gate — live in [`config/function-samples/`](config/function-samples/).
531
+
532
+ ### Writing a function
533
+
534
+ The file name is the tool name — `video-duration.ts` becomes `video-duration`. Files beginning with `_`, `.d.ts` files, and `*.test.*` / `*.spec.*` files are ignored, as are subdirectories (which are for helper modules).
535
+
536
+ ```ts
537
+ export const meta: ButtressFunctionMeta = {
538
+ description: 'Transcribe the audio track of a video file',
539
+ parameters: {
540
+ type: 'object',
541
+ properties: { path: { type: 'string' } },
542
+ required: ['path'],
543
+ },
544
+ timeout: '10m',
545
+ }
546
+
547
+ export default async function ({ path }: { path: string }, context: ButtressFunctionContext) {
548
+ const wav = `${context.tempDir}/audio.wav`
549
+ const { code, stderr } = await context.spawn('ffmpeg', ['-i', path, '-ar', '16000', wav])
550
+ if (code !== 0) throw new Error(`ffmpeg failed: ${stderr}`)
551
+
552
+ context.emit('progress', { stage: 'transcribing' })
553
+ return context.buttress.transcribe({ filePath: wav })
554
+ }
555
+ ```
556
+
557
+ `meta.parameters` is plain JSON Schema and is handed to MCP clients verbatim. The default export receives the parsed input object and a context:
558
+
559
+ | Context member | What it does |
560
+ | -------------- | ------------ |
561
+ | `spawn(cmd, args?, opts?)` | Run a child process; resolves `{ code, signal, stdout, stderr, truncated }`. A non-zero exit resolves — check `code`. Every child is killed when the call ends. |
562
+ | `buttress.completion({ model?, messages, max_tokens?, onToken?, … })` | Run a chat completion on this server's LLM generator. `messages` go through the model's chat template; thinking is off unless you pass `enable_thinking: true` (which fills `reasoning_content`). Other keys reach the backend verbatim. |
563
+ | `buttress.transcribe({ model?, filePath \| audioData, options? })` | Transcribe audio with this server's STT generator. |
564
+ | `buttress.synthesize({ model?, text, options? })` | Synthesize speech with this server's TTS generator (`onnx-tts`); the WAV lands in `tempDir` → `{ path, sampling_rate, channels }`. |
565
+ | `emit(event, data)` | Progress event; delivered to SSE callers, ignored otherwise. |
566
+ | `signal` | `AbortSignal`, aborted on timeout or caller disconnect. |
567
+ | `tempDir` | Per-call scratch directory, created on first access. |
568
+ | `fileUrl(path)` | Download URL (`/functions/files/…`) for a file inside `tempDir`, so callers can fetch outputs without filesystem access. |
569
+ | `log`, `fetch`, `env`, `config`, `dir` | Prefixed logging, host `fetch`, `process.env`, the `[functions.config]` table, the functions directory. |
570
+ | `libs` | `_`/`lodash`, `moment`, `math`/`mathjs`, `voca`, `chroma`, `json5`, `qs`, `bytes`, `ms`, `nanoid`, `md5`. |
571
+
572
+ Functions may `import` Node builtins (`node:fs/promises`, …) and sibling files inside the functions directory. Package imports are not supported.
573
+
574
+ Edits are picked up on the next call — the server re-transpiles when a file in the function's module graph changes, so no restart is needed. A file that fails to load is logged and skipped; the rest keep working.
575
+
576
+ For faster authoring feedback, opt into eager reloading with `[functions] hot_reload = true` (or `BUTTRESS_FUNCTIONS_HOT_RELOAD=1`): the server watches the directory and reloads on save, so a broken file is logged the moment you save it, and the function count in `/status` and discovery announcements moves without waiting for a call. The lazy mtime check keeps running either way — hot reload only adds immediacy, so a missed watch event costs nothing. On platforms without recursive `fs.watch` the server logs a warning and stays lazy.
577
+
578
+ ### Endpoints
579
+
580
+ | Endpoint | Purpose |
581
+ | -------- | ------- |
582
+ | `GET /functions` | List callable functions with their JSON Schemas |
583
+ | `POST /functions/<name>` | Run one; JSON body = input object, response is `{ "result": … }`. A multipart body stages its file fields inline (see below) |
584
+ | `POST /functions/<name>?stream=1` | Same, as SSE: `progress` events (from `context.emit`) then `result` or `error` |
585
+ | `POST /functions/mcp` | MCP over Streamable HTTP (stateless; `GET`/`DELETE` return 405) |
586
+ | `GET /functions/files/<path>` | Download a file a function wrote to its `tempDir` — functions hand out these URLs via `context.fileUrl` |
587
+ | `POST /functions/upload` | Stage an input file on the server (multipart, `file` field) → `{ "path", "url", "name", "size" }` |
588
+
589
+ Errors come back as `{ "error": { "code", "message" } }` with `FUNCTION_NOT_FOUND` (404), `FUNCTION_TIMEOUT` (504), `FUNCTION_FAILED` (500) or `FUNCTION_FILE_NOT_FOUND` (404).
590
+
591
+ Downloads carry the same auth as function calls, only ever serve files under the functions scratch root (anything else — traversal, directories — is a 404), and stay available until the ~24h scratch sweep. Typical flow: a function returns `{ url: context.fileUrl(outPath) }` and the caller (or MCP agent) fetches `<base><url>` with its existing `Authorization` header — see `config/function-samples/text-to-speech.ts` for the end-to-end shape.
592
+
593
+ Uploads are the same idea in the other direction, for functions whose input is a media file (`transcribe-media` and friends). The direct route is a multipart **call**: post `multipart/form-data` to `POST /functions/<name>` and every file field is staged into the call's scratch directory with its server-local path injected into the input under the field's name — one request uploads and runs:
594
+
595
+ ```bash
596
+ curl -X POST <base>/functions/transcribe-media -F file=@interview.mp4
597
+ # optional: -F input='{"model": "..."}' for typed values; other plain fields arrive as strings
598
+ ```
599
+
600
+ To stage a file once and reuse it across calls, `curl -F file=@interview.mp4 <base>/functions/upload` stores it in its own scratch directory and returns `{ "path", "url", "name", "size" }` — pass `path` as the function's input. Either way the client file name is sanitized to a bare name, and staged files share the auth guard and the ~24h sweep. Requests are capped by `[server] max_body_size` (default 50MB) — raise it for large media.
601
+
602
+ All of this activity is observable: the `/status` dashboard (and the `/buttress/status` JSON it polls) carries a **Local Functions** card with counters since startup and recent history for calls (per surface: HTTP/SSE/MCP, with durations and failure reasons), uploads, downloads, and auth decisions (allowed/denied with mode and subject — never credentials).
603
+
604
+ To point an agent at the MCP endpoint:
605
+
606
+ ```bash
607
+ bricks buttress mcp-config --write # discovers the server, mints a token, updates .mcp.json
608
+ ```
609
+
610
+ ### Security
611
+
612
+ Functions execute code and spawn processes on the host, so this surface is **fail-closed**: on a server with no workspace binding every call is rejected with `403 FUNCTIONS_UNAUTHENTICATED_DISABLED`. Either bind the server (`bricks buttress bind`) so calls require a workspace access token like every other data path, or opt in explicitly:
613
+
614
+ ```toml
615
+ [functions]
616
+ allow_unauthenticated = true # or BUTTRESS_FUNCTIONS_ALLOW_UNAUTHENTICATED=1
617
+ ```
618
+
619
+ Only do that on a trusted network — it lets anyone who can reach the port run every function.
620
+
621
+ Requests carrying a browser `Origin` header are rejected regardless of the above, so a web page you happen to visit cannot reach this surface. To allow a browser client, list its origin explicitly:
622
+
623
+ ```toml
624
+ [functions]
625
+ cors_allowed_origins = ["http://localhost:3000"] # or "*" to allow any origin
626
+ ```
627
+
628
+ #### Custom auth (`_auth.ts`)
629
+
630
+ Dropping an `_auth.ts` (or `.js`) into the functions directory puts your own logic in front of every `/functions` endpoint. The file has the same shape as a function file — `meta` plus a default-exported handler — and `meta.mode` picks how it composes with the workspace auth above:
631
+
632
+ - **`both`** (default): workspace auth runs first, unchanged; your function runs after it as an extra gate. It can only narrow access — use it for per-function allow-lists, subject checks, audit logging.
633
+ - **`override`**: your function alone decides. A presented workspace token is still verified into `request.workspaceAuth`, so you can keep honoring workspace tokens while also accepting other credentials — e.g. static API keys on a server that is not bound to any workspace (`allow_unauthenticated` stops mattering in this mode).
634
+
635
+ ```ts
636
+ export const meta: ButtressAuthMeta = { mode: 'override' }
637
+
638
+ export default async function (request: ButtressAuthRequest, context: ButtressAuthContext) {
639
+ if (request.workspaceAuth.authenticated) return true // workspace tokens keep working
640
+ const keys = context.config.api_keys // [functions.config] api_keys = [...]
641
+ if (Array.isArray(keys) && keys.includes(request.headers['x-api-key'])) return true
642
+ return { ok: false, status: 401, error: 'Invalid or missing API key' }
643
+ }
644
+ ```
645
+
646
+ The handler receives `{ method, path, name?, headers, query, token, workspaceAuth }` (`name` is only set for `POST /functions/<name>` — MCP `tools/call` targets live in the JSON-RPC body, which the guard does not parse) and a trimmed context (`log`, `fetch`, `env`, `config`, `dir`, `libs`). Only a returned `true` or `{ ok: true }` allows the request; anything else denies with `403 FUNCTIONS_AUTH_REJECTED` (or the `status`/`error` you return). Denials never fail open: while an `_auth` file exists but does not load, every call is rejected with `500 FUNCTIONS_AUTH_UNAVAILABLE`, and a handler that throws denies with `500 FUNCTIONS_AUTH_ERROR`. The browser-`Origin` block above always runs first — a permissive `_auth` cannot re-open it. Edits follow the same lazy reload as function files, and deleting the file reverts to plain workspace auth. See [`config/function-samples/_auth.ts`](config/function-samples/_auth.ts) for a commented version, and the scaffolded `buttress-functions.d.ts` for the full `ButtressAuth*` types.
647
+
648
+ Function files themselves are trusted input, exactly like this config file. They run in a `node:vm` context with a clean global (no ambient `process` or `require`), but that is for clarity, not isolation — a function that is handed `spawn` can do anything the server process can. Only put code you wrote (or reviewed) in the functions directory.
649
+
179
650
  ## Session State Cache
180
651
 
181
652
  The server supports session state caching for ggml-llm generators, which saves KV cache state to disk after completions. This enables:
@@ -0,0 +1,37 @@
1
+ # Function Samples
2
+
3
+ Ready-to-copy examples for the [Local Functions](../../README.md#local-functions-experimental)
4
+ feature (experimental — interfaces may change between releases).
5
+ Copy any of them into your configured functions directory (`[functions] dir`
6
+ in the server TOML) — the server picks new files up on the next call, no restart
7
+ needed. On first start the server scaffolds `buttress-functions.d.ts` and a
8
+ `tsconfig.json` into that directory, which give these files full editor typings.
9
+
10
+ | File | Shows | Needs |
11
+ |------|-------|-------|
12
+ | `host-info.ts` | The simplest possible function: a node builtin + `context.libs` | nothing |
13
+ | `summarize-text.ts` | Calling this server's own LLM (`context.buttress.completion`) | an LLM `[[generators]]` entry |
14
+ | `transcribe-media.ts` | `context.spawn` (ffmpeg), the scratch dir, SSE progress, STT | `ffmpeg` on PATH + an STT `[[generators]]` entry |
15
+ | `text-to-speech.ts` | TTS (`context.buttress.synthesize`) + downloadable output (`context.fileUrl`) | an `onnx-tts` `[[generators]]` entry |
16
+ | `_auth.ts` | Custom auth: keep workspace tokens working, add static API keys | see the file header |
17
+
18
+ `_auth.ts` is not a function: copying it changes how every `/functions` endpoint
19
+ authenticates callers. Read its header before copying it.
20
+
21
+ Quick test after copying (on a bound server pass a workspace token, or run
22
+ unbound with `allow_unauthenticated = true`):
23
+
24
+ ```bash
25
+ curl -X POST http://localhost:2080/functions/host-info
26
+ ```
27
+
28
+ The media samples take their input file inline — a multipart call stages the
29
+ file and hands the function its server-local path in one request:
30
+
31
+ ```bash
32
+ curl -X POST http://localhost:2080/functions/transcribe-media -F file=@interview.mp4
33
+ ```
34
+
35
+ (To reuse a file across calls, stage it once with
36
+ `curl -F file=@interview.mp4 http://localhost:2080/functions/upload` and pass
37
+ the returned `path` as the `file` input.)