@pie-project/server 0.5.1 → 0.6.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.
package/README.md CHANGED
@@ -1,39 +1,37 @@
1
1
  # @pie-project/server
2
2
 
3
3
  What `pie serve` boots (controller, gateway, worker, the built-in
4
- inferlets), inside your Node.js process on the native GPU engines. Clients
5
- connect as they would to a `pie serve`: over WebSocket with
6
- `@pie-project/client`, or over the OpenAI / Anthropic / Gemini HTTP routes.
4
+ inferlets), inside your process. Under Node.js it runs on the native GPU
5
+ engines; under a bundler, the same import is pie compiled for the browser,
6
+ on WebGPU. Clients connect as they would to a `pie serve`: with
7
+ `@pie-project/client` over the server's socket, or (Node) over the OpenAI /
8
+ Anthropic / Gemini HTTP routes.
7
9
 
8
10
  ```js
9
11
  import { Server } from '@pie-project/server';
12
+ import '@pie-project/language-python'; // Python inferlets run; one line per language
10
13
 
11
14
  const server = await Server.start({
12
- server: { port: 0 }, // 0: a free port; see server.url
13
- model: { model: 'Qwen/Qwen3.5-0.8B' }, // what `pie model import` fetched
15
+ model: 'Qwen/Qwen3.5-0.8B', // Node: what `pie model import` fetched
16
+ // model: '/models/qwen3.5-0.8b.wgpu.zt', // browser: the artifact's URL, served with Range support
17
+ config: { server: { listen: '127.0.0.1:0' } }, // the rest of `pie serve`'s config
14
18
  });
15
19
 
16
- const client = await server.connect(); // a PieClient over ws
20
+ await server.install('./beam_search.wasm'); // an inferlet of your own: a path or URL, or bytes with a file name
21
+ const client = await server.connect(); // a PieClient
17
22
  const process = await client.launchProcess('text-completion', { prompt: 'Hello' });
18
23
  console.log(await process.result());
19
-
20
- const reply = await fetch(server.httpUrl + '/v1/chat/completions', {
21
- method: 'POST',
22
- headers: { 'content-type': 'application/json' },
23
- body: JSON.stringify({ messages: [{ role: 'user', content: 'Hi' }] }),
24
- });
25
- console.log((await reply.json()).choices[0].message.content);
26
-
27
24
  await server.shutdown();
28
25
  ```
29
26
 
30
- `Server.start` takes the config `pie serve --config` reads (what `pie config
31
- init` writes), as an object or as TOML text, and resolves once the model is
32
- loaded and the listener is bound. A handle garbage-collected without
33
- `shutdown()` still stops the engine, on a background thread.
27
+ `Server.start` resolves once the model is loaded and the server is
28
+ listening, with every registered language component installed; `config` is
29
+ the file `pie serve --config` reads, minus the model, as an object. A bare
30
+ `Server.start(config)` with `model.source` inside, as an object or TOML text,
31
+ is accepted too. Under Node, `server.httpUrl` is where the HTTP routes live.
32
+ A handle garbage-collected without `shutdown()` still stops the engine.
34
33
 
35
- The Python twin is the `pie-server` wheel (`python/server`); the browser
36
- build of pie is `javascript/browser`.
34
+ The Python twin is the `pie-server` wheel (`packages/python/server`).
37
35
 
38
36
  ## Installing
39
37
 
@@ -41,16 +39,24 @@ build of pie is `javascript/browser`.
41
39
  npm install @pie-project/server
42
40
  ```
43
41
 
44
- The addon comes as an optional dependency per platform
45
- (`@pie-project/server-linux-x64`, `-linux-arm64`, `-darwin-arm64`,
42
+ Under Node the engine is a native addon, one optional dependency per
43
+ platform (`@pie-project/server-linux-x64`, `-linux-arm64`, `-darwin-arm64`,
46
44
  `-win32-x64`), each carrying every engine that platform supports (Linux:
47
- CUDA, Vulkan, wgpu; macOS: Metal, wgpu; Windows: CUDA, Vulkan); the config's
48
- `engine.type` picks one at boot.
45
+ CUDA, Vulkan, wgpu; macOS: Metal, wgpu; Windows: CUDA, Vulkan); the
46
+ config's `engine.type` picks one at boot. Under a bundler (Vite, webpack 5)
47
+ the `browser` export condition resolves to `@pie-project/server-web`: the
48
+ runtime and the WebGPU engine as wasm, run in a Web Worker. The page must be
49
+ cross-origin isolated and the browser needs WebGPU and JSPI (Chrome 137+);
50
+ `packages/javascript/server-web/README.md` has the details.
51
+
52
+ Language components (`@pie-project/language-python`,
53
+ `@pie-project/language-javascript`) register themselves on import; a
54
+ registration after `start()` lands on the running server too.
49
55
 
50
56
  ## Building
51
57
 
52
- From source, the addon is built from this directory with the engine features
53
- for your GPU (`cuda` by default, `metal` on macOS):
58
+ From source, the Node addon is built from this directory with the engine
59
+ features for your GPU (`cuda` by default, `metal` on macOS):
54
60
 
55
61
  ```bash
56
62
  npm run build # release, default features
@@ -59,4 +65,5 @@ bash build.sh dev --no-default-features # no engine: loads, refuses to serve
59
65
  ```
60
66
 
61
67
  It lands beside `index.mjs` as `pie-server.<platform>-<arch>.node`, which
62
- the loader prefers over the platform package.
68
+ the loader prefers over the platform package. The browser bundle is
69
+ `packages/javascript/server-web/build.sh`.
package/index.d.ts CHANGED
@@ -3,19 +3,36 @@ import type { PieClient } from '@pie-project/client';
3
3
  /** The native addon file this platform loads, or null when none is built. */
4
4
  export function addonPath(): string | null;
5
5
 
6
- /** A running pie engine in this process. */
6
+ /** `pie serve`'s config: the file's shape, as an object or TOML text. */
7
+ export type Config = string | Record<string, unknown>;
8
+
9
+ export interface StartOptions {
10
+ /** What to serve: a Hugging Face repo, a path, an imported artifact's name. In the browser: the artifact's URL. */
11
+ model: string;
12
+ /** The rest of the config (server, engine, sandbox, …), as an object; TOML text is accepted for a bare `start(config)`. */
13
+ config?: Record<string, unknown> | string;
14
+ /** Node: where the runtime keeps its files (installed inferlets, languages, caches, models); `$PIE_HOME`, else `~/.pie`. */
15
+ home?: string;
16
+ /** Browser only: download progress of a lazy boot. */
17
+ onProgress?: (served: number, total: number, from: 'network' | 'cache') => void;
18
+ }
19
+
20
+ /** A running pie engine in this process. The same class serves in Node (native engines) and in the browser (WebGPU). */
7
21
  export class Server {
8
22
  /**
9
- * Boot the engine from the TOML text `pie serve --config` reads, or an
10
- * object of the same shape. Resolves once the engine is serving.
23
+ * Boot the engine; resolves once it is serving, with every registered
24
+ * language component (`import "@pie-project/language-python"`) installed.
11
25
  */
12
- static start(config: string | Record<string, unknown>): Promise<Server>;
13
- /** `ws://host:port` the engine listens on. */
26
+ static start(options: StartOptions | Config): Promise<Server>;
27
+ /** `ws://host:port` the engine listens on; `pie://local` in the browser. */
14
28
  readonly url: string;
15
- /** `http://host:port`, where the OpenAI / Anthropic / Gemini routes live. */
16
- readonly httpUrl: string;
29
+ /** Node: `http://host:port`, where the OpenAI / Anthropic / Gemini routes live. */
30
+ readonly httpUrl?: string;
17
31
  /** True until `shutdown()` resolves. */
18
32
  readonly running: boolean;
33
+ /** A language component (`python`, `javascript`) from bytes or a URL. */
34
+ installLanguage(language: 'python' | 'javascript' | string, source: Uint8Array | ArrayBuffer | URL | string): Promise<string>;
35
+ install(source: Uint8Array | ArrayBuffer | URL | string, file?: string | null, version?: string | null): Promise<string>;
19
36
  /** A `PieClient` connected to this engine; closed by `shutdown()`. */
20
37
  connect(options?: { identity?: string }): Promise<PieClient>;
21
38
  /** Stop the engine and release it. Idempotent. */
package/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // `@pie-project/server`: what `pie serve` boots, inside a Node process.
2
2
  //
3
3
  // import { Server } from '@pie-project/server';
4
- // const server = await Server.start({ server: { port: 0 }, model: { model: 'Qwen/Qwen3.5-0.8B' } });
4
+ // const server = await Server.start({ server: { listen: '127.0.0.1:0' }, model: { source: 'Qwen/Qwen3.5-0.8B' } });
5
5
  // const client = await server.connect();
6
6
  // ...
7
7
  // await server.shutdown();
@@ -12,11 +12,14 @@
12
12
  // dependency; this wrapper loads it and adds `connect()`.
13
13
 
14
14
  import { createRequire } from 'node:module';
15
- import { fileURLToPath } from 'node:url';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { homedir } from 'node:os';
16
17
  import { dirname, join } from 'node:path';
17
18
  import { existsSync } from 'node:fs';
19
+ import { readFile } from 'node:fs/promises';
18
20
  import WebSocket from 'ws';
19
- import { PieClient } from '@pie-project/client';
21
+ import { PieClient, programFile } from '@pie-project/client';
22
+ import { attachLanguages } from '@pie-project/server-web/languages';
20
23
 
21
24
  const here = dirname(fileURLToPath(import.meta.url));
22
25
  const require = createRequire(import.meta.url);
@@ -48,25 +51,74 @@ function load() {
48
51
  return native;
49
52
  }
50
53
 
54
+ function isBytes(source) {
55
+ return source instanceof Uint8Array || source instanceof ArrayBuffer;
56
+ }
57
+
58
+ function urlOf(source) {
59
+ if (source instanceof URL) return source;
60
+ try {
61
+ return new URL(String(source));
62
+ } catch {
63
+ return pathToFileURL(String(source));
64
+ }
65
+ }
66
+
67
+ /** Bytes from what `install*` accept: bytes, or a file/http URL to read. */
68
+ async function bytesOf(source) {
69
+ if (source instanceof Uint8Array) return source;
70
+ if (source instanceof ArrayBuffer) return new Uint8Array(source);
71
+ const url = urlOf(source);
72
+ if (url.protocol === 'file:') return new Uint8Array(await readFile(fileURLToPath(url)));
73
+ const response = await fetch(url);
74
+ if (!response.ok) throw new Error(`${url}: ${response.status} ${response.statusText}`);
75
+ return new Uint8Array(await response.arrayBuffer());
76
+ }
77
+
78
+ /** Under Node a language package that is installed need not be imported by hand: importing it here registers it. */
79
+ async function importInstalledLanguages() {
80
+ for (const language of ['python', 'javascript']) {
81
+ try {
82
+ await import(`@pie-project/language-${language}`);
83
+ } catch (error) {
84
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
85
+ }
86
+ }
87
+ }
88
+
51
89
  /** A running pie engine in this process. */
52
90
  export class Server {
53
91
  #handle;
54
92
  #clients = [];
93
+ #detach = null;
55
94
 
56
95
  constructor(handle) {
57
96
  this.#handle = handle;
58
97
  }
59
98
 
60
99
  /**
61
- * Boot the engine from the TOML text `pie serve --config` reads, or an
62
- * object of the same shape (`{server: {port: 0}, model: {…}}`).
63
- * `server.port = 0` asks the OS for a free port; `url` says which.
64
- * Resolves once the engine is serving — minutes for a large model.
100
+ * Boot the engine. `{ model, config }` names the model (`Qwen/Qwen3.5-0.8B`,
101
+ * a path, an artifact) and gives the rest of `pie serve`'s config as an
102
+ * object; a bare config — TOML text, or an object of the file's shape with
103
+ * `model.source` inside — is accepted as well. `server.listen` at port 0 asks the OS
104
+ * for a free port; `url` says which. Resolves once the engine is serving,
105
+ * with every registered language component installed.
65
106
  */
66
- static async start(config) {
107
+ static async start(options) {
108
+ let config = options;
109
+ let home = process.env.PIE_HOME?.trim() || join(homedir(), '.pie');
110
+ if (options && typeof options === 'object' && typeof options.model === 'string') {
111
+ const { model, config: rest = {} } = options;
112
+ if (typeof rest !== 'object') throw new Error('Server.start: with `model`, `config` is an object');
113
+ config = { ...rest, model: { name: 'default', ...(rest.model ?? {}), source: model } };
114
+ home = options.home ?? home;
115
+ }
67
116
  const api = load();
68
- const handle = typeof config === 'string' ? await api.startToml(config) : await api.start(config);
69
- return new Server(handle);
117
+ const handle = typeof config === 'string' ? await api.startToml(config, home) : await api.start(config, home);
118
+ const server = new Server(handle);
119
+ await importInstalledLanguages();
120
+ server.#detach = await attachLanguages(server);
121
+ return server;
70
122
  }
71
123
 
72
124
  /** `ws://host:port` the engine listens on. */
@@ -84,10 +136,24 @@ export class Server {
84
136
  return this.#handle.running;
85
137
  }
86
138
 
139
+ /** A language component (`python`, `javascript`) from bytes or a URL; a script inferlet in that language runs once this resolves. */
140
+ async installLanguage(language, source) {
141
+ const handle = this.#alive();
142
+ return handle.installLanguage(language, Buffer.from(await bytesOf(source)));
143
+ }
144
+
145
+ async install(source, file = null, version = null) {
146
+ const handle = this.#alive();
147
+ if (!file) {
148
+ if (isBytes(source)) throw new Error('Server.install: bytes need `file`, the program\'s file name (`x.wasm`, `x.py`, `x.js`)');
149
+ file = programFile(urlOf(source));
150
+ }
151
+ return handle.install(Buffer.from(await bytesOf(source)), file, version ?? undefined);
152
+ }
153
+
87
154
  /** A `PieClient` connected to this engine (over Node's `ws`, which can send the identity header). Closed by `shutdown()`. */
88
155
  async connect({ identity = 'default/node' } = {}) {
89
- if (!this.running) throw new Error('the server is shut down');
90
- const client = new PieClient(this.url, { WebSocket, identity });
156
+ const client = new PieClient(this.#alive().url, { WebSocket, identity });
91
157
  await client.connect();
92
158
  this.#clients.push(client);
93
159
  return client;
@@ -95,6 +161,8 @@ export class Server {
95
161
 
96
162
  /** Close the clients from `connect()`, then stop the engine. Idempotent. */
97
163
  async shutdown() {
164
+ this.#detach?.();
165
+ this.#detach = null;
98
166
  for (const client of this.#clients.splice(0)) {
99
167
  try {
100
168
  await client.close();
@@ -104,4 +172,9 @@ export class Server {
104
172
  }
105
173
  await this.#handle.shutdown();
106
174
  }
175
+
176
+ #alive() {
177
+ if (!this.running) throw new Error('the server is shut down');
178
+ return this.#handle;
179
+ }
107
180
  }
package/languages.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from '@pie-project/server-web/languages';
package/languages.mjs ADDED
@@ -0,0 +1,3 @@
1
+ // `import { registerLanguage } from "@pie-project/server/languages"`: what a
2
+ // language package calls on import.
3
+ export * from '@pie-project/server-web/languages';
package/package.json CHANGED
@@ -1,19 +1,28 @@
1
1
  {
2
2
  "name": "@pie-project/server",
3
- "version": "0.5.1",
4
- "description": "What `pie serve` boots, inside a Node.js process, on the native GPU engines.",
3
+ "version": "0.6.0",
4
+ "description": "What `pie serve` boots, inside your process: on the native GPU engines under Node.js, on WebGPU under a bundler for the browser.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "types": "index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./index.d.ts",
11
- "import": "./index.mjs"
11
+ "browser": "./web.mjs",
12
+ "node": "./index.mjs",
13
+ "default": "./index.mjs"
14
+ },
15
+ "./languages": {
16
+ "types": "./languages.d.ts",
17
+ "default": "./languages.mjs"
12
18
  }
13
19
  },
14
20
  "files": [
15
21
  "index.mjs",
16
22
  "index.d.ts",
23
+ "web.mjs",
24
+ "languages.mjs",
25
+ "languages.d.ts",
17
26
  "README.md"
18
27
  ],
19
28
  "scripts": {
@@ -30,19 +39,20 @@
30
39
  "win32"
31
40
  ],
32
41
  "dependencies": {
33
- "@pie-project/client": "^0.5.0",
34
- "ws": "^8.18.0"
35
- },
36
- "optionalDependencies": {
37
- "@pie-project/server-linux-x64": "0.5.1",
38
- "@pie-project/server-linux-arm64": "0.5.1",
39
- "@pie-project/server-darwin-arm64": "0.5.1",
40
- "@pie-project/server-win32-x64": "0.5.1"
42
+ "@pie-project/client": "^0.6.0",
43
+ "@pie-project/server-web": "0.6.0",
44
+ "ws": "^8.22.0"
41
45
  },
42
46
  "license": "Apache-2.0",
43
47
  "repository": {
44
48
  "type": "git",
45
49
  "url": "https://github.com/pie-project/pie",
46
- "directory": "javascript/server"
50
+ "directory": "packages/javascript/server"
51
+ },
52
+ "optionalDependencies": {
53
+ "@pie-project/server-darwin-arm64": "0.6.0",
54
+ "@pie-project/server-linux-arm64": "0.6.0",
55
+ "@pie-project/server-linux-x64": "0.6.0",
56
+ "@pie-project/server-win32-x64": "0.6.0"
47
57
  }
48
58
  }
package/web.mjs ADDED
@@ -0,0 +1,3 @@
1
+ // The browser half: what a bundler resolves `@pie-project/server` to under
2
+ // its `browser` condition.
3
+ export * from '@pie-project/server-web';