@octocodeai/freellama-mcp-server 0.6.0 → 0.6.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/CONFIGURATION.md +5 -7
- package/DEVELOPING.md +18 -15
- package/LICENSE-MIT +1 -1
- package/README.md +10 -9
- package/dist/index.js +252 -39
- package/docs/ARCHITECTURE.md +43 -30
- package/docs/CLI.md +22 -16
- package/docs/CPU_GPU_ROUTING.md +11 -11
- package/docs/MODEL_SELECTION.md +46 -4
- package/docs/MONITORING.md +8 -6
- package/docs/PRODUCTION.md +31 -19
- package/docs/README.md +2 -6
- package/docs/TESTING.md +7 -7
- package/package.json +10 -9
package/CONFIGURATION.md
CHANGED
|
@@ -9,7 +9,7 @@ server-launch config (for example `.mcp.json`'s `env` block) or the launching sh
|
|
|
9
9
|
|
|
10
10
|
| Variable | Default | Affects |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| `FREELLAMA_OLLAMA_ENDPOINT` | `http://127.0.0.1:11434` | Default Ollama endpoint for `doctor`, the `models` detail/raw/library views,
|
|
12
|
+
| `FREELLAMA_OLLAMA_ENDPOINT` | `http://127.0.0.1:11434` | Default Ollama endpoint for `doctor`, the `models` detail/raw/library views, all `ollama_*` tools, and the autostarted serve's `--upstream` |
|
|
13
13
|
| `FREELLAMA_SERVE_ENDPOINT` | `http://127.0.0.1:11435` | Default `endpoint` for serve-backed tools and `delegate_research` |
|
|
14
14
|
| `FREELLAMA_MCP_DEFAULT_MODEL` | unset (routes to an installed coding model) | `delegate_research`'s default `model` |
|
|
15
15
|
| `FREELLAMA_MCP_DEFAULT_ADAPTER` | `bash` | `delegate_research`'s adapter (`octocode` to switch) |
|
|
@@ -22,11 +22,11 @@ server-launch config (for example `.mcp.json`'s `env` block) or the launching sh
|
|
|
22
22
|
| `FREELLAMA_EXTERNAL_COST_MODEL` | unset | External-model label in estimated avoided-cost telemetry; set all three cost variables or none |
|
|
23
23
|
| `FREELLAMA_EXTERNAL_COST_INPUT_USD_PER_M` | unset | External input USD per million tokens |
|
|
24
24
|
| `FREELLAMA_EXTERNAL_COST_OUTPUT_USD_PER_M` | unset | External output USD per million tokens |
|
|
25
|
-
| `FREELLAMA_CONTROL_TIMEOUT_SECONDS` | `30` | Decision-only serve calls; read by `packages/rust-core/src/
|
|
26
|
-
| `FREELLAMA_TASK_TIMEOUT_SECONDS` | `900` | Generation calls; read by `packages/rust-core/src/
|
|
25
|
+
| `FREELLAMA_CONTROL_TIMEOUT_SECONDS` | `30` | Decision-only serve calls; read by the native client (`packages/rust-core/src/serve_client.rs`) |
|
|
26
|
+
| `FREELLAMA_TASK_TIMEOUT_SECONDS` | `900` | Generation calls; read by the native client (`packages/rust-core/src/serve_client.rs`) |
|
|
27
27
|
| `FREELLAMA_AUTH_TOKEN_FILE` | unset | Bearer-token file used by the native client and research adapters for all serve routes |
|
|
28
28
|
| `FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR` | platform data directory | Prompt-free, model-specific token-estimator calibration shared across adapter processes |
|
|
29
|
-
| `FREELLAMA_MCP_AUTOSTART_SERVE` | `1` | When the default loopback serve endpoint does not answer, start `freellama serve` as a child of the MCP server (stopped with it). `0` disables |
|
|
29
|
+
| `FREELLAMA_MCP_AUTOSTART_SERVE` | `1` | When the default loopback serve endpoint does not answer, start `freellama serve` as a child of the MCP server (stopped with it): once in the background at launch, so other local clients find it, and again on a tool call if it is still down. `0` disables |
|
|
30
30
|
| `FREELLAMA_SERVE_BINARY` | checkout `target/release`/`debug`, then the platform package | Binary used for serve autostart |
|
|
31
31
|
| `FREELLAMA_AGENT_OS_SANDBOX` | `auto` | Bash adapter OS confinement: `bwrap` on Linux, `sandbox-exec` on macOS, when present. `off` keeps only the restricted shell |
|
|
32
32
|
| `FREELLAMA_AGENT_OCTOCODE_PACKAGE` | `octocode@19.1.0` | Pinned npm spec the Octocode adapter runs through `npx` |
|
|
@@ -103,6 +103,4 @@ slots or relax memory guards. Existing queued work keeps its captured cost. See
|
|
|
103
103
|
`packages/mcp/evidence/model-evidence.json`. A published package does not include that repository
|
|
104
104
|
file, so its evidence table is empty unless you set `FREELLAMA_MCP_MODEL_EVIDENCE`. An unmeasured
|
|
105
105
|
model yields a `verify` verdict, and an unreadable evidence file is reported on stderr and treated
|
|
106
|
-
as empty. Tool descriptions state the rules
|
|
107
|
-
[`skills/freellama/references/`](https://github.com/bgauryy/FreeLlama/tree/main/skills/freellama/references/) and
|
|
108
|
-
[`docs/dev/evidence/`](https://github.com/bgauryy/FreeLlama/tree/main/docs/dev/evidence/).
|
|
106
|
+
as empty. Tool descriptions state the rules.
|
package/DEVELOPING.md
CHANGED
|
@@ -13,10 +13,11 @@ yarn install
|
|
|
13
13
|
yarn build
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`yarn build` writes
|
|
16
|
+
`yarn build` writes the MCP server, `packages/mcp/dist/index.js` (shebang, executable). During
|
|
17
17
|
checkout development it loads `native/freellama.<triple>.node`. Published installs resolve the
|
|
18
18
|
matching `@octocodeai/freellama-native-<target>` optional package instead, so the base MCP package remains
|
|
19
|
-
portable.
|
|
19
|
+
portable. The research adapters compile to `dist/research/{bash-agent,octocode-agent}.js`, and the
|
|
20
|
+
repository `docs/` guides are copied into `docs/`.
|
|
20
21
|
|
|
21
22
|
Piecewise, if you are iterating on one layer:
|
|
22
23
|
|
|
@@ -24,7 +25,7 @@ Piecewise, if you are iterating on one layer:
|
|
|
24
25
|
# 1. Native addon → packages/mcp/native/freellama.<triple>.node
|
|
25
26
|
yarn build:native
|
|
26
27
|
|
|
27
|
-
# 2. This server → dist/index.js via esbuild
|
|
28
|
+
# 2. This server → dist/index.js and dist/research/*.js via esbuild, plus docs/
|
|
28
29
|
yarn workspace @octocodeai/freellama-mcp-server build
|
|
29
30
|
```
|
|
30
31
|
|
|
@@ -33,8 +34,8 @@ root produces both in a checkout. `dist/index.js` loads `native/index.js`, which
|
|
|
33
34
|
first and then a matching optional platform package. The `.node` binary is gitignored (a build
|
|
34
35
|
artifact). `native/index.js` is a small hand-written loader; `native/index.d.ts` is generated from
|
|
35
36
|
`napi.rs` by `yarn build:native`. Both are checked in; do not edit the declarations by hand —
|
|
36
|
-
change the Rust doc comments or `ts_args_type`, rebuild, and commit.
|
|
37
|
-
|
|
37
|
+
change the Rust doc comments or `ts_args_type`, rebuild, and commit. The unit test
|
|
38
|
+
`test/unit/native-dts-drift.test.ts` fails when the declarations and the `napi.rs` exports differ.
|
|
38
39
|
|
|
39
40
|
## Test
|
|
40
41
|
|
|
@@ -42,15 +43,17 @@ Vitest, in three tiers (all TypeScript, under `test/`):
|
|
|
42
43
|
|
|
43
44
|
| Tier | Command (from this package) | Checks | Needs |
|
|
44
45
|
|---|---|---|---|
|
|
45
|
-
| `test/unit/` | `yarn test` (watch: `yarn test:watch`) | pure functions straight from `src/*.ts` — no build step, this is the TDD loop | nothing,
|
|
46
|
+
| `test/unit/` | `yarn test` (watch: `yarn test:watch`) | pure functions straight from `src/*.ts` — no build step, this is the TDD loop | nothing, a few seconds |
|
|
46
47
|
| `test/integration/` | `yarn test:integration` | the built server over the real MCP protocol: tool contract, structured content, guardrails; rebuilds `dist/` itself | Ollama at `FREELLAMA_OLLAMA_ENDPOINT` (default :11434) |
|
|
47
48
|
| `test/e2e/` | `yarn test:e2e` | every tool against the live system: real routing/execution, pull → delete round trip (net-zero), a real delegated research run | serve + Ollama + models, ~40s |
|
|
48
49
|
|
|
49
|
-
The same commands work from the repository root (`yarn test` there also runs the CLI package's tests
|
|
50
|
+
The same commands work from the repository root (`yarn test` there also runs the CLI package's tests
|
|
51
|
+
and the Rust suite).
|
|
50
52
|
The integration/e2e tiers fail fast with a readable message when Ollama is down, and the lifecycle
|
|
51
53
|
test refuses to delete a model already installed on the test system.
|
|
52
54
|
|
|
53
|
-
For default-endpoint autostart,
|
|
55
|
+
For default-endpoint autostart, the server starts `freellama serve` in the background at launch and
|
|
56
|
+
logs a failure to stderr. On a tool call, a spawn failure, early exit, or readiness timeout rejects the
|
|
54
57
|
request while leaving the MCP transport available for diagnosis or retry. Autostart does not apply
|
|
55
58
|
to an explicit non-default endpoint; see `FREELLAMA_MCP_AUTOSTART_SERVE` in
|
|
56
59
|
[configuration](CONFIGURATION.md#environment-variables).
|
|
@@ -74,8 +77,8 @@ Start the serve-backed tools' dependency from the repository root:
|
|
|
74
77
|
cargo run --release -- serve
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
`freellama proxy` only forwards raw `/api/*` calls; only `serve` has the `/_freellama/v1/*` routes
|
|
81
|
+
these tools call.
|
|
79
82
|
|
|
80
83
|
## Publish
|
|
81
84
|
|
|
@@ -91,18 +94,18 @@ cd packages/mcp
|
|
|
91
94
|
npm pack --dry-run # confirm exactly what a publish would ship
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Treat `npm publish` as a public release action. Follow the
|
|
95
|
-
[release
|
|
97
|
+
Treat `npm publish` as a public release action. Follow the
|
|
98
|
+
[release package checks](https://github.com/bgauryy/FreeLlama/blob/main/docs/TESTING.md#verify-release-packages) and obtain the repository owner's release authorization first.
|
|
96
99
|
|
|
97
100
|
## Why native bindings
|
|
98
101
|
|
|
99
102
|
- **No routing reimplementation.** `run_task`, `run_task {preview:true}`, and the installed-model list are HTTP
|
|
100
|
-
calls into a running `freellama serve`, matching the CLI
|
|
103
|
+
calls into a running `freellama serve`, matching the CLI requests in `packages/cli/src/requests.rs`. `doctor` talks to Ollama
|
|
101
104
|
and the host's portable machine-discovery layer directly. `ollama_manage` / `ollama_delete` hit Ollama's HTTP API. `delegate_research`
|
|
102
105
|
is a subprocess. Routing decisions still live in exactly one place — the Rust core; see
|
|
103
106
|
[what the MCP layer computes](README.md#what-the-mcp-layer-computes).
|
|
104
|
-
- **One connection pool.** `packages/rust-core/src/
|
|
105
|
-
`OnceLock` and clones the handle per request
|
|
107
|
+
- **One connection pool.** `packages/rust-core/src/serve_client.rs` holds a single `reqwest::Client` in a
|
|
108
|
+
`OnceLock` and clones the handle per request; `napi.rs` and the CLI share it.
|
|
106
109
|
- **`unsafe_code` stays isolated.** The crate is `deny(unsafe_code)` except `napi.rs`, which
|
|
107
110
|
carries an explicit `#[allow(unsafe_code)]` for napi-derive's FFI glue.
|
|
108
111
|
- **The napi build is off by default** (`cargo build` never touches it); the addon is built with
|
package/LICENSE-MIT
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Guy Bary
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
6
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The server uses the official [TypeScript SDK](https://github.com/modelcontextpro
|
|
|
14
14
|
|
|
15
15
|
## Understand the tools
|
|
16
16
|
|
|
17
|
-
`doctor`, `models`, `run_task`, and `
|
|
17
|
+
`doctor`, `models`, `run_task`, `task_jobs`, `scope`, and `session` call the Rust core through native NAPI bindings
|
|
18
18
|
(`packages/rust-core/src/napi.rs`), with no CLI subprocess and no routing decisions in TypeScript; the
|
|
19
19
|
TypeScript layer validates request shapes and derives presentation fields (see
|
|
20
20
|
[what the MCP layer computes](#what-the-mcp-layer-computes)). `run_task` has four modes: one task,
|
|
@@ -190,7 +190,7 @@ flowchart TD
|
|
|
190
190
|
S --> G["Primary Ollama"]
|
|
191
191
|
S --> U["Optional CPU Ollama"]
|
|
192
192
|
B -->|"detail, raw; manage; delete"| G
|
|
193
|
-
B -->|"library view"| W["ollama.com library"]
|
|
193
|
+
B -->|"library view"| W["ollama.com library + Hugging Face GGUF"]
|
|
194
194
|
B -->|"delegate_research"| A["allowlisted local adapter subprocess"]
|
|
195
195
|
A --> S
|
|
196
196
|
C -->|"resources/read on demand"| D["packaged docs/*.md"]
|
|
@@ -218,9 +218,8 @@ The TypeScript server adds a small set of derived fields on top of core results:
|
|
|
218
218
|
|
|
219
219
|
## Read packaged documentation through MCP
|
|
220
220
|
|
|
221
|
-
The published MCP package includes the
|
|
222
|
-
directory
|
|
223
|
-
source of truth; the package build copies it into `docs/`.
|
|
221
|
+
The published MCP package includes the Markdown guides from the repository `docs/` directory.
|
|
222
|
+
The repository directory remains the source of truth; the package build copies it into `docs/`.
|
|
224
223
|
|
|
225
224
|
MCP clients can list resources and read `freellama://docs/index` (the repository `docs/README.md`),
|
|
226
225
|
then fetch exactly one relevant guide such as `freellama://docs/PRODUCTION` or
|
|
@@ -243,6 +242,8 @@ node packages/mcp/dist/index.js
|
|
|
243
242
|
```
|
|
244
243
|
|
|
245
244
|
The server speaks MCP over stdio — configure it in your MCP client, do not run it interactively.
|
|
245
|
+
At launch it starts `freellama serve` in the background when the default endpoint does not answer;
|
|
246
|
+
see `FREELLAMA_MCP_AUTOSTART_SERVE` in [Configuration](CONFIGURATION.md#environment-variables).
|
|
246
247
|
|
|
247
248
|
**Claude Desktop (`claude_desktop_config.json`):**
|
|
248
249
|
|
|
@@ -264,7 +265,7 @@ The server speaks MCP over stdio — configure it in your MCP client, do not run
|
|
|
264
265
|
| Tool | Needs `freellama serve`? | What it does |
|
|
265
266
|
|---|---|---|
|
|
266
267
|
| `doctor` | No (except `status`/`usage`) | Runtime/configuration diagnostic. Target another Ollama with `ollamaEndpoint` and serve with `serveEndpoint`. `summary` (default) returns endpoint, version, resident count, host/profile signals, and next step; `full` adds all non-duplicated diagnostics, including admission and categorized Ollama settings; `status` returns serve's live view (per-backend queues, current and adaptive limits, circuit breakers, loaded models, host memory, Ollama's effective settings, today's usage); `usage` returns task and token totals per day and model (`days`, default 7). The report distinguishes an observed same-user macOS process from configuration hints and never claims remote process visibility. |
|
|
267
|
-
| `models` | `installed` (default) and `resident` do; `detail`/`raw`/`library` don't | Views: `installed` (capabilities, use explanations, derived `model_type`, VRAM, context, policy_rank), `resident` (loaded now + managed GPU/CPU split), `detail` (one model, maximum context), paged `raw` (`GET /api/tags`), `library` (two-step
|
|
268
|
+
| `models` | `installed` (default) and `resident` do; `detail`/`raw`/`library` don't | Views: `installed` (capabilities, use explanations, derived `model_type`, VRAM, context, policy_rank), `resident` (loaded now + managed GPU/CPU split), `detail` (one model, maximum context), paged `raw` (`GET /api/tags`), `library` (two-step public search: omit `model` to search ollama.com families and, for a `query`, Hugging Face GGUF repos (`source` limits it to one; `limit` applies per source); pass `model:"<family>"` or `model:"hf.co/<owner>/<repo>"` for every pullable tag with its size and `fitsInMemory`). `includeLibrary:true` enriches installed/detail views with sourced public guidance; `includeReadme:true` returns bounded README text in library step 2 or enriched detail. |
|
|
268
269
|
| `run_task` | Yes | **Routes or executes** a chat/generate/embed call. `preview: true` accepts routing fields only and returns a decision without generation. Execution omits preview and supplies the payload. Embedding results withhold raw vectors by default (`returnEmbeddings: true` to get them). `tasks: [{id, independent:true, task}]` (max 64, `maxParallelism` bounds fan-out) runs a batch whose nested `task` takes the same fields; it is not a dependency scheduler. `warm: true` warms an exact installed `model` through managed admission with residency/profile controls and optional `defer:true`. |
|
|
269
270
|
| `scope` | Yes | Creates, inspects, forks, or deletes process-local message history; history inclusion requires `includeMessages:true`. |
|
|
270
271
|
| `session` | Yes | Creates, deletes, or kills model affinity without storing messages or KV; `kill` cancels the session's in-flight work. |
|
|
@@ -318,8 +319,8 @@ Memory admission, authentication, and capability requirements apply in every mod
|
|
|
318
319
|
tasks or `input` for embeddings, plus any applicable Ollama runtime controls.
|
|
319
320
|
|
|
320
321
|
Preview rejects `prompt`, `systemPrompt`, `messages`, `input`, `images`, `tools`, `keepAlive`, `format`, `think`,
|
|
321
|
-
`options`, `logprobs`, `topLogprobs`, `returnEmbeddings`, `priority`, `maxWaitSeconds`, `timeoutSeconds`,
|
|
322
|
-
`defer`, `scopeId`, and `scopeRevision`. This prevents a client from attaching
|
|
322
|
+
`options`, `logprobs`, `topLogprobs`, `returnEmbeddings`, `priority`, `maxWaitSeconds`, `timeoutSeconds`,
|
|
323
|
+
`stopOnRepetition`, `defer`, `scopeId`, and `scopeRevision`. This prevents a client from attaching
|
|
323
324
|
work to a decision-only call and mistakenly assuming it ran.
|
|
324
325
|
|
|
325
326
|
`run_task.messages` preserves Ollama message fields beyond `role` and `content`, including images,
|
|
@@ -368,7 +369,7 @@ Scripts that bypass MCP post the same fields in snake_case to `POST /_freellama/
|
|
|
368
369
|
`task`, `objective`, `model`, `session_id`, `required_capabilities`, `prompt` or `messages`,
|
|
369
370
|
`images`, `priority`, `keep_alive`, `max_wait_seconds`, `timeout_seconds`, `stop_on_repetition`,
|
|
370
371
|
and `request_options: {options, format, think, logprobs, top_logprobs}`. A batch is
|
|
371
|
-
`{tasks: [{id, independent: true, task: {...}}], max_parallelism}`; sessions are
|
|
372
|
+
`POST /_freellama/v1/task-batches` with `{tasks: [{id, independent: true, task: {...}}], max_parallelism}`; sessions are
|
|
372
373
|
`POST /_freellama/v1/sessions` with `{}`. `model` accepts a bare name (`name` resolves to
|
|
373
374
|
`name:latest`, or the one installed tag of that family); an ambiguous or unknown name is refused
|
|
374
375
|
with the installed candidates. See [Request shapes](https://github.com/bgauryy/FreeLlama/blob/main/docs/ARCHITECTURE.md#request-shapes)
|
package/dist/index.js
CHANGED
|
@@ -21629,6 +21629,12 @@ function serveBinaryCandidates() {
|
|
|
21629
21629
|
if (process.env.FREELLAMA_SERVE_BINARY) candidates.unshift(process.env.FREELLAMA_SERVE_BINARY);
|
|
21630
21630
|
return candidates;
|
|
21631
21631
|
}
|
|
21632
|
+
function startServeInBackground() {
|
|
21633
|
+
ensureServe(void 0).catch((error2) => {
|
|
21634
|
+
process.stderr.write(`[freellama serve] background start failed: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
21635
|
+
`);
|
|
21636
|
+
});
|
|
21637
|
+
}
|
|
21632
21638
|
function stopAutostartedServe() {
|
|
21633
21639
|
if (child && child.exitCode === null) child.kill("SIGTERM");
|
|
21634
21640
|
child = null;
|
|
@@ -22371,27 +22377,38 @@ function libraryTrialCandidate(tag, embedding) {
|
|
|
22371
22377
|
caution: "Measure this exact tag on representative tasks before choosing it. Include resident runners, context/KV caches and OS headroom in memory fit. Discovery does not authorize a pull."
|
|
22372
22378
|
};
|
|
22373
22379
|
}
|
|
22380
|
+
var SIZE_CHIP = /^(?:\d+x)?(?:e)?\d+(?:\.\d+)?[kmbt]$/i;
|
|
22374
22381
|
function parseModelSearch(html) {
|
|
22375
22382
|
const results = [];
|
|
22376
|
-
for (const block of html.split(/<li\
|
|
22383
|
+
for (const block of html.split(/<li\b/).slice(1)) {
|
|
22377
22384
|
const link = block.match(/href="\/(?:library\/([\w.-]+)|([\w.-]+\/[\w.-]+))"/);
|
|
22378
22385
|
const name = link?.[1] ?? link?.[2];
|
|
22379
22386
|
if (!name) continue;
|
|
22380
|
-
|
|
22381
|
-
|
|
22382
|
-
const description =
|
|
22383
|
-
const
|
|
22384
|
-
|
|
22385
|
-
);
|
|
22386
|
-
const
|
|
22387
|
-
|
|
22388
|
-
)
|
|
22387
|
+
const rawDescription = block.match(/<p\b[^>]*>([\s\S]*?)<\/p>/)?.[1];
|
|
22388
|
+
if (rawDescription === void 0) continue;
|
|
22389
|
+
const description = decodeHtml(rawDescription.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
|
|
22390
|
+
const chips = [...block.matchAll(/<span\b[^>]*class="[^"]*\b(?:rounded-md|text-(?:indigo|cyan|blue)-\d+)\b[^"]*"[^>]*>\s*([\w.-]+)\s*<\/span>/g)].map((m) => ({ text: m[1].toLowerCase(), at: m.index }));
|
|
22391
|
+
const first = chips.find((chip) => !SIZE_CHIP.test(chip.text));
|
|
22392
|
+
const groupStart = first ? block.lastIndexOf("<div", first.at) : -1;
|
|
22393
|
+
const groupEnd = groupStart >= 0 ? block.indexOf("</div>", first.at) : -1;
|
|
22394
|
+
const inGroup = (at) => groupStart < 0 || groupEnd < 0 || at > groupStart && at < groupEnd;
|
|
22395
|
+
const words = chips.filter((chip) => !SIZE_CHIP.test(chip.text) && /^[a-z]+$/.test(chip.text));
|
|
22396
|
+
const sizes = chips.filter((chip) => SIZE_CHIP.test(chip.text)).map((chip) => chip.text);
|
|
22397
|
+
const capabilities = [...new Set(words.filter((chip) => inGroup(chip.at)).map((chip) => chip.text))];
|
|
22398
|
+
const labels = [...new Set(words.filter((chip) => !inGroup(chip.at)).map((chip) => chip.text))];
|
|
22399
|
+
const legacyStat = (label) => block.match(new RegExp(`<span >([\\d.,KMB]+)<\\/span>\\s*<span class="hidden sm:flex"> ${label}`))?.[1] ?? null;
|
|
22400
|
+
const downloads = block.match(/title="([\d,]+) downloads"/)?.[1];
|
|
22401
|
+
const pullsShort = block.match(/title="[\d,]+ downloads"[\s\S]*?<span\s*>([\d.,]+[KMB]?)<\/span>/)?.[1];
|
|
22402
|
+
const tagCount = block.match(/>\s*(\d+)\s+tags?\s*</)?.[1];
|
|
22389
22403
|
results.push({
|
|
22390
22404
|
name,
|
|
22391
22405
|
description: clipText(description, 160),
|
|
22392
22406
|
capabilities,
|
|
22393
|
-
|
|
22394
|
-
|
|
22407
|
+
...labels.length ? { labels } : {},
|
|
22408
|
+
...sizes.length ? { sizes } : {},
|
|
22409
|
+
pulls: pullsShort ?? legacyStat("Pulls"),
|
|
22410
|
+
...downloads ? { downloads: Number(downloads.replace(/,/g, "")) } : {},
|
|
22411
|
+
tags: tagCount ?? legacyStat("Tag"),
|
|
22395
22412
|
cloudAvailable: capabilities.includes("cloud")
|
|
22396
22413
|
});
|
|
22397
22414
|
}
|
|
@@ -22430,6 +22447,130 @@ function parseModelTags(html, family) {
|
|
|
22430
22447
|
return { family, tags };
|
|
22431
22448
|
}
|
|
22432
22449
|
|
|
22450
|
+
// src/huggingface.ts
|
|
22451
|
+
var HUB = "https://huggingface.co";
|
|
22452
|
+
var MAX_JSON_BYTES = 4 * 1024 * 1024;
|
|
22453
|
+
function huggingFaceReference(model) {
|
|
22454
|
+
const match = model.match(/^(?:https?:\/\/)?(?:hf\.co|huggingface\.co)\/([\w.-]+)\/([\w.-]+?)(?::([\w.-]+))?$/i);
|
|
22455
|
+
if (!match || [match[1], match[2]].some((part) => part === "." || part === "..")) return null;
|
|
22456
|
+
const repo = `${match[1]}/${match[2]}`;
|
|
22457
|
+
return { repo, family: `hf.co/${repo}`, quant: match[3] ?? null, url: `${HUB}/${repo}` };
|
|
22458
|
+
}
|
|
22459
|
+
var QUANT = /^(?:UD-)?(?:I?Q\d(?:_[A-Z0-9]+)*|BF16|F16|F32|MXFP4(?:_MOE)?|TQ\d_\d)$/i;
|
|
22460
|
+
var SPLIT = /-\d{5}-of-\d{5}$/;
|
|
22461
|
+
function quantOf(path6) {
|
|
22462
|
+
const parts = path6.split("/");
|
|
22463
|
+
const stem = parts.pop().replace(/\.gguf$/i, "").replace(SPLIT, "");
|
|
22464
|
+
for (let index = 0; index <= stem.length; index++) {
|
|
22465
|
+
if (index > 0 && !/[-.]/.test(stem[index - 1])) continue;
|
|
22466
|
+
if (QUANT.test(stem.slice(index))) return stem.slice(index);
|
|
22467
|
+
}
|
|
22468
|
+
return parts.reverse().find((dir) => QUANT.test(dir)) ?? null;
|
|
22469
|
+
}
|
|
22470
|
+
var decimalSize = (bytes) => bytes >= 1e9 ? `${(bytes / 1e9).toFixed(1)}GB` : `${Math.round(bytes / 1e6)}MB`;
|
|
22471
|
+
function parseHuggingFaceFiles(tree, repo) {
|
|
22472
|
+
const builds = /* @__PURE__ */ new Map();
|
|
22473
|
+
const projectors = [];
|
|
22474
|
+
for (const entry of tree) {
|
|
22475
|
+
if (entry.type !== "file" || !entry.path || !/\.gguf$/i.test(entry.path)) continue;
|
|
22476
|
+
const file = entry.path.split("/").pop();
|
|
22477
|
+
if (/mmproj/i.test(file)) {
|
|
22478
|
+
projectors.push(entry.path);
|
|
22479
|
+
continue;
|
|
22480
|
+
}
|
|
22481
|
+
const key = entry.path.replace(/-\d{5}-of-(\d{5})\.gguf$/i, "-00001-of-$1.gguf");
|
|
22482
|
+
const bytes = entry.lfs?.size ?? entry.size ?? 0;
|
|
22483
|
+
const build = builds.get(key) ?? { path: key, file: key.split("/").pop(), quant: quantOf(entry.path), bytes: 0, files: 0 };
|
|
22484
|
+
builds.set(key, { ...build, bytes: build.bytes + bytes, files: build.files + 1 });
|
|
22485
|
+
}
|
|
22486
|
+
const perQuant = /* @__PURE__ */ new Map();
|
|
22487
|
+
for (const { quant } of builds.values()) if (quant) perQuant.set(quant.toLowerCase(), (perQuant.get(quant.toLowerCase()) ?? 0) + 1);
|
|
22488
|
+
const tags = [...builds.values()].sort((a, b) => a.bytes - b.bytes).map(({ path: path6, file, quant, bytes, files }) => ({
|
|
22489
|
+
tag: `hf.co/${repo}:${quant && perQuant.get(quant.toLowerCase()) === 1 ? quant : file}`,
|
|
22490
|
+
quant,
|
|
22491
|
+
path: path6,
|
|
22492
|
+
size: bytes ? decimalSize(bytes) : null,
|
|
22493
|
+
sizeBytes: bytes || null,
|
|
22494
|
+
files,
|
|
22495
|
+
cloud: false,
|
|
22496
|
+
// Hugging Face's Ollama guide shows single-file builds; say so rather than promise a pull.
|
|
22497
|
+
...files > 1 ? { note: `Split into ${files} files; the Hub's Ollama guide documents single-file GGUFs, so confirm the pull.` } : {}
|
|
22498
|
+
}));
|
|
22499
|
+
return { tags, projectors };
|
|
22500
|
+
}
|
|
22501
|
+
function parseHuggingFaceSearch(models) {
|
|
22502
|
+
return models.filter((model) => model.id).map((model) => ({
|
|
22503
|
+
name: `hf.co/${model.id}`,
|
|
22504
|
+
source: "huggingface",
|
|
22505
|
+
task: model.pipeline_tag ?? null,
|
|
22506
|
+
downloads: model.downloads ?? null,
|
|
22507
|
+
likes: model.likes ?? null,
|
|
22508
|
+
updated: model.lastModified ?? null
|
|
22509
|
+
}));
|
|
22510
|
+
}
|
|
22511
|
+
var HUB_EMBEDDING_TASKS = /* @__PURE__ */ new Set(["feature-extraction", "sentence-similarity"]);
|
|
22512
|
+
var HuggingFaceClient = class {
|
|
22513
|
+
constructor(options = {}) {
|
|
22514
|
+
this.options = options;
|
|
22515
|
+
}
|
|
22516
|
+
options;
|
|
22517
|
+
async json(url) {
|
|
22518
|
+
const response = await (this.options.fetch ?? fetch)(url, {
|
|
22519
|
+
method: "GET",
|
|
22520
|
+
headers: { accept: "application/json" },
|
|
22521
|
+
signal: AbortSignal.timeout(this.options.timeoutMs ?? Math.min(DEFAULT_OLLAMA_FETCH_TIMEOUT_SECONDS * 1e3, 1e4))
|
|
22522
|
+
});
|
|
22523
|
+
if (!response.ok) {
|
|
22524
|
+
await response.body?.cancel();
|
|
22525
|
+
throw new Error(response.status === 404 || response.status === 401 ? `Hugging Face has no public model at ${url} (HTTP ${response.status}).` : `Hugging Face returned HTTP ${response.status}.`);
|
|
22526
|
+
}
|
|
22527
|
+
const text = await response.text();
|
|
22528
|
+
if (text.length > MAX_JSON_BYTES) throw new Error("Hugging Face response exceeds the 4 MiB limit.");
|
|
22529
|
+
return JSON.parse(text);
|
|
22530
|
+
}
|
|
22531
|
+
stamp() {
|
|
22532
|
+
return new Date((this.options.now ?? Date.now)()).toISOString();
|
|
22533
|
+
}
|
|
22534
|
+
/** GGUF repositories matching `query`, most downloaded (or newest) first. */
|
|
22535
|
+
async search(query, { limit = 10, order = "popular", task } = {}) {
|
|
22536
|
+
const params = new URLSearchParams({
|
|
22537
|
+
search: query,
|
|
22538
|
+
filter: "gguf",
|
|
22539
|
+
sort: order === "newest" ? "lastModified" : "downloads",
|
|
22540
|
+
direction: "-1",
|
|
22541
|
+
limit: String(limit)
|
|
22542
|
+
});
|
|
22543
|
+
for (const field of ["downloads", "likes", "pipeline_tag", "lastModified"]) params.append("expand[]", field);
|
|
22544
|
+
if (task) params.set("pipeline_tag", task);
|
|
22545
|
+
const url = `${HUB}/api/models?${params}`;
|
|
22546
|
+
return { url, models: parseHuggingFaceSearch(await this.json(url)), fetchedAt: this.stamp() };
|
|
22547
|
+
}
|
|
22548
|
+
/** One repo's pullable quants, task, architecture and context length. */
|
|
22549
|
+
async lookup(model) {
|
|
22550
|
+
const ref = huggingFaceReference(model);
|
|
22551
|
+
if (!ref) throw new Error("Use hf.co/<owner>/<repo>, the name Ollama pulls from Hugging Face.");
|
|
22552
|
+
const info = await this.json(
|
|
22553
|
+
`${HUB}/api/models/${ref.repo}?expand[]=gguf&expand[]=pipeline_tag&expand[]=downloads&expand[]=likes&expand[]=lastModified&expand[]=sha&expand[]=gated`
|
|
22554
|
+
);
|
|
22555
|
+
const tree = await this.json(`${HUB}/api/models/${ref.repo}/tree/${info.sha ?? "main"}?recursive=true`);
|
|
22556
|
+
return {
|
|
22557
|
+
...ref,
|
|
22558
|
+
task: info.pipeline_tag ?? null,
|
|
22559
|
+
revision: info.sha ?? null,
|
|
22560
|
+
// "auto" or "manual": Hugging Face serves the files only after you accept the repo's terms.
|
|
22561
|
+
gated: info.gated && info.gated !== "false" ? info.gated : false,
|
|
22562
|
+
architecture: info.gguf?.architecture ?? null,
|
|
22563
|
+
contextLength: info.gguf?.context_length ?? null,
|
|
22564
|
+
parameters: info.gguf?.total ?? null,
|
|
22565
|
+
downloads: info.downloads ?? null,
|
|
22566
|
+
likes: info.likes ?? null,
|
|
22567
|
+
updated: info.lastModified ?? null,
|
|
22568
|
+
...parseHuggingFaceFiles(tree, ref.repo),
|
|
22569
|
+
fetchedAt: this.stamp()
|
|
22570
|
+
};
|
|
22571
|
+
}
|
|
22572
|
+
};
|
|
22573
|
+
|
|
22433
22574
|
// src/model-knowledge.ts
|
|
22434
22575
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
22435
22576
|
var FAILURE_TTL_MS = 60 * 1e3;
|
|
@@ -22707,12 +22848,32 @@ var modelsResultSchema = external_exports.object({
|
|
|
22707
22848
|
library: external_exports.unknown().optional()
|
|
22708
22849
|
}).passthrough();
|
|
22709
22850
|
var ollamaLibrary = new OllamaLibraryClient();
|
|
22851
|
+
var huggingFace = new HuggingFaceClient();
|
|
22710
22852
|
var HOST_MEMORY_FIT_FRACTION = 0.6;
|
|
22853
|
+
var HUB_TASKS = /* @__PURE__ */ new Map([
|
|
22854
|
+
["embedding", ["feature-extraction", "sentence-similarity"]],
|
|
22855
|
+
["vision", ["image-text-to-text"]]
|
|
22856
|
+
]);
|
|
22857
|
+
function annotateTags(tags, installed, memoryBytes, embedding) {
|
|
22858
|
+
const budget = memoryBytes ? memoryBytes * HOST_MEMORY_FIT_FRACTION : null;
|
|
22859
|
+
const local = installed ? new Set([...installed].map((name) => name.toLowerCase())) : null;
|
|
22860
|
+
const annotated = tags.map((t) => ({
|
|
22861
|
+
...t,
|
|
22862
|
+
installed: local ? local.has(t.tag.toLowerCase()) : null,
|
|
22863
|
+
fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
|
|
22864
|
+
fitScope: "host_memory_budget_only"
|
|
22865
|
+
}));
|
|
22866
|
+
const sized = annotated.filter((t) => t.fitsInMemory === true && !t.cloud && t.sizeBytes);
|
|
22867
|
+
const best = budget ? sized.sort((a, b) => embedding ? (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0) : (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0))[0] : void 0;
|
|
22868
|
+
return { annotated, budget, best };
|
|
22869
|
+
}
|
|
22870
|
+
var NO_PROFILE = "No machine profile (freellama serve unreachable), so memory fit could not be checked and no tag is recommended. Start serve, or read the sizes yourself.";
|
|
22711
22871
|
async function libraryLookup({
|
|
22712
22872
|
model,
|
|
22713
22873
|
query,
|
|
22714
22874
|
capabilities,
|
|
22715
22875
|
order,
|
|
22876
|
+
source,
|
|
22716
22877
|
limit,
|
|
22717
22878
|
cursor,
|
|
22718
22879
|
includeReadme,
|
|
@@ -22740,6 +22901,40 @@ async function libraryLookup({
|
|
|
22740
22901
|
return errorResult(new Error("Library step 1 has no `cursor`; it pages step-2 tags. Use `limit` for search, or pass `model`."));
|
|
22741
22902
|
}
|
|
22742
22903
|
try {
|
|
22904
|
+
if (model && huggingFaceReference(model)) {
|
|
22905
|
+
const hub2 = await huggingFace.lookup(model);
|
|
22906
|
+
const { installed: installed2, installedUnavailable: installedUnavailable2, memoryBytes } = await localState();
|
|
22907
|
+
const embedding = HUB_EMBEDDING_TASKS.has(hub2.task ?? "");
|
|
22908
|
+
const { annotated, budget, best } = annotateTags(hub2.tags, installed2, memoryBytes, embedding);
|
|
22909
|
+
const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
|
|
22910
|
+
return structuredResult({
|
|
22911
|
+
family: hub2.family,
|
|
22912
|
+
source: "huggingface",
|
|
22913
|
+
sourcePage: hub2.url,
|
|
22914
|
+
library: {
|
|
22915
|
+
status: hub2.tags.length ? "available" : "no_gguf",
|
|
22916
|
+
fetchedAt: hub2.fetchedAt,
|
|
22917
|
+
task: hub2.task,
|
|
22918
|
+
revision: hub2.revision,
|
|
22919
|
+
...hub2.gated ? { gated: hub2.gated, gatedNote: "Accept the repo's terms on Hugging Face first; its Ollama guide documents private pulls through your Ollama SSH key added to your Hugging Face account." } : {},
|
|
22920
|
+
architecture: hub2.architecture,
|
|
22921
|
+
contextLength: hub2.contextLength,
|
|
22922
|
+
parameters: hub2.parameters,
|
|
22923
|
+
downloads: hub2.downloads,
|
|
22924
|
+
likes: hub2.likes,
|
|
22925
|
+
updated: hub2.updated,
|
|
22926
|
+
...hub2.projectors.length ? { visionProjectors: hub2.projectors } : {}
|
|
22927
|
+
},
|
|
22928
|
+
...installedUnavailable2 ? { installedUnavailable: installedUnavailable2 } : {},
|
|
22929
|
+
machineMemoryBytes: memoryBytes,
|
|
22930
|
+
fitBudgetBytes: budget,
|
|
22931
|
+
tags: tagPage.items,
|
|
22932
|
+
page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
|
|
22933
|
+
pull: hub2.tags.length ? `ollama_manage {action:"pull", model:"<tag>"} after approval; Ollama picks Q4_K_M for a bare ${hub2.family} when the repo has it.` : "This repository has no GGUF files Ollama can pull.",
|
|
22934
|
+
recommendationUnavailable: budget ? void 0 : NO_PROFILE,
|
|
22935
|
+
recommendation: best ? libraryTrialCandidate(best.tag, embedding) : null
|
|
22936
|
+
});
|
|
22937
|
+
}
|
|
22743
22938
|
if (model) {
|
|
22744
22939
|
const ref = libraryReference(model);
|
|
22745
22940
|
if (!ref) throw new Error("Use an exact Ollama family or namespace/model name, not a URL or path.");
|
|
@@ -22755,20 +22950,12 @@ async function libraryLookup({
|
|
|
22755
22950
|
});
|
|
22756
22951
|
const tags = library.tags;
|
|
22757
22952
|
const { installed: installed2, installedUnavailable: installedUnavailable2, memoryBytes } = await localState();
|
|
22758
|
-
const budget = memoryBytes ? memoryBytes * HOST_MEMORY_FIT_FRACTION : null;
|
|
22759
|
-
const annotated = tags.map((t) => ({
|
|
22760
|
-
...t,
|
|
22761
|
-
installed: installed2 ? installed2.has(t.tag) : null,
|
|
22762
|
-
fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
|
|
22763
|
-
fitScope: "host_memory_budget_only"
|
|
22764
|
-
}));
|
|
22765
|
-
const runnable = annotated.filter((t) => t.fitsInMemory === true && !t.cloud);
|
|
22766
22953
|
const isEmbeddingFamily = library.card.capabilities.includes("embedding");
|
|
22767
|
-
const
|
|
22768
|
-
const best = budget ? isEmbeddingFamily ? sized.sort((a, b) => (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0))[0] : sized.sort((a, b) => (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0))[0] : void 0;
|
|
22954
|
+
const { annotated, budget, best } = annotateTags(tags, installed2, memoryBytes, isEmbeddingFamily);
|
|
22769
22955
|
const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
|
|
22770
22956
|
return structuredResult({
|
|
22771
22957
|
family,
|
|
22958
|
+
source: "ollama",
|
|
22772
22959
|
sourcePage: ref.url,
|
|
22773
22960
|
library: {
|
|
22774
22961
|
status: library.status,
|
|
@@ -22784,7 +22971,7 @@ async function libraryLookup({
|
|
|
22784
22971
|
fitBudgetBytes: budget,
|
|
22785
22972
|
tags: tagPage.items,
|
|
22786
22973
|
page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
|
|
22787
|
-
recommendationUnavailable: budget ? void 0 :
|
|
22974
|
+
recommendationUnavailable: budget ? void 0 : NO_PROFILE,
|
|
22788
22975
|
recommendation: best ? libraryTrialCandidate(best.tag, isEmbeddingFamily) : null
|
|
22789
22976
|
});
|
|
22790
22977
|
}
|
|
@@ -22792,20 +22979,44 @@ async function libraryLookup({
|
|
|
22792
22979
|
for (const c of capabilities ?? []) params.append("c", c);
|
|
22793
22980
|
if (query) params.set("q", query);
|
|
22794
22981
|
if (order === "newest") params.set("o", "newest");
|
|
22795
|
-
const
|
|
22796
|
-
const
|
|
22797
|
-
|
|
22982
|
+
const hubTasks = (capabilities ?? []).map((c) => HUB_TASKS.get(c));
|
|
22983
|
+
const hubSkipped = source === "ollama" ? "source is ollama" : !query ? "the Hub is searched only with a query" : hubTasks.length > 1 || hubTasks.some((tasks) => !tasks) ? `the Hub has no filter for ${(capabilities ?? []).filter((c) => !HUB_TASKS.has(c)).join(", ") || "several capabilities at once"}` : null;
|
|
22984
|
+
if (source === "huggingface" && hubSkipped) throw new Error(`source "huggingface" cannot be searched: ${hubSkipped}.`);
|
|
22985
|
+
const searchHub = async () => {
|
|
22986
|
+
const results = await Promise.all((hubTasks[0] ?? [void 0]).map((task) => huggingFace.search(query, { limit: limit ?? 10, order, task })));
|
|
22987
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22988
|
+
const models = results.flatMap((r) => r.models).filter((m) => !seen.has(m.name) && seen.add(m.name)).sort((a, b) => order === "newest" ? String(b.updated).localeCompare(String(a.updated)) : (b.downloads ?? 0) - (a.downloads ?? 0)).slice(0, limit ?? 10);
|
|
22989
|
+
return { url: results[0].url, models, fetchedAt: results[0].fetchedAt };
|
|
22990
|
+
};
|
|
22991
|
+
const [ollamaSearch, hubSearch] = await Promise.allSettled([
|
|
22992
|
+
source === "huggingface" ? Promise.resolve(null) : ollamaLibrary.search(params),
|
|
22993
|
+
hubSkipped ? Promise.resolve(null) : searchHub()
|
|
22994
|
+
]);
|
|
22995
|
+
const tried = [source === "huggingface" ? null : ollamaSearch, hubSkipped ? null : hubSearch].filter(Boolean);
|
|
22996
|
+
if (tried.every((result) => result.status === "rejected")) {
|
|
22997
|
+
throw new Error(tried.map((result) => result.reason instanceof Error ? result.reason.message : String(result.reason)).join("; "));
|
|
22998
|
+
}
|
|
22999
|
+
const ollama = ollamaSearch.status === "fulfilled" ? ollamaSearch.value : null;
|
|
23000
|
+
const hub = hubSearch.status === "fulfilled" ? hubSearch.value : null;
|
|
23001
|
+
const ollamaRows = (ollama?.models ?? []).slice(0, limit ?? 10).map((m) => ({ ...m, source: "ollama" }));
|
|
23002
|
+
const hubRows = hub?.models ?? [];
|
|
22798
23003
|
const { installed, installedUnavailable } = await localState();
|
|
22799
|
-
const
|
|
23004
|
+
const local = installed ? [...installed].map((n) => n.toLowerCase()) : null;
|
|
23005
|
+
const isInstalled = (name) => local ? local.some((n) => n.split(":")[0] === name.toLowerCase()) : null;
|
|
23006
|
+
const reason = (result) => result.status === "rejected" ? result.reason instanceof Error ? result.reason.message : String(result.reason) : void 0;
|
|
22800
23007
|
return structuredResult({
|
|
22801
|
-
query: url,
|
|
22802
|
-
|
|
22803
|
-
|
|
23008
|
+
query: ollama?.url ?? hub?.url ?? null,
|
|
23009
|
+
...ollama && hub ? { hubQuery: hub.url } : {},
|
|
23010
|
+
fetchedAt: ollama?.fetchedAt ?? hub?.fetchedAt ?? null,
|
|
23011
|
+
...ollama && ollamaRows.length === 0 ? { searchUnavailable: "No ollama.com result cards were recognized. The query may have no matches or Ollama's markup may have changed; inspect the source URL." } : {},
|
|
23012
|
+
...reason(ollamaSearch) ? { ollamaUnavailable: reason(ollamaSearch) } : {},
|
|
23013
|
+
...reason(hubSearch) ? { huggingFaceUnavailable: reason(hubSearch) } : {},
|
|
23014
|
+
...hubSkipped && source !== "ollama" ? { huggingFaceSkipped: hubSkipped } : {},
|
|
22804
23015
|
order: order ?? "popular",
|
|
22805
|
-
count:
|
|
23016
|
+
count: ollamaRows.length + hubRows.length,
|
|
22806
23017
|
...installedUnavailable ? { installedUnavailable } : {},
|
|
22807
|
-
nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" to get tags, sizes and memory fit.',
|
|
22808
|
-
models:
|
|
23018
|
+
nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" (an Ollama family or hf.co/<owner>/<repo>) to get tags, sizes and memory fit.',
|
|
23019
|
+
models: [...ollamaRows, ...hubRows].map((m) => ({ ...m, installed: isInstalled(m.name) }))
|
|
22809
23020
|
});
|
|
22810
23021
|
} catch (error2) {
|
|
22811
23022
|
return errorResult(error2);
|
|
@@ -22815,7 +23026,7 @@ function registerModelsTool(server2) {
|
|
|
22815
23026
|
server2.registerTool(
|
|
22816
23027
|
"models",
|
|
22817
23028
|
{
|
|
22818
|
-
description: "Use when: listing installed or loaded models with capabilities, policy rank and memory fit, inspecting one model, or searching ollama.com (view:library). Do not use when: running work (run_task) or pulling (ollama_manage). Returns: models; library lists families first, then one family's tags.",
|
|
23029
|
+
description: "Use when: listing installed or loaded models with capabilities, policy rank and memory fit, inspecting one model, or searching ollama.com and Hugging Face GGUF repos (view:library). Do not use when: running work (run_task) or pulling (ollama_manage). Returns: models; library lists families first, then one family's tags.",
|
|
22819
23030
|
inputSchema: external_exports.object({
|
|
22820
23031
|
view: external_exports.enum(["installed", "resident", "detail", "raw", "library"]).optional(),
|
|
22821
23032
|
model: external_exports.string().min(1).optional(),
|
|
@@ -22823,8 +23034,9 @@ function registerModelsTool(server2) {
|
|
|
22823
23034
|
includeLibrary: external_exports.boolean().optional(),
|
|
22824
23035
|
includeReadme: external_exports.boolean().optional(),
|
|
22825
23036
|
query: external_exports.string().min(1).optional(),
|
|
22826
|
-
capabilities: external_exports.array(external_exports.
|
|
23037
|
+
capabilities: external_exports.array(external_exports.string().regex(/^[a-z][a-z-]*$/).describe("ollama.com capability word, e.g. vision, tools, thinking, embedding, audio, decision, cloud")).min(1).max(8).optional(),
|
|
22827
23038
|
order: external_exports.enum(["popular", "newest"]).optional(),
|
|
23039
|
+
source: external_exports.enum(["ollama", "huggingface"]).optional(),
|
|
22828
23040
|
limit: external_exports.number().int().positive().max(50).optional(),
|
|
22829
23041
|
cursor: external_exports.string().min(1).optional(),
|
|
22830
23042
|
endpoint: endpointParam,
|
|
@@ -22833,7 +23045,7 @@ function registerModelsTool(server2) {
|
|
|
22833
23045
|
outputSchema: modelsResultSchema,
|
|
22834
23046
|
annotations: { readOnlyHint: true }
|
|
22835
23047
|
},
|
|
22836
|
-
async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, limit, cursor, endpoint, ollamaEndpoint }) => {
|
|
23048
|
+
async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, source, limit, cursor, endpoint, ollamaEndpoint }) => {
|
|
22837
23049
|
try {
|
|
22838
23050
|
const selectedView = view ?? "installed";
|
|
22839
23051
|
if (includeLibrary !== void 0 && selectedView !== "installed" && selectedView !== "detail") {
|
|
@@ -22842,7 +23054,7 @@ function registerModelsTool(server2) {
|
|
|
22842
23054
|
if (includeReadme !== void 0 && !(selectedView === "library" && model || selectedView === "detail" && includeLibrary === true)) {
|
|
22843
23055
|
return errorResult(new Error('`includeReadme` needs library step 2, or view "detail" with `includeLibrary:true`.'));
|
|
22844
23056
|
}
|
|
22845
|
-
if (selectedView !== "library" && [query, capabilities, order].some((value) => value !== void 0)) {
|
|
23057
|
+
if (selectedView !== "library" && [query, capabilities, order, source].some((value) => value !== void 0)) {
|
|
22846
23058
|
return errorResult(new Error(`view "${selectedView}" does not accept library search fields.`));
|
|
22847
23059
|
}
|
|
22848
23060
|
if (selectedView !== "library" && selectedView !== "raw" && [limit, cursor].some((value) => value !== void 0)) {
|
|
@@ -22854,7 +23066,7 @@ function registerModelsTool(server2) {
|
|
|
22854
23066
|
if (selectedView !== "detail" && selectedView !== "library" && model !== void 0) {
|
|
22855
23067
|
return errorResult(new Error('`model` is valid only for views "detail" and "library".'));
|
|
22856
23068
|
}
|
|
22857
|
-
if (selectedView === "library" && model && [query, capabilities, order].some((value) => value !== void 0)) {
|
|
23069
|
+
if (selectedView === "library" && model && [query, capabilities, order, source].some((value) => value !== void 0)) {
|
|
22858
23070
|
return errorResult(
|
|
22859
23071
|
new Error("Library step 2 accepts only `model` plus endpoint overrides; omit step-1 search fields.")
|
|
22860
23072
|
);
|
|
@@ -22917,7 +23129,7 @@ function registerModelsTool(server2) {
|
|
|
22917
23129
|
});
|
|
22918
23130
|
}
|
|
22919
23131
|
case "library":
|
|
22920
|
-
return await libraryLookup({ model, query, capabilities, order, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
|
|
23132
|
+
return await libraryLookup({ model, query, capabilities, order, source, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
|
|
22921
23133
|
default: {
|
|
22922
23134
|
const result = parsedResult(await listModels2(endpoint));
|
|
22923
23135
|
if (!isStructuredSuccess(result)) return result;
|
|
@@ -23942,3 +24154,4 @@ registerOllamaLifecycleTools(server);
|
|
|
23942
24154
|
registerDelegateResearchTool(server);
|
|
23943
24155
|
var transport = withoutSchemaDialect(new StdioServerTransport());
|
|
23944
24156
|
await server.connect(transport);
|
|
24157
|
+
startServeInBackground();
|