@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.
Files changed (45) hide show
  1. package/README.md +238 -17
  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 +16 -0
  9. package/lib/cli-update.d.ts +1 -0
  10. package/lib/functions/auth.d.ts +28 -0
  11. package/lib/functions/config.d.ts +20 -0
  12. package/lib/functions/constants.d.ts +17 -0
  13. package/lib/functions/executor.d.ts +27 -0
  14. package/lib/functions/files.d.ts +29 -0
  15. package/lib/functions/index.d.ts +52 -0
  16. package/lib/functions/input.d.ts +11 -0
  17. package/lib/functions/libs.d.ts +10 -0
  18. package/lib/functions/loader.d.ts +48 -0
  19. package/lib/functions/mcp.d.ts +35 -0
  20. package/lib/functions/query.d.ts +27 -0
  21. package/lib/functions/registry.d.ts +35 -0
  22. package/lib/functions/scaffold.d.ts +18 -0
  23. package/lib/functions/status.d.ts +119 -0
  24. package/lib/functions/templates.d.ts +14 -0
  25. package/lib/functions/transpile.d.ts +23 -0
  26. package/lib/functions/types.d.ts +196 -0
  27. package/lib/functions/uploads.d.ts +42 -0
  28. package/lib/functions/watcher.d.ts +34 -0
  29. package/lib/index.d.ts +11 -3
  30. package/lib/index.mjs +347 -63
  31. package/lib/routes/functions.check.d.ts +13 -0
  32. package/lib/routes/functions.d.ts +17 -0
  33. package/lib/routes/generator-cache.d.ts +58 -0
  34. package/lib/routes/index.d.ts +1 -0
  35. package/lib/routes/llm-shared.d.ts +22 -19
  36. package/lib/routes/stt-shared.d.ts +34 -0
  37. package/lib/routes/tts-shared.d.ts +34 -0
  38. package/lib/services/create-onnx-init-context.d.ts +10 -0
  39. package/lib/services/onnx-stt.d.ts +1 -1
  40. package/lib/services/onnx-tts.d.ts +1 -1
  41. package/lib/types.d.ts +23 -0
  42. package/lib/utils/functionsAuthGuard.d.ts +12 -0
  43. package/lib/utils/update.d.ts +38 -0
  44. package/package.json +23 -3
  45. package/public/status.html +116 -0
@@ -0,0 +1,196 @@
1
+ import type { Backend } from '../index';
2
+ import type { Config } from '../types';
3
+ import type { VerifiedIdentity } from '../utils/buttressAuth';
4
+ /**
5
+ * Declarative metadata a local function file exports alongside its handler.
6
+ *
7
+ * `parameters` is a raw JSON Schema object: it is handed to MCP `tools/list`
8
+ * verbatim, so authors describe their input once and every surface agrees.
9
+ */
10
+ export type FunctionMeta = {
11
+ description?: string;
12
+ parameters?: Record<string, any>;
13
+ /** Per-function deadline. Number of ms, or an `ms()` string like "10m". */
14
+ timeout?: number | string;
15
+ };
16
+ export type FunctionEmit = (event: string, data?: unknown) => void;
17
+ export type SpawnOptions = {
18
+ cwd?: string;
19
+ env?: Record<string, string | undefined>;
20
+ /** Written to the child's stdin, which is then closed. */
21
+ input?: string | Uint8Array;
22
+ /** 'utf8' (default) resolves stdout/stderr as strings, 'buffer' as Buffer. */
23
+ encoding?: 'utf8' | 'buffer';
24
+ /** Cap on captured stdout/stderr per stream (default 8MB). */
25
+ maxBuffer?: number;
26
+ onStdout?: (chunk: Buffer) => void;
27
+ onStderr?: (chunk: Buffer) => void;
28
+ };
29
+ export type SpawnResult = {
30
+ code: number | null;
31
+ signal: NodeJS.Signals | null;
32
+ stdout: string | Buffer;
33
+ stderr: string | Buffer;
34
+ /** True when either stream hit `maxBuffer` and capture stopped early. */
35
+ truncated: boolean;
36
+ };
37
+ /**
38
+ * `messages` are rendered with the model's own chat template and thinking is
39
+ * off by default (see `CHAT_COMPLETION_DEFAULTS`); anything passed explicitly
40
+ * overrides that. Unlisted keys reach the backend verbatim.
41
+ */
42
+ export type CompletionOptions = {
43
+ model?: string;
44
+ messages?: any[];
45
+ prompt?: string;
46
+ /** Cap on generated tokens; mapped onto the backend's `n_predict`. */
47
+ max_tokens?: number;
48
+ /** Emit reasoning into `reasoning_content` instead of suppressing it. */
49
+ enable_thinking?: boolean;
50
+ onToken?: (token: Record<string, any>) => void;
51
+ } & Record<string, any>;
52
+ export type CompletionResult = {
53
+ content: string;
54
+ reasoning_content?: string;
55
+ tool_calls?: any[];
56
+ interrupted?: boolean;
57
+ usage: {
58
+ prompt_tokens: number;
59
+ completion_tokens: number;
60
+ total_tokens: number;
61
+ };
62
+ };
63
+ export type TranscribeOptions = {
64
+ model?: string;
65
+ /** Absolute (or functions-dir relative) path to an audio file. */
66
+ filePath?: string;
67
+ /** Raw audio bytes; mutually exclusive with `filePath`. */
68
+ audioData?: Uint8Array | Buffer;
69
+ options?: Record<string, any>;
70
+ };
71
+ export type SynthesizeOptions = {
72
+ model?: string;
73
+ text: string;
74
+ /** Passed to the TTS backend verbatim; `options.speaker` picks a voice. */
75
+ options?: Record<string, any>;
76
+ };
77
+ export type SynthesizeResult = {
78
+ /** WAV file inside the call's `tempDir` — pair with `context.fileUrl`. */
79
+ path: string;
80
+ sampling_rate: number;
81
+ channels: number;
82
+ };
83
+ /** The second argument every local function handler receives. */
84
+ export type FunctionContext = {
85
+ spawn: (command: string, args?: string[], options?: SpawnOptions) => Promise<SpawnResult>;
86
+ buttress: {
87
+ completion: (options: CompletionOptions) => Promise<CompletionResult>;
88
+ transcribe: (options: TranscribeOptions) => Promise<any>;
89
+ synthesize: (options: SynthesizeOptions) => Promise<SynthesizeResult>;
90
+ };
91
+ fetch: typeof fetch;
92
+ log: (...args: unknown[]) => void;
93
+ emit: FunctionEmit;
94
+ signal: AbortSignal;
95
+ env: Record<string, string | undefined>;
96
+ config: Record<string, any>;
97
+ /** Per-call scratch directory, created on first access. */
98
+ tempDir: string;
99
+ /**
100
+ * Download URL path for a file inside `tempDir` (relative input resolves
101
+ * against it). Callers fetch it with the same auth as any function call.
102
+ */
103
+ fileUrl: (target: string) => string;
104
+ dir: string;
105
+ libs: Record<string, any>;
106
+ };
107
+ export type FunctionHandler = (input: any, context: FunctionContext) => any;
108
+ export type LoadedFunction = {
109
+ name: string;
110
+ file: string;
111
+ meta: FunctionMeta;
112
+ handler: FunctionHandler;
113
+ timeoutMs: number;
114
+ /** Every file in this function's module graph → mtimeMs when it was loaded. */
115
+ files: Map<string, number>;
116
+ };
117
+ export type FunctionSummary = {
118
+ name: string;
119
+ description: string;
120
+ parameters: Record<string, any>;
121
+ };
122
+ export type FunctionsConfig = {
123
+ enabled: boolean;
124
+ dir: string;
125
+ allowUnauthenticated: boolean;
126
+ defaultTimeoutMs: number;
127
+ /**
128
+ * Watch the functions directory and reload eagerly on change (off by
129
+ * default). Lazy mtime reload stays active either way.
130
+ */
131
+ hotReload: boolean;
132
+ /** Free-form `[functions.config]` table, surfaced as `context.config`. */
133
+ userConfig: Record<string, any>;
134
+ };
135
+ /** Per-call dependencies the executor needs from the Elysia store. */
136
+ export type FunctionRuntime = {
137
+ backend: Backend;
138
+ config: Config;
139
+ };
140
+ /** Metadata an `_auth` file exports alongside its authorize handler. */
141
+ export type AuthMeta = {
142
+ /**
143
+ * How the custom function composes with the built-in workspace auth.
144
+ * 'both' (default): runs after workspace auth passes, as an extra gate.
145
+ * 'override': replaces workspace auth — the function is the only authority
146
+ * (a presented workspace token is still verified into `workspaceAuth`).
147
+ */
148
+ mode?: 'override' | 'both';
149
+ };
150
+ /** Workspace-auth outcome handed to a custom auth function. */
151
+ export type WorkspaceAuthInfo = {
152
+ /** Whether this server is bound to a workspace. */
153
+ bound: boolean;
154
+ /** Whether the caller presented a valid workspace access token. */
155
+ authenticated: boolean;
156
+ identity: VerifiedIdentity | null;
157
+ };
158
+ /** The first argument the authorize handler receives. */
159
+ export type AuthRequestInfo = {
160
+ method: string;
161
+ path: string;
162
+ /** Function name for `GET`/`POST /functions/<name>`; undefined for list and MCP. */
163
+ name?: string;
164
+ headers: Record<string, string | undefined>;
165
+ query: Record<string, any>;
166
+ /** Raw bearer token (Authorization header or `?token=`), if any. */
167
+ token: string | null;
168
+ workspaceAuth: WorkspaceAuthInfo;
169
+ };
170
+ /** The second argument the authorize handler receives. */
171
+ export type AuthFunctionContext = {
172
+ log: (...args: unknown[]) => void;
173
+ fetch: typeof fetch;
174
+ env: Record<string, string | undefined>;
175
+ /** The `[functions.config]` table, same as `context.config` in functions. */
176
+ config: Record<string, any>;
177
+ dir: string;
178
+ libs: Record<string, any>;
179
+ };
180
+ /** Only `true` (or `{ ok: true }`) allows the request; anything else denies. */
181
+ export type AuthDecision = boolean | {
182
+ ok: boolean;
183
+ /** Response status for a denial, 400–499 (default 403). */
184
+ status?: number;
185
+ /** Message returned to the caller on denial. */
186
+ error?: string;
187
+ };
188
+ export type AuthHandler = (request: AuthRequestInfo, context: AuthFunctionContext) => AuthDecision | Promise<AuthDecision>;
189
+ export type LoadedAuthFunction = {
190
+ file: string;
191
+ mode: 'override' | 'both';
192
+ /** Invoke the operator's authorize handler with its baked-in context. */
193
+ run: (request: AuthRequestInfo) => Promise<AuthDecision>;
194
+ /** Every file in the auth module graph → mtimeMs when it was loaded. */
195
+ files: Map<string, number>;
196
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Staging of uploaded files for the functions surface.
3
+ *
4
+ * Two callers share this: `POST /functions/upload` (standalone staging, one
5
+ * fresh directory per request) and multipart `POST /functions/<name>` (inline
6
+ * staging into the call's own scratch directory, so an input file lives and
7
+ * dies with the call that consumed it).
8
+ */
9
+ /** Multipart field values that carry a file, duck-typed — never `instanceof`. */
10
+ export declare const isUploadFile: (value: unknown) => value is File;
11
+ export type WrittenUpload = {
12
+ path: string;
13
+ name: string;
14
+ size: number;
15
+ };
16
+ /**
17
+ * Stream one uploaded file into `targetDir` under its sanitized client name.
18
+ * Same-named siblings (two multipart files sharing a name) get a numeric
19
+ * prefix instead of clobbering each other.
20
+ */
21
+ export declare const writeUploadFile: (targetDir: string, file: File) => Promise<WrittenUpload>;
22
+ /** A caller mistake in the multipart shape — reported as 400, not 500. */
23
+ export declare class MultipartInputError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ export type StagedUpload = WrittenUpload & {
27
+ field: string;
28
+ };
29
+ export type MultipartInput = {
30
+ input: Record<string, any>;
31
+ staged: StagedUpload[];
32
+ };
33
+ /**
34
+ * Turn a parsed multipart body into a function input object.
35
+ *
36
+ * The optional `input` field carries a JSON object for typed values; every
37
+ * other string field overlays it as a string, and every file field is staged
38
+ * into `uploadsDir` with the resulting server-local path injected under the
39
+ * field's name (an array of paths when the field repeats). Precedence when a
40
+ * key appears more than once: file path > plain field > `input` JSON.
41
+ */
42
+ export declare const stageMultipartInput: (body: Record<string, unknown>, uploadsDir: string) => Promise<MultipartInput>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Optional eager reload for the functions directory.
3
+ *
4
+ * The default reload strategy is lazy (every call stats its module graph), so
5
+ * a change is only *noticed* on the next call. With `[functions] hot_reload`
6
+ * an fs.watch keeps the loaded state warm instead: bursts of change events
7
+ * are debounced and handed to the service, which re-lists functions and
8
+ * re-loads `_auth` — so a broken save is logged at save time, and the
9
+ * function count on `/status` and in ANNOUNCE moves without any traffic.
10
+ *
11
+ * This is an additive optimization, not a correctness mechanism: the lazy
12
+ * mtime check keeps running either way, so a missed watch event costs
13
+ * nothing but immediacy. Watchers are created non-persistent — they never
14
+ * hold the process open.
15
+ */
16
+ import fs from 'node:fs';
17
+ export type FunctionsWatcher = {
18
+ close: () => void;
19
+ };
20
+ export type WatchFn = (target: string, options: {
21
+ recursive: boolean;
22
+ persistent: boolean;
23
+ }, listener: (eventType: string, filename: string | Buffer | null) => void) => fs.FSWatcher;
24
+ export type CreateWatcherOptions = {
25
+ debounceMs?: number;
26
+ /** Injectable for tests; defaults to `fs.watch`. */
27
+ watchFn?: WatchFn;
28
+ };
29
+ /**
30
+ * Watch `dir` recursively and report debounced bursts of changed file names.
31
+ * Returns null (with a warning) where recursive watching is unavailable —
32
+ * callers fall back to lazy reload alone.
33
+ */
34
+ export declare const createFunctionsWatcher: (dir: string, onBurst: (changed: Set<string>) => void, { debounceMs, watchFn }?: CreateWatcherOptions) => FunctionsWatcher | null;
package/lib/index.d.ts CHANGED
@@ -2,10 +2,14 @@ import type { AnyElysia } from 'elysia';
2
2
  import * as backendCore from '@fugood/buttress-backend-core';
3
3
  import { AutodiscoverService } from './autodiscover';
4
4
  import type { Config } from './types';
5
+ import { compareVersions } from './utils/update';
6
+ import type { FunctionsConfig, FunctionsService } from './functions';
5
7
  export { startModelDownload } from '@fugood/buttress-backend-core';
6
8
  export { processConfig } from './utils/config';
9
+ export { resolveFunctionsConfig } from './functions';
10
+ export type { FunctionsConfig, FunctionsService } from './functions';
7
11
  export declare const checkForUpdates: () => Promise<string | null>;
8
- export declare const compareVersions: (current: string, latest: string) => boolean;
12
+ export { compareVersions };
9
13
  export declare const logUpdateMessage: (latestVersion: string) => void;
10
14
  export declare const checkAndNotifyUpdates: () => Promise<void>;
11
15
  export type Backend = typeof backendCore;
@@ -15,15 +19,19 @@ export interface StartServerOptions {
15
19
  config: Config;
16
20
  enableOpenAICompat?: boolean;
17
21
  enableAnthropicMessages?: boolean;
22
+ /** Resolved `[functions]` config; null/omitted leaves the feature off. */
23
+ functions?: FunctionsConfig | null;
18
24
  }
19
- export declare const createServer: ({ backend, router, config, enableOpenAICompat, enableAnthropicMessages, }: StartServerOptions) => Promise<{
25
+ export declare const createServer: ({ backend, router, config, enableOpenAICompat, enableAnthropicMessages, functions: functionsConfig, }: StartServerOptions) => Promise<{
20
26
  app: AnyElysia;
21
27
  config: Config;
28
+ functions: FunctionsService | null;
22
29
  }>;
23
- export declare const startServer: ({ backend, router, config, enableOpenAICompat, enableAnthropicMessages, }: StartServerOptions) => Promise<{
30
+ export declare const startServer: ({ backend, router, config, enableOpenAICompat, enableAnthropicMessages, functions: functionsConfig, }: StartServerOptions) => Promise<{
24
31
  app: AnyElysia;
25
32
  port: number;
26
33
  openaiEnabled: boolean;
27
34
  anthropicMessagesEnabled: boolean;
35
+ functions: FunctionsService | null;
28
36
  autoDiscover: AutodiscoverService | null;
29
37
  }>;