@gjsify/local-ai-servers 0.0.0-stage → 0.64.19
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 +43 -2
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/discover.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/types/discover.d.ts +76 -0
- package/lib/types/discover.spec.d.ts +2 -0
- package/lib/types/index.d.ts +1 -0
- package/package.json +66 -5
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gjsify/local-ai-servers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Find the local inference servers that are running (Ollama, LM Studio, llama.cpp's `llama-server`) and the models they have loaded, so an app can reuse a loaded model through the server's OpenAI-compatible endpoint instead of loading a second copy into the same GPU.
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project. **Tier 3 (experimental, ADR 0003)**: tested against mocked server responses, not yet against live servers. Design: ADR 0136.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gjsify install @gjsify/local-ai-servers
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { discoverLocalServers, loadedModels } from '@gjsify/local-ai-servers';
|
|
17
|
+
|
|
18
|
+
const servers = await discoverLocalServers();
|
|
19
|
+
const [first] = loadedModels(servers);
|
|
20
|
+
if (first) {
|
|
21
|
+
// Hand the endpoint to your own OpenAI-compatible client.
|
|
22
|
+
client.connect({ baseUrl: first.server.openAiBaseUrl, model: first.model.id });
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What it asks, and what it never does
|
|
27
|
+
|
|
28
|
+
- Only fixed loopback addresses: Ollama on `127.0.0.1:11434` (or `OLLAMA_HOST`), LM Studio on `127.0.0.1:1234`, `llama-server` on `127.0.0.1:8080`. Never a port scan, never the LAN.
|
|
29
|
+
- Only listing endpoints (`/api/tags`, `/api/ps`, `/api/v0/models`, `/v1/models`), each with a 500 ms timeout. Never inference.
|
|
30
|
+
- A response with the wrong shape means "not that server". A program that answers the OpenAI listing without identifying itself is reported as `openai-compatible`.
|
|
31
|
+
- Nothing runs until the app calls it.
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
- `fetch`: injectable; defaults to the runtime's global `fetch`.
|
|
36
|
+
- `env`: where `OLLAMA_HOST` is read from; defaults to `process.env`.
|
|
37
|
+
- `timeoutMs`: per request.
|
|
38
|
+
- `probes`: replace the defaults, for example `[ollamaProbe('http://127.0.0.1:11500')]`.
|
|
39
|
+
|
|
40
|
+
Each model carries `loaded` (`true`, `false`, or `undefined` when the server does not say) and, for Ollama, `sizeBytes` and `vramBytes`.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";const e=500,isRecord=e=>typeof e==`object`&&!!e&&!Array.isArray(e),optionalBytes=e=>typeof e==`number`&&Number.isFinite(e)&&e>=0?e:void 0;function openAiModels(e){if(!isRecord(e)||!Array.isArray(e.data))return;let t=e.data.filter(isRecord);if(!(t.length!==e.data.length||!t.every(e=>typeof e.id==`string`)))return t}function ollamaOrigin(e={}){let t=(e.OLLAMA_HOST??``).trim();if(t===``)return`http://127.0.0.1:11434`;let n=`http`,r=t,i=`11434`,a=t.indexOf(`://`);a>=0&&(n=t.slice(0,a).toLowerCase(),r=t.slice(a+3),n===`http`&&(i=`80`),n===`https`&&(i=`443`)),r=r.split(`/`)[0];let o=r,s=i,c=/^\[([^\]]+)\](?::(\d+))?$/.exec(r);return c?(o=c[1],s=c[2]??i):/^[^:]*:\d*$/.test(r)&&([o,s]=r.split(`:`),s===``&&(s=i)),(o===``||o===`0.0.0.0`)&&(o=`127.0.0.1`),o===`::`&&(o=`::1`),`${n}://${o.includes(`:`)?`[${o}]`:o}:${s}`}function ollamaProbe(e){return{kind:`ollama`,origin:e,async detect(t){let n=await t(`/api/tags`);if(!isRecord(n)||!Array.isArray(n.models))return;let r=n.models.filter(isRecord).filter(e=>typeof e.name==`string`),i=await t(`/api/ps`).catch(()=>void 0),a=isRecord(i)&&Array.isArray(i.models)?new Map(i.models.filter(isRecord).filter(e=>typeof e.name==`string`).map(e=>[e.name,e])):void 0,o=r.map(e=>{let t=a?.get(e.name);return{id:e.name,loaded:a===void 0?void 0:t!==void 0,sizeBytes:optionalBytes(e.size),vramBytes:t===void 0?void 0:optionalBytes(t.size_vram)}});return{kind:`ollama`,origin:e,openAiBaseUrl:`${e}/v1`,models:o}}}}function lmStudioProbe(e=`http://127.0.0.1:1234`){return{kind:`lm-studio`,origin:e,async detect(t){let n=openAiModels(await t(`/api/v0/models`).catch(()=>void 0));if(n?.every(e=>typeof e.state==`string`)){let t=n.map(e=>({id:e.id,loaded:e.state===`loaded`}));return{kind:`lm-studio`,origin:e,openAiBaseUrl:`${e}/v1`,models:t}}let r=openAiModels(await t(`/v1/models`));if(r===void 0)return;let i=r.map(e=>({id:e.id,loaded:void 0}));return{kind:`openai-compatible`,origin:e,openAiBaseUrl:`${e}/v1`,models:i}}}}function llamaCppProbe(e=`http://127.0.0.1:8080`){return{kind:`llama-cpp`,origin:e,async detect(t){let n=openAiModels(await t(`/v1/models`));if(n===void 0)return;let r=n.length>0&&n.every(e=>e.owned_by===`llamacpp`),i=n.map(e=>{let t=isRecord(e.status)?e.status.value:void 0;return{id:e.id,loaded:typeof t==`string`?t===`loaded`:r?!0:void 0}});return{kind:r?`llama-cpp`:`openai-compatible`,origin:e,openAiBaseUrl:`${e}/v1`,models:i}}}}function defaultProbes(e={}){return[ollamaProbe(ollamaOrigin(e)),lmStudioProbe(),llamaCppProbe()]}function jsonGetter(e,t,n){return async r=>{let i=new AbortController,a=setTimeout(()=>i.abort(),n);try{let n=await t(`${e}${r}`,{signal:i.signal});if(!n.ok)throw Error(`${e}${r}: HTTP ${n.status}`);return await n.json()}finally{clearTimeout(a)}}}async function discoverLocalServers(e={}){let t=e.fetch??((e,t)=>globalThis.fetch(e,t)),n=e.timeoutMs??500,r=e.probes??defaultProbes(e.env??globalThis.process?.env??{});return(await Promise.all(r.map(e=>e.detect(jsonGetter(e.origin,t,n)).catch(()=>void 0)))).filter(e=>e!==void 0)}function loadedModels(e){return e.flatMap(e=>e.models.filter(e=>e.loaded===!0).map(t=>({server:e,model:t})))}export{e as DEFAULT_TIMEOUT_MS,defaultProbes,discoverLocalServers,llamaCppProbe,lmStudioProbe,loadedModels,ollamaOrigin,ollamaProbe};
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DEFAULT_TIMEOUT_MS as e,defaultProbes as t,discoverLocalServers as n,llamaCppProbe as r,lmStudioProbe as i,loadedModels as a,ollamaOrigin as o,ollamaProbe as s}from"./discover.js";export{e as DEFAULT_TIMEOUT_MS,t as defaultProbes,n as discoverLocalServers,r as llamaCppProbe,i as lmStudioProbe,a as loadedModels,o as ollamaOrigin,s as ollamaProbe};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which local inference servers are running, and which models they have loaded (ADR 0136).
|
|
3
|
+
*
|
|
4
|
+
* An app that finds a model already loaded in Ollama, LM Studio or llama.cpp's server can use it
|
|
5
|
+
* through that server's OpenAI-compatible endpoint instead of loading a second copy into the
|
|
6
|
+
* same GPU. This module only FINDS them: it reads each server's listing endpoints and returns
|
|
7
|
+
* where to send requests. Inference goes through the app's own OpenAI-compatible client
|
|
8
|
+
* (ADR 0132 decision 6), never through this package.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT ASKS, AND WHAT IT NEVER DOES. Only the fixed loopback addresses these servers listen on
|
|
11
|
+
* by default, plus `OLLAMA_HOST` when the user set it — never a port scan, never the LAN. Each
|
|
12
|
+
* request has a short timeout, and a response that does not have the expected shape is "not that
|
|
13
|
+
* server", so another program on port 8080 is not mistaken for one. Nothing here runs unless the
|
|
14
|
+
* app calls it.
|
|
15
|
+
*/
|
|
16
|
+
export type LocalServerKind = 'ollama' | 'lm-studio' | 'llama-cpp' | 'openai-compatible';
|
|
17
|
+
export interface LocalModel {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** In memory now; `undefined` when the server does not say. */
|
|
20
|
+
readonly loaded: boolean | undefined;
|
|
21
|
+
/** On-disk size, where the server reports it. */
|
|
22
|
+
readonly sizeBytes?: number;
|
|
23
|
+
/** GPU memory the loaded model holds (Ollama's `size_vram`). */
|
|
24
|
+
readonly vramBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface LocalServer {
|
|
27
|
+
readonly kind: LocalServerKind;
|
|
28
|
+
/** `http://127.0.0.1:11434` — what was asked. */
|
|
29
|
+
readonly origin: string;
|
|
30
|
+
/** Base URL for an OpenAI-compatible client: `<origin>/v1`. */
|
|
31
|
+
readonly openAiBaseUrl: string;
|
|
32
|
+
readonly models: readonly LocalModel[];
|
|
33
|
+
}
|
|
34
|
+
export type FetchLike = (url: string, init: {
|
|
35
|
+
signal: AbortSignal;
|
|
36
|
+
}) => Promise<Response>;
|
|
37
|
+
/** One kind of server at one address. */
|
|
38
|
+
export interface ServerProbe {
|
|
39
|
+
readonly kind: LocalServerKind;
|
|
40
|
+
readonly origin: string;
|
|
41
|
+
detect(getJson: (path: string) => Promise<unknown>): Promise<LocalServer | undefined>;
|
|
42
|
+
}
|
|
43
|
+
export interface DiscoverOptions {
|
|
44
|
+
readonly fetch?: FetchLike;
|
|
45
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
46
|
+
/** Per request; a local server answers in milliseconds, a missing one should not cost more. */
|
|
47
|
+
readonly timeoutMs?: number;
|
|
48
|
+
/** Defaults to {@link defaultProbes} for `env`. */
|
|
49
|
+
readonly probes?: readonly ServerProbe[];
|
|
50
|
+
}
|
|
51
|
+
export declare const DEFAULT_TIMEOUT_MS = 500;
|
|
52
|
+
/**
|
|
53
|
+
* The address Ollama's own CLI would talk to, read from `OLLAMA_HOST` the way Ollama reads it:
|
|
54
|
+
* scheme optional (`http`), port 11434 unless a scheme is given without one. A server bound to
|
|
55
|
+
* every interface (`0.0.0.0`, `::`) is reached on loopback.
|
|
56
|
+
*/
|
|
57
|
+
export declare function ollamaOrigin(env?: Readonly<Record<string, string | undefined>>): string;
|
|
58
|
+
export declare function ollamaProbe(origin: string): ServerProbe;
|
|
59
|
+
export declare function lmStudioProbe(origin?: string): ServerProbe;
|
|
60
|
+
/**
|
|
61
|
+
* llama.cpp's `llama-server`, recognised by `owned_by: "llamacpp"` in its listing. Anything else
|
|
62
|
+
* answering the OpenAI listing on the same port is reported as `openai-compatible` — usable the
|
|
63
|
+
* same way, without the claim about which program it is.
|
|
64
|
+
*/
|
|
65
|
+
export declare function llamaCppProbe(origin?: string): ServerProbe;
|
|
66
|
+
export declare function defaultProbes(env?: Readonly<Record<string, string | undefined>>): ServerProbe[];
|
|
67
|
+
/**
|
|
68
|
+
* Ask every probe at once and return the servers that answered as themselves, in probe order.
|
|
69
|
+
* A server that is down, slow or something else is left out, never an error.
|
|
70
|
+
*/
|
|
71
|
+
export declare function discoverLocalServers(options?: DiscoverOptions): Promise<LocalServer[]>;
|
|
72
|
+
/** Every loaded model across `servers`, with the server to reach it through. */
|
|
73
|
+
export declare function loadedModels(servers: readonly LocalServer[]): {
|
|
74
|
+
server: LocalServer;
|
|
75
|
+
model: LocalModel;
|
|
76
|
+
}[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './discover.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@gjsify/local-ai-servers",
|
|
3
|
+
"version": "0.64.19",
|
|
4
|
+
"description": "Find local inference servers (Ollama, LM Studio, llama.cpp) and the models they have loaded, so an app can reuse one through its OpenAI-compatible endpoint instead of loading a second copy. Loopback only, short timeouts, never inference.",
|
|
5
|
+
"module": "lib/esm/index.js",
|
|
6
|
+
"types": "lib/types/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/types/index.d.ts",
|
|
12
|
+
"default": "./lib/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"!lib/types/**/*.spec.d.ts"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clear": "gjsify clear lib tmp tsconfig.tsbuildinfo test.gjs.mjs test.node.mjs",
|
|
21
|
+
"check": "gjsify tsc --noEmit",
|
|
22
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
23
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
24
|
+
"build:types": "gjsify tsc",
|
|
25
|
+
"build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
|
|
26
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
27
|
+
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
28
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
|
|
29
|
+
"test:gjs": "gjsify run test.gjs.mjs",
|
|
30
|
+
"test:node": "node test.node.mjs"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"ollama",
|
|
34
|
+
"lm-studio",
|
|
35
|
+
"llama.cpp",
|
|
36
|
+
"openai-compatible",
|
|
37
|
+
"local-ai",
|
|
38
|
+
"discovery"
|
|
39
|
+
],
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@gjsify/cli": "^0.64.19",
|
|
42
|
+
"@gjsify/fetch": "^0.64.19",
|
|
43
|
+
"@gjsify/unit": "^0.64.19",
|
|
44
|
+
"@types/node": "^25.9.2",
|
|
45
|
+
"typescript": "^6.0.3"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
51
|
+
"directory": "packages/gjs/local-ai-servers"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/gjs/local-ai-servers#readme",
|
|
57
|
+
"gjsify": {
|
|
58
|
+
"tier": 3,
|
|
59
|
+
"runtimes": {
|
|
60
|
+
"gjs": "polyfill",
|
|
61
|
+
"node": "none",
|
|
62
|
+
"browser": "none",
|
|
63
|
+
"nativescript": "none"
|
|
64
|
+
},
|
|
65
|
+
"headless": true
|
|
66
|
+
}
|
|
67
|
+
}
|