@pie-project/server 0.5.1 → 0.5.2

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: { port: 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(inferletBytes, manifestToml); // an inferlet of your own, from bytes or a URL
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.model` 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 (`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
+ `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
+ `javascript/server-web/build.sh`.
package/index.d.ts CHANGED
@@ -3,19 +3,35 @@ 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
+ /** Browser only: download progress of a lazy boot. */
15
+ onProgress?: (served: number, total: number, from: 'network' | 'cache') => void;
16
+ }
17
+
18
+ /** A running pie engine in this process. The same class serves in Node (native engines) and in the browser (WebGPU). */
7
19
  export class Server {
8
20
  /**
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.
21
+ * Boot the engine; resolves once it is serving, with every registered
22
+ * language component (`import "@pie-project/language-python"`) installed.
11
23
  */
12
- static start(config: string | Record<string, unknown>): Promise<Server>;
13
- /** `ws://host:port` the engine listens on. */
24
+ static start(options: StartOptions | Config): Promise<Server>;
25
+ /** `ws://host:port` the engine listens on; `pie://local` in the browser. */
14
26
  readonly url: string;
15
- /** `http://host:port`, where the OpenAI / Anthropic / Gemini routes live. */
16
- readonly httpUrl: string;
27
+ /** Node: `http://host:port`, where the OpenAI / Anthropic / Gemini routes live. */
28
+ readonly httpUrl?: string;
17
29
  /** True until `shutdown()` resolves. */
18
30
  readonly running: boolean;
31
+ /** A language component (`python`, `javascript`) from bytes or a URL. */
32
+ installLanguage(language: 'python' | 'javascript' | string, source: Uint8Array | ArrayBuffer | URL | string): Promise<string>;
33
+ /** An inferlet from its component bytes (or URL) and manifest text, replacing an installed version; resolves to `name@version`. */
34
+ install(source: Uint8Array | ArrayBuffer | URL | string, manifest: string): Promise<string>;
19
35
  /** A `PieClient` connected to this engine; closed by `shutdown()`. */
20
36
  connect(options?: { identity?: string }): Promise<PieClient>;
21
37
  /** Stop the engine and release it. Idempotent. */
package/index.mjs CHANGED
@@ -15,8 +15,10 @@ import { createRequire } from 'node:module';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { dirname, join } from 'node:path';
17
17
  import { existsSync } from 'node:fs';
18
+ import { readFile } from 'node:fs/promises';
18
19
  import WebSocket from 'ws';
19
20
  import { PieClient } from '@pie-project/client';
21
+ import { attachLanguages } from '@pie-project/server-web/languages';
20
22
 
21
23
  const here = dirname(fileURLToPath(import.meta.url));
22
24
  const require = createRequire(import.meta.url);
@@ -48,25 +50,59 @@ function load() {
48
50
  return native;
49
51
  }
50
52
 
53
+ /** Bytes from what `install*` accept: bytes, or a file/http URL to read. */
54
+ async function bytesOf(source) {
55
+ if (source instanceof Uint8Array) return source;
56
+ if (source instanceof ArrayBuffer) return new Uint8Array(source);
57
+ const url = source instanceof URL ? source : new URL(String(source), 'file://');
58
+ if (url.protocol === 'file:') return new Uint8Array(await readFile(fileURLToPath(url)));
59
+ const response = await fetch(url);
60
+ if (!response.ok) throw new Error(`${url}: ${response.status} ${response.statusText}`);
61
+ return new Uint8Array(await response.arrayBuffer());
62
+ }
63
+
64
+ /** Under Node a language package that is installed need not be imported by hand: importing it here registers it. */
65
+ async function importInstalledLanguages() {
66
+ for (const language of ['python', 'javascript']) {
67
+ try {
68
+ await import(`@pie-project/language-${language}`);
69
+ } catch (error) {
70
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
71
+ }
72
+ }
73
+ }
74
+
51
75
  /** A running pie engine in this process. */
52
76
  export class Server {
53
77
  #handle;
54
78
  #clients = [];
79
+ #detach = null;
55
80
 
56
81
  constructor(handle) {
57
82
  this.#handle = handle;
58
83
  }
59
84
 
60
85
  /**
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.
86
+ * Boot the engine. `{ model, config }` names the model (`Qwen/Qwen3.5-0.8B`,
87
+ * a path, an artifact) and gives the rest of `pie serve`'s config as an
88
+ * object; a bare config — TOML text, or an object of the file's shape with
89
+ * `model.model` inside — is accepted as well. `server.port = 0` asks the OS
90
+ * for a free port; `url` says which. Resolves once the engine is serving,
91
+ * with every registered language component installed.
65
92
  */
66
- static async start(config) {
93
+ static async start(options) {
94
+ let config = options;
95
+ if (options && typeof options === 'object' && typeof options.model === 'string') {
96
+ const { model, config: rest = {} } = options;
97
+ if (typeof rest !== 'object') throw new Error('Server.start: with `model`, `config` is an object');
98
+ config = { ...rest, model: { name: 'default', ...(rest.model ?? {}), model } };
99
+ }
67
100
  const api = load();
68
101
  const handle = typeof config === 'string' ? await api.startToml(config) : await api.start(config);
69
- return new Server(handle);
102
+ const server = new Server(handle);
103
+ await importInstalledLanguages();
104
+ server.#detach = await attachLanguages(server);
105
+ return server;
70
106
  }
71
107
 
72
108
  /** `ws://host:port` the engine listens on. */
@@ -84,10 +120,21 @@ export class Server {
84
120
  return this.#handle.running;
85
121
  }
86
122
 
123
+ /** A language component (`python`, `javascript`) from bytes or a URL; a script inferlet in that language runs once this resolves. */
124
+ async installLanguage(language, source) {
125
+ const handle = this.#alive();
126
+ return handle.installLanguage(language, Buffer.from(await bytesOf(source)));
127
+ }
128
+
129
+ /** An inferlet from its component bytes (or URL) and manifest text, replacing an installed version. Resolves to `name@version`. */
130
+ async install(source, manifest) {
131
+ const handle = this.#alive();
132
+ return handle.install(Buffer.from(await bytesOf(source)), manifest);
133
+ }
134
+
87
135
  /** A `PieClient` connected to this engine (over Node's `ws`, which can send the identity header). Closed by `shutdown()`. */
88
136
  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 });
137
+ const client = new PieClient(this.#alive().url, { WebSocket, identity });
91
138
  await client.connect();
92
139
  this.#clients.push(client);
93
140
  return client;
@@ -95,6 +142,8 @@ export class Server {
95
142
 
96
143
  /** Close the clients from `connect()`, then stop the engine. Idempotent. */
97
144
  async shutdown() {
145
+ this.#detach?.();
146
+ this.#detach = null;
98
147
  for (const client of this.#clients.splice(0)) {
99
148
  try {
100
149
  await client.close();
@@ -104,4 +153,9 @@ export class Server {
104
153
  }
105
154
  await this.#handle.shutdown();
106
155
  }
156
+
157
+ #alive() {
158
+ if (!this.running) throw new Error('the server is shut down');
159
+ return this.#handle;
160
+ }
107
161
  }
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.5.2",
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": {
@@ -31,13 +40,14 @@
31
40
  ],
32
41
  "dependencies": {
33
42
  "@pie-project/client": "^0.5.0",
43
+ "@pie-project/server-web": "0.5.2",
34
44
  "ws": "^8.18.0"
35
45
  },
36
46
  "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"
47
+ "@pie-project/server-linux-x64": "0.5.2",
48
+ "@pie-project/server-linux-arm64": "0.5.2",
49
+ "@pie-project/server-darwin-arm64": "0.5.2",
50
+ "@pie-project/server-win32-x64": "0.5.2"
41
51
  },
42
52
  "license": "Apache-2.0",
43
53
  "repository": {
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';