@fugood/buttress-server 2.25.0 → 2.25.1-beta.1
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.
- package/README.md +238 -17
- package/config/function-samples/README.md +37 -0
- package/config/function-samples/_auth.ts +48 -0
- package/config/function-samples/host-info.ts +25 -0
- package/config/function-samples/summarize-text.ts +55 -0
- package/config/function-samples/text-to-speech.ts +51 -0
- package/config/function-samples/transcribe-media.ts +65 -0
- package/config/sample.toml +16 -0
- package/lib/cli-update.d.ts +1 -0
- package/lib/functions/auth.d.ts +28 -0
- package/lib/functions/config.d.ts +20 -0
- package/lib/functions/constants.d.ts +17 -0
- package/lib/functions/executor.d.ts +27 -0
- package/lib/functions/files.d.ts +29 -0
- package/lib/functions/index.d.ts +52 -0
- package/lib/functions/input.d.ts +11 -0
- package/lib/functions/libs.d.ts +10 -0
- package/lib/functions/loader.d.ts +48 -0
- package/lib/functions/mcp.d.ts +35 -0
- package/lib/functions/query.d.ts +27 -0
- package/lib/functions/registry.d.ts +35 -0
- package/lib/functions/scaffold.d.ts +18 -0
- package/lib/functions/status.d.ts +119 -0
- package/lib/functions/templates.d.ts +14 -0
- package/lib/functions/transpile.d.ts +23 -0
- package/lib/functions/types.d.ts +196 -0
- package/lib/functions/uploads.d.ts +42 -0
- package/lib/functions/watcher.d.ts +34 -0
- package/lib/index.d.ts +11 -3
- package/lib/index.mjs +347 -63
- package/lib/routes/functions.check.d.ts +13 -0
- package/lib/routes/functions.d.ts +17 -0
- package/lib/routes/generator-cache.d.ts +58 -0
- package/lib/routes/index.d.ts +1 -0
- package/lib/routes/llm-shared.d.ts +22 -19
- package/lib/routes/stt-shared.d.ts +34 -0
- package/lib/routes/tts-shared.d.ts +34 -0
- package/lib/services/create-onnx-init-context.d.ts +10 -0
- package/lib/services/onnx-stt.d.ts +1 -1
- package/lib/services/onnx-tts.d.ts +1 -1
- package/lib/types.d.ts +23 -0
- package/lib/utils/functionsAuthGuard.d.ts +12 -0
- package/lib/utils/update.d.ts +38 -0
- package/package.json +23 -3
- package/public/status.html +116 -0
package/README.md
CHANGED
|
@@ -8,6 +8,41 @@ A high-performance RPC server for managing GGML LLM generators with configurable
|
|
|
8
8
|
npm install -g @fugood/buttress-server
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
### Standalone binary (no Node.js required)
|
|
12
|
+
|
|
13
|
+
A self-contained executable built with `bun build --compile`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -fsSL https://cdn.bricks.tools/bricks-buttress/release/install.sh | sh
|
|
17
|
+
# beta channel
|
|
18
|
+
curl -fsSL https://cdn.bricks.tools/bricks-buttress/beta/install.sh | sh -s -- --beta
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Windows (PowerShell):
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
irm https://cdn.bricks.tools/bricks-buttress/release/install.ps1 | iex
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The installer detects the host's supported GGML accelerator (CUDA > Vulkan >
|
|
28
|
+
Snapdragon, falling back to the default CPU/Metal build) and downloads only
|
|
29
|
+
the native modules the binary needs — the ggml llama.node / whisper.node
|
|
30
|
+
variant packages plus onnxruntime, sharp, and oxc-transform prebuilds — into a
|
|
31
|
+
`node_modules` sidecar next to the executable. It installs to
|
|
32
|
+
`~/.bricks-cli/bin` (shared with the BRICKS CLI, so one PATH entry covers
|
|
33
|
+
both). Override detection with
|
|
34
|
+
`--ggml-variant=default|cuda|vulkan|snapdragon|all`.
|
|
35
|
+
|
|
36
|
+
Build the distribution locally from this package:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun run build:dist -- --target=darwin-arm64 # or --platform=linux, etc.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See `scripts/build-distribution.js` for how native modules are swapped to
|
|
43
|
+
sidecar loaders at bundle time, and `scripts/unix/install.sh` /
|
|
44
|
+
`scripts/windows/install.ps1` for the host detection.
|
|
45
|
+
|
|
11
46
|
## Quick Start
|
|
12
47
|
|
|
13
48
|
### Using CLI
|
|
@@ -113,6 +148,7 @@ Configuration is loaded from a TOML file passed via `--config` / `-c`. Every top
|
|
|
113
148
|
| `[autodiscover]` | LAN UDP / HTTP / mDNS discovery toggles |
|
|
114
149
|
| `[openai_compat]` | Enable `/oai-compat/v1/*` — see [Compatibility Endpoints](#compatibility-endpoints-experimental) |
|
|
115
150
|
| `[anthropic_messages]` | Enable `/anthropic-messages` — see [Compatibility Endpoints](#compatibility-endpoints-experimental)|
|
|
151
|
+
| `[functions]` | Enable local functions — see [Local Functions](#local-functions-experimental) |
|
|
116
152
|
| `[[generators]]` | Array of generator instances — one entry per loaded model |
|
|
117
153
|
|
|
118
154
|
### `[env]`
|
|
@@ -159,6 +195,8 @@ Most ggml-llm `[generators.model]` keys can also live in `[runtime]` as defaults
|
|
|
159
195
|
| `kv_unified` | boolean | Use a unified KV cache across sequences |
|
|
160
196
|
| `swa_full` | boolean | Materialize full attention even for sliding-window layers |
|
|
161
197
|
| `ctx_shift` | boolean | Allow llama.cpp's rolling context shift |
|
|
198
|
+
| `state_cache_budget_mb` | number | Memory budget of the cross-turn KV prefix cache for recurrent / hybrid models (default `160`, `0` disables) |
|
|
199
|
+
| `state_cache_max_checkpoints` | number | Snapshot count cap for that cache (default `8`, `0` = unlimited); the memory budget takes precedence |
|
|
162
200
|
| `use_mmap`, `use_mlock` | boolean | Memory-mapping / locking |
|
|
163
201
|
| `no_extra_bufts` | boolean | Disable extra compute buffer types |
|
|
164
202
|
| `cpu_mask`, `cpu_strict` | string / boolean | CPU affinity (advanced) |
|
|
@@ -241,7 +279,7 @@ Loads a GGUF LLM. Runtime keys above can be overridden per-generator under `[gen
|
|
|
241
279
|
| `n_gpu_layers` | number\|`"auto"` | Layers offloaded to GPU (default `"auto"`) |
|
|
242
280
|
| `n_batch` | number | Prompt batch size (default `512`) |
|
|
243
281
|
| `n_ubatch`, `n_threads`, `n_parallel`, `n_cpu_moe` | number | Same semantics as the `[runtime]` defaults |
|
|
244
|
-
| `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 |
|
|
282
|
+
| `flash_attn_type`, `cache_type_k`, `cache_type_v`, `kv_unified`, `swa_full`, `ctx_shift`, `state_cache_budget_mb`, `state_cache_max_checkpoints`, `use_mmap`, `use_mlock`, `no_extra_bufts`, `cpu_mask`, `cpu_strict`, `devices` | various | Per-model overrides for the `[runtime]` defaults |
|
|
245
283
|
|
|
246
284
|
**Multimodal (mtmd)** — auto-downloads the matching `mmproj-*.gguf` from the same repo and calls `initMultimodal`:
|
|
247
285
|
|
|
@@ -387,25 +425,32 @@ download = true
|
|
|
387
425
|
|
|
388
426
|
### Programmatic Usage
|
|
389
427
|
|
|
428
|
+
`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.
|
|
429
|
+
|
|
390
430
|
```javascript
|
|
391
|
-
import { startServer } from '@fugood/buttress-server'
|
|
431
|
+
import { startServer, processConfig } from '@fugood/buttress-server'
|
|
432
|
+
import { resolveFunctionsConfig } from '@fugood/buttress-server'
|
|
433
|
+
|
|
434
|
+
const config = processConfig({
|
|
435
|
+
server: { port: 3000 },
|
|
436
|
+
runtime: { cache_dir: './.buttress-cache' },
|
|
437
|
+
generators: [
|
|
438
|
+
{
|
|
439
|
+
type: 'ggml-llm',
|
|
440
|
+
model: {
|
|
441
|
+
repo_id: 'ggml-org/gemma-3-270m-qat-GGUF',
|
|
442
|
+
quantization: 'mxfp4',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
})
|
|
392
447
|
|
|
393
448
|
startServer({
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
generators: [
|
|
400
|
-
{
|
|
401
|
-
type: 'ggml-llm',
|
|
402
|
-
model: {
|
|
403
|
-
repo_id: 'ggml-org/gemma-3-270m-qat-GGUF',
|
|
404
|
-
quantization: 'mxfp4',
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
]
|
|
408
|
-
}
|
|
449
|
+
config,
|
|
450
|
+
enableOpenAICompat: false,
|
|
451
|
+
enableAnthropicMessages: false,
|
|
452
|
+
// Omit (or pass null) to leave local functions off.
|
|
453
|
+
functions: resolveFunctionsConfig(config.global, { configDir: process.cwd() }),
|
|
409
454
|
})
|
|
410
455
|
.then(({ port }) => {
|
|
411
456
|
console.log(`Server running on port ${port}`)
|
|
@@ -413,6 +458,8 @@ startServer({
|
|
|
413
458
|
.catch(console.error)
|
|
414
459
|
```
|
|
415
460
|
|
|
461
|
+
Note that the `[env]` table is applied by the CLI entrypoint only — a programmatic caller sets `process.env` itself.
|
|
462
|
+
|
|
416
463
|
### Environment Variable Priority
|
|
417
464
|
|
|
418
465
|
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:
|
|
@@ -449,6 +496,14 @@ Buttress server for remote inference with GGML backends.
|
|
|
449
496
|
|
|
450
497
|
Usage:
|
|
451
498
|
bricks-buttress [options]
|
|
499
|
+
bricks-buttress update [--check] [-y] [--channel <release|beta>]
|
|
500
|
+
|
|
501
|
+
Commands:
|
|
502
|
+
update Update bricks-buttress to the latest version.
|
|
503
|
+
Standalone binary installs re-run the CDN
|
|
504
|
+
installer (refreshing the native-module
|
|
505
|
+
sidecar); npm/bun installs update the package.
|
|
506
|
+
`--check` only reports whether an update exists.
|
|
452
507
|
|
|
453
508
|
Options:
|
|
454
509
|
-h, --help Show this help message
|
|
@@ -495,6 +550,172 @@ enabled = true
|
|
|
495
550
|
| `/oai-compat/v1/*` | `[openai_compat] enabled = true` |
|
|
496
551
|
| `/anthropic-messages` | `[anthropic_messages] enabled = true` |
|
|
497
552
|
|
|
553
|
+
## Local Functions (Experimental)
|
|
554
|
+
|
|
555
|
+
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.
|
|
556
|
+
|
|
557
|
+
> **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.
|
|
558
|
+
|
|
559
|
+
```toml
|
|
560
|
+
[functions]
|
|
561
|
+
enabled = true
|
|
562
|
+
dir = "./functions" # relative paths resolve against this config file
|
|
563
|
+
# allow_unauthenticated = false # see Security below
|
|
564
|
+
# default_timeout = "5m" # per-call deadline; override per function via meta.timeout
|
|
565
|
+
# hot_reload = false # watch the dir and reload eagerly on change (see below)
|
|
566
|
+
|
|
567
|
+
[functions.config] # optional; handed to functions as `context.config`
|
|
568
|
+
# api_base = "https://example.internal"
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
`ENABLE_FUNCTIONS_ENDPOINT=1` and `BUTTRESS_FUNCTIONS_DIR=<dir>` are equivalent to the first two keys.
|
|
572
|
+
|
|
573
|
+
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.
|
|
574
|
+
|
|
575
|
+
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/).
|
|
576
|
+
|
|
577
|
+
### Writing a function
|
|
578
|
+
|
|
579
|
+
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).
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
export const meta: ButtressFunctionMeta = {
|
|
583
|
+
description: 'Transcribe the audio track of a video file',
|
|
584
|
+
parameters: {
|
|
585
|
+
type: 'object',
|
|
586
|
+
properties: { path: { type: 'string' } },
|
|
587
|
+
required: ['path'],
|
|
588
|
+
},
|
|
589
|
+
timeout: '10m',
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export default async function ({ path }: { path: string }, context: ButtressFunctionContext) {
|
|
593
|
+
const wav = `${context.tempDir}/audio.wav`
|
|
594
|
+
const { code, stderr } = await context.spawn('ffmpeg', ['-i', path, '-ar', '16000', wav])
|
|
595
|
+
if (code !== 0) throw new Error(`ffmpeg failed: ${stderr}`)
|
|
596
|
+
|
|
597
|
+
context.emit('progress', { stage: 'transcribing' })
|
|
598
|
+
return context.buttress.transcribe({ filePath: wav })
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
`meta.parameters` is plain JSON Schema and is handed to MCP clients verbatim. The default export receives the parsed input object and a context:
|
|
603
|
+
|
|
604
|
+
| Context member | What it does |
|
|
605
|
+
| -------------- | ------------ |
|
|
606
|
+
| `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. |
|
|
607
|
+
| `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. |
|
|
608
|
+
| `buttress.transcribe({ model?, filePath \| audioData, options? })` | Transcribe audio with this server's STT generator. |
|
|
609
|
+
| `buttress.synthesize({ model?, text, options? })` | Synthesize speech with this server's TTS generator (`onnx-tts`); the WAV lands in `tempDir` → `{ path, sampling_rate, channels }`. |
|
|
610
|
+
| `emit(event, data)` | Progress event; delivered to SSE callers, ignored otherwise. |
|
|
611
|
+
| `signal` | `AbortSignal`, aborted on timeout or caller disconnect. |
|
|
612
|
+
| `tempDir` | Per-call scratch directory, created on first access. |
|
|
613
|
+
| `fileUrl(path)` | Download URL (`/functions/files/…`) for a file inside `tempDir`, so callers can fetch outputs without filesystem access. |
|
|
614
|
+
| `log`, `fetch`, `env`, `config`, `dir` | Prefixed logging, host `fetch`, `process.env`, the `[functions.config]` table, the functions directory. |
|
|
615
|
+
| `libs` | `_`/`lodash`, `moment`, `math`/`mathjs`, `voca`, `chroma`, `json5`, `qs`, `bytes`, `ms`, `nanoid`, `md5`. |
|
|
616
|
+
|
|
617
|
+
Functions may `import` Node builtins (`node:fs/promises`, …) and sibling files inside the functions directory. Package imports are not supported.
|
|
618
|
+
|
|
619
|
+
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.
|
|
620
|
+
|
|
621
|
+
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.
|
|
622
|
+
|
|
623
|
+
### Endpoints
|
|
624
|
+
|
|
625
|
+
| Endpoint | Purpose |
|
|
626
|
+
| -------- | ------- |
|
|
627
|
+
| `GET /functions` | List callable functions with their JSON Schemas |
|
|
628
|
+
| `POST /functions/<name>` | Run one; JSON body = input object, response is `{ "result": … }`. A multipart body stages its file fields inline (see below) |
|
|
629
|
+
| `GET /functions/<name>?…` | Run one with the query string as its input (see below) |
|
|
630
|
+
| `POST /functions/<name>?stream=1` | Same, as SSE: `progress` events (from `context.emit`) then `result` or `error`. `GET` streams too |
|
|
631
|
+
| `POST /functions/mcp` | MCP over Streamable HTTP (stateless; `GET`/`DELETE` return 405) |
|
|
632
|
+
| `GET /functions/files/<path>` | Download a file a function wrote to its `tempDir` — functions hand out these URLs via `context.fileUrl` |
|
|
633
|
+
| `POST /functions/upload` | Stage an input file on the server (multipart, `file` field) → `{ "path", "url", "name", "size" }` |
|
|
634
|
+
|
|
635
|
+
Errors come back as `{ "error": { "code", "message" } }` with `FUNCTION_NOT_FOUND` (404), `FUNCTION_TIMEOUT` (504), `FUNCTION_FAILED` (500) or `FUNCTION_FILE_NOT_FOUND` (404).
|
|
636
|
+
|
|
637
|
+
#### Calling with GET
|
|
638
|
+
|
|
639
|
+
`GET /functions/<name>` runs the same call with no body at all — the query string is the input, which is what a browser address bar, a webhook, an `EventSource` (it cannot set headers, hence `?token=…`) or a bare `curl` can produce without ceremony:
|
|
640
|
+
|
|
641
|
+
```bash
|
|
642
|
+
curl '<base>/functions/weather?city=Taipei&days=3&units=metric'
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Query values are strings, so the function's declared `meta.parameters` schema doubles as the coercion table: declared `number`/`integer`, `boolean`, `array` and `object` properties are converted, everything else stays a string. A value that does not fit its declared type is passed through verbatim rather than turned into `NaN` — the handler still owns validation.
|
|
646
|
+
|
|
647
|
+
| Declared type | Query form |
|
|
648
|
+
| ------------- | ---------- |
|
|
649
|
+
| `number` / `integer` | `?n=3` |
|
|
650
|
+
| `boolean` | `?verbose=true`, `?verbose=1`, or a bare `?verbose`; `false`/`0` for the other side |
|
|
651
|
+
| `array` | `?tag=a&tag=b`, `?tag=a,b`, or `?tag=["a","b"]` (items coerce by `items`) |
|
|
652
|
+
| `object` | `?filter={"lang":"en"}` |
|
|
653
|
+
|
|
654
|
+
For exact types regardless of the schema, pass the whole object as JSON in `input` — plain parameters overlay it, exactly like a multipart call: `?input={"n":3}¬e=hi`. `stream`, `token` and `access_token` steer the request and never reach the handler.
|
|
655
|
+
|
|
656
|
+
GET is offered for every function; HTTP asks that a GET be safe to repeat and only you know whether yours is, so pick the method that matches what the function does. Responses are `no-store`.
|
|
657
|
+
|
|
658
|
+
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.
|
|
659
|
+
|
|
660
|
+
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:
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
curl -X POST <base>/functions/transcribe-media -F file=@interview.mp4
|
|
664
|
+
# optional: -F input='{"model": "..."}' for typed values; other plain fields arrive as strings
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
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.
|
|
668
|
+
|
|
669
|
+
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).
|
|
670
|
+
|
|
671
|
+
To point an agent at the MCP endpoint:
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
bricks buttress mcp-config --write # discovers the server, mints a token, updates .mcp.json
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Security
|
|
678
|
+
|
|
679
|
+
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:
|
|
680
|
+
|
|
681
|
+
```toml
|
|
682
|
+
[functions]
|
|
683
|
+
allow_unauthenticated = true # or BUTTRESS_FUNCTIONS_ALLOW_UNAUTHENTICATED=1
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Only do that on a trusted network — it lets anyone who can reach the port run every function.
|
|
687
|
+
|
|
688
|
+
Browser-initiated cross-site requests are rejected regardless of the above, so a web page you happen to visit cannot reach this surface — that covers both requests carrying an `Origin` header and no-CORS loads that send none (`<img src>`, `<script src>`, a prefetch of a `GET` call), which browsers mark with `Sec-Fetch-Site`. To allow a browser client, list its origin explicitly:
|
|
689
|
+
|
|
690
|
+
```toml
|
|
691
|
+
[functions]
|
|
692
|
+
cors_allowed_origins = ["http://localhost:3000"] # or "*" to allow any origin
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
A listed origin only helps requests that carry one; a no-CORS load has no origin to match, so only `"*"` lets those through.
|
|
696
|
+
|
|
697
|
+
#### Custom auth (`_auth.ts`)
|
|
698
|
+
|
|
699
|
+
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:
|
|
700
|
+
|
|
701
|
+
- **`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.
|
|
702
|
+
- **`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).
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
export const meta: ButtressAuthMeta = { mode: 'override' }
|
|
706
|
+
|
|
707
|
+
export default async function (request: ButtressAuthRequest, context: ButtressAuthContext) {
|
|
708
|
+
if (request.workspaceAuth.authenticated) return true // workspace tokens keep working
|
|
709
|
+
const keys = context.config.api_keys // [functions.config] api_keys = [...]
|
|
710
|
+
if (Array.isArray(keys) && keys.includes(request.headers['x-api-key'])) return true
|
|
711
|
+
return { ok: false, status: 401, error: 'Invalid or missing API key' }
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
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.
|
|
716
|
+
|
|
717
|
+
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.
|
|
718
|
+
|
|
498
719
|
## Session State Cache
|
|
499
720
|
|
|
500
721
|
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.)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Custom auth for the /functions endpoints.
|
|
2
|
+
//
|
|
3
|
+
// Unlike other "_"-prefixed files, "_auth" is special: dropping it into the
|
|
4
|
+
// functions directory activates it. `meta.mode` picks how it composes with
|
|
5
|
+
// the built-in workspace auth:
|
|
6
|
+
//
|
|
7
|
+
// - 'both' (default): runs after workspace auth passes — an extra gate that
|
|
8
|
+
// can only narrow access (e.g. per-function allow-lists).
|
|
9
|
+
// - 'override': fully replaces workspace auth — this function alone decides.
|
|
10
|
+
// A presented workspace token is still verified into
|
|
11
|
+
// `request.workspaceAuth` so the function can choose to honor it.
|
|
12
|
+
//
|
|
13
|
+
// This sample uses 'override' to keep workspace tokens working while ALSO
|
|
14
|
+
// accepting static API keys for callers that have none. Configure the keys in
|
|
15
|
+
// the server TOML:
|
|
16
|
+
//
|
|
17
|
+
// [functions.config]
|
|
18
|
+
// api_keys = ["replace-with-a-long-random-string"]
|
|
19
|
+
//
|
|
20
|
+
// Callers then send `X-Api-Key: <key>` (or `Authorization: Bearer <key>`).
|
|
21
|
+
// With no api_keys configured it denies everything except valid workspace
|
|
22
|
+
// tokens — it never fails open.
|
|
23
|
+
|
|
24
|
+
export const meta: ButtressAuthMeta = {
|
|
25
|
+
mode: 'override',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function authorize(
|
|
29
|
+
request: ButtressAuthRequest,
|
|
30
|
+
context: ButtressAuthContext,
|
|
31
|
+
): Promise<ButtressAuthResult> {
|
|
32
|
+
// A valid workspace access token keeps working like before.
|
|
33
|
+
if (request.workspaceAuth.authenticated) return true
|
|
34
|
+
|
|
35
|
+
const keys = context.config.api_keys
|
|
36
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: 'No workspace token and no [functions.config] api_keys configured',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Headers reach this function lower-cased.
|
|
44
|
+
const presented = request.headers['x-api-key'] || request.token
|
|
45
|
+
if (typeof presented === 'string' && keys.includes(presented)) return true
|
|
46
|
+
|
|
47
|
+
return { ok: false, status: 401, error: 'Invalid or missing API key' }
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Report facts about the machine this server runs on.
|
|
2
|
+
//
|
|
3
|
+
// A good first function to try: it needs no external tools, no configured
|
|
4
|
+
// generators and no input.
|
|
5
|
+
//
|
|
6
|
+
// curl -X POST http://<host>:<port>/functions/host-info
|
|
7
|
+
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
|
|
10
|
+
export const meta: ButtressFunctionMeta = {
|
|
11
|
+
description: 'Report hostname, OS, CPU, memory and uptime of the Buttress host',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async function (_input: unknown, context: ButtressFunctionContext) {
|
|
15
|
+
const { bytes, ms } = context.libs
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
hostname: os.hostname(),
|
|
19
|
+
platform: `${os.platform()} ${os.release()} (${os.arch()})`,
|
|
20
|
+
cpus: os.cpus().length,
|
|
21
|
+
memory: { total: bytes(os.totalmem()), free: bytes(os.freemem()) },
|
|
22
|
+
uptime: ms(Math.round(os.uptime()) * 1000, { long: true }),
|
|
23
|
+
load: os.loadavg().map((value) => Number(value.toFixed(2))),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Summarize text with this server's own LLM generator — no API keys, the
|
|
2
|
+
// model already configured under [[generators]] does the work in-process.
|
|
3
|
+
//
|
|
4
|
+
// Needs at least one LLM generator (e.g. ggml-llm) in the server config.
|
|
5
|
+
//
|
|
6
|
+
// curl -X POST http://<host>:<port>/functions/summarize-text \
|
|
7
|
+
// -H 'Content-Type: application/json' \
|
|
8
|
+
// -d '{"text": "…", "style": "bullets"}'
|
|
9
|
+
|
|
10
|
+
export const meta: ButtressFunctionMeta = {
|
|
11
|
+
description: 'Summarize a piece of text with the local LLM',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
text: { type: 'string', description: 'The text to summarize' },
|
|
16
|
+
style: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['bullets', 'paragraph'],
|
|
19
|
+
default: 'bullets',
|
|
20
|
+
description: 'Shape of the summary',
|
|
21
|
+
},
|
|
22
|
+
model: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Optional model override (repo_id of a configured generator)',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['text'],
|
|
28
|
+
},
|
|
29
|
+
timeout: '5m',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Input = { text: string; style?: 'bullets' | 'paragraph'; model?: string }
|
|
33
|
+
|
|
34
|
+
export default async function (
|
|
35
|
+
{ text, style = 'bullets', model }: Input,
|
|
36
|
+
context: ButtressFunctionContext,
|
|
37
|
+
) {
|
|
38
|
+
const target = style === 'paragraph' ? 'one short paragraph' : '3-5 concise bullet points'
|
|
39
|
+
|
|
40
|
+
const { content, usage } = await context.buttress.completion({
|
|
41
|
+
model,
|
|
42
|
+
// A summary is bounded work; without a cap a model that starts repeating
|
|
43
|
+
// itself runs to the context limit before the call's deadline stops it.
|
|
44
|
+
max_tokens: 512,
|
|
45
|
+
messages: [
|
|
46
|
+
{
|
|
47
|
+
role: 'system',
|
|
48
|
+
content: `Summarize the user's text as ${target}. Reply with the summary only.`,
|
|
49
|
+
},
|
|
50
|
+
{ role: 'user', content: text },
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return { summary: content.trim(), usage }
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Synthesize speech with this server's own TTS generator and hand back a
|
|
2
|
+
// download URL — the caller never needs filesystem access to the server.
|
|
3
|
+
//
|
|
4
|
+
// Needs an onnx-tts generator in the server config, e.g.:
|
|
5
|
+
//
|
|
6
|
+
// [[generators]]
|
|
7
|
+
// type = "onnx-tts"
|
|
8
|
+
// [generators.model]
|
|
9
|
+
// repo_id = "Xenova/speecht5_tts"
|
|
10
|
+
//
|
|
11
|
+
// curl -X POST http://<host>:<port>/functions/text-to-speech \
|
|
12
|
+
// -H 'Content-Type: application/json' \
|
|
13
|
+
// -d '{"text": "Hello from Buttress"}'
|
|
14
|
+
// # → { "result": { "url": "/functions/files/…", … } }
|
|
15
|
+
// curl -OJ "http://<host>:<port>$url" # same auth headers as the call
|
|
16
|
+
|
|
17
|
+
export const meta: ButtressFunctionMeta = {
|
|
18
|
+
description: 'Synthesize speech from text with the local TTS model; returns a download URL',
|
|
19
|
+
parameters: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
text: { type: 'string', description: 'Text to speak' },
|
|
23
|
+
speaker: { type: 'string', description: 'Optional registered speaker id' },
|
|
24
|
+
model: { type: 'string', description: 'Optional TTS model override' },
|
|
25
|
+
},
|
|
26
|
+
required: ['text'],
|
|
27
|
+
},
|
|
28
|
+
timeout: '5m',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Input = { text: string; speaker?: string; model?: string }
|
|
32
|
+
|
|
33
|
+
export default async function ({ text, speaker, model }: Input, context: ButtressFunctionContext) {
|
|
34
|
+
const {
|
|
35
|
+
path: audioPath,
|
|
36
|
+
sampling_rate,
|
|
37
|
+
channels,
|
|
38
|
+
} = await context.buttress.synthesize({
|
|
39
|
+
text,
|
|
40
|
+
model,
|
|
41
|
+
options: speaker ? { speaker } : {},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
// Download with the same Authorization the function call used; the file
|
|
46
|
+
// stays available until the scratch-dir sweep (~24h).
|
|
47
|
+
url: context.fileUrl(audioPath),
|
|
48
|
+
sampling_rate,
|
|
49
|
+
channels,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Transcribe any audio or video file: ffmpeg extracts 16 kHz mono WAV audio
|
|
2
|
+
// into the per-call scratch directory, then this server's STT generator
|
|
3
|
+
// transcribes it. Progress reaches SSE callers (`?stream=1`) via context.emit.
|
|
4
|
+
//
|
|
5
|
+
// Needs `ffmpeg` on the server's PATH and an STT generator (ggml-stt or
|
|
6
|
+
// onnx-stt) in the server config.
|
|
7
|
+
//
|
|
8
|
+
// Remote callers send the media inline — a multipart body stages every file
|
|
9
|
+
// field and injects its server-local path into the input, so one request
|
|
10
|
+
// does the whole job (bump [server] max_body_size for large files):
|
|
11
|
+
//
|
|
12
|
+
// curl -X POST 'http://<host>:<port>/functions/transcribe-media?stream=1' \
|
|
13
|
+
// -F file=@interview.mp4
|
|
14
|
+
//
|
|
15
|
+
// To reuse one file across several calls, stage it once instead and pass the
|
|
16
|
+
// returned path as {"file": "<path>"}:
|
|
17
|
+
//
|
|
18
|
+
// curl -F file=@interview.mp4 http://<host>:<port>/functions/upload
|
|
19
|
+
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
|
|
22
|
+
export const meta: ButtressFunctionMeta = {
|
|
23
|
+
description: 'Transcribe an audio/video file using ffmpeg and the local STT model',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
file: { type: 'string', description: 'Absolute path to a media file on the server' },
|
|
28
|
+
model: { type: 'string', description: 'Optional STT model override' },
|
|
29
|
+
},
|
|
30
|
+
required: ['file'],
|
|
31
|
+
},
|
|
32
|
+
timeout: '15m',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Input = { file: string; model?: string }
|
|
36
|
+
|
|
37
|
+
export default async function ({ file, model }: Input, context: ButtressFunctionContext) {
|
|
38
|
+
const wavPath = path.join(context.tempDir, 'audio.wav')
|
|
39
|
+
|
|
40
|
+
context.emit('stage', 'extract-audio')
|
|
41
|
+
// -hide_banner/-loglevel error keep stderr to the actual failure, so a
|
|
42
|
+
// thrown error reaches the caller instead of ffmpeg's build configuration.
|
|
43
|
+
const { code, stderr } = await context.spawn('ffmpeg', [
|
|
44
|
+
'-hide_banner',
|
|
45
|
+
'-loglevel',
|
|
46
|
+
'error',
|
|
47
|
+
'-y',
|
|
48
|
+
'-i',
|
|
49
|
+
file,
|
|
50
|
+
'-vn',
|
|
51
|
+
'-ac',
|
|
52
|
+
'1',
|
|
53
|
+
'-ar',
|
|
54
|
+
'16000',
|
|
55
|
+
wavPath,
|
|
56
|
+
])
|
|
57
|
+
if (code !== 0) {
|
|
58
|
+
throw new Error(`ffmpeg failed (${code}): ${String(stderr).slice(-2000)}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
context.emit('stage', 'transcribe')
|
|
62
|
+
const transcription = await context.buttress.transcribe({ model, filePath: wavPath })
|
|
63
|
+
|
|
64
|
+
return { transcription }
|
|
65
|
+
}
|
package/config/sample.toml
CHANGED
|
@@ -29,6 +29,22 @@ enabled = true
|
|
|
29
29
|
# cors_allowed_origins = ["http://localhost:3000", "https://example.com"]
|
|
30
30
|
# cors_allowed_origins = "*"
|
|
31
31
|
|
|
32
|
+
# Local functions (EXPERIMENTAL): .ts/.js files exposed as MCP tools and HTTP
|
|
33
|
+
# endpoints. Interfaces may change between releases.
|
|
34
|
+
# See the "Local Functions" section of README.md; ready-to-copy examples live
|
|
35
|
+
# in config/function-samples/. An optional _auth.ts in the functions directory
|
|
36
|
+
# customizes auth (extra gate, or full replacement of workspace auth).
|
|
37
|
+
# [functions]
|
|
38
|
+
# enabled = true
|
|
39
|
+
# dir = "./functions" # relative to this config file
|
|
40
|
+
# default_timeout = "5m" # per-call deadline; meta.timeout overrides it
|
|
41
|
+
# hot_reload = false # watch the dir and reload eagerly on change
|
|
42
|
+
# allow_unauthenticated = false # UNBOUND servers reject calls unless this is true
|
|
43
|
+
# cors_allowed_origins = ["http://localhost:3000"] # browsers are blocked unless listed
|
|
44
|
+
# [functions.config] # free-form; reaches functions as context.config
|
|
45
|
+
# api_base = "https://example.internal"
|
|
46
|
+
# api_keys = ["a-long-random-string"] # e.g. consumed by function-samples/_auth.ts
|
|
47
|
+
|
|
32
48
|
[runtime]
|
|
33
49
|
cache_dir = "./.buttress-cache"
|
|
34
50
|
# huggingface_token = "hf_xx"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const runUpdateCommand: (args: string[]) => Promise<never>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional operator-supplied auth for the `/functions` surface.
|
|
3
|
+
*
|
|
4
|
+
* Dropping `_auth.ts` (or `.js`) into the functions directory activates it —
|
|
5
|
+
* the underscore keeps it out of tool discovery like any other helper file.
|
|
6
|
+
* `meta.mode` decides how it composes with the built-in workspace auth:
|
|
7
|
+
*
|
|
8
|
+
* - 'both' (default): workspace auth runs first, unchanged; the custom
|
|
9
|
+
* function is an additional gate, so it can only narrow access.
|
|
10
|
+
* - 'override': the custom function is the only authority. A presented
|
|
11
|
+
* workspace token is still verified so the function can honor it via
|
|
12
|
+
* `request.workspaceAuth` (e.g. accept workspace tokens OR an API key).
|
|
13
|
+
*
|
|
14
|
+
* Failure never widens access: while an `_auth` file exists but does not
|
|
15
|
+
* load, every call is rejected until it is fixed. Reload follows the same
|
|
16
|
+
* mtime-driven lazy scheme as function files, and deleting the file reverts
|
|
17
|
+
* the server to plain workspace auth on the next request.
|
|
18
|
+
*/
|
|
19
|
+
import type { FunctionsConfig, LoadedAuthFunction } from './types';
|
|
20
|
+
export declare const AUTH_BASENAME = "_auth";
|
|
21
|
+
export type AuthLoader = {
|
|
22
|
+
/**
|
|
23
|
+
* Current auth function, or null when no `_auth` file exists. Throws when
|
|
24
|
+
* the file is present but unloadable — callers must treat that as deny-all.
|
|
25
|
+
*/
|
|
26
|
+
get: () => Promise<LoadedAuthFunction | null>;
|
|
27
|
+
};
|
|
28
|
+
export declare const createAuthLoader: (config: FunctionsConfig) => AuthLoader;
|