smallm 0.5.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.
Files changed (64) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +244 -0
  3. package/dist/benchmark.d.ts +10 -0
  4. package/dist/benchmark.d.ts.map +1 -0
  5. package/dist/benchmark.js +35 -0
  6. package/dist/benchmark.js.map +1 -0
  7. package/dist/cache-file.d.ts +40 -0
  8. package/dist/cache-file.d.ts.map +1 -0
  9. package/dist/cache-file.js +163 -0
  10. package/dist/cache-file.js.map +1 -0
  11. package/dist/cache.d.ts +23 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +58 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/cli.d.ts +32 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +256 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/data/benchmarks.json +61 -0
  20. package/dist/embeddings.d.ts +50 -0
  21. package/dist/embeddings.d.ts.map +1 -0
  22. package/dist/embeddings.js +118 -0
  23. package/dist/embeddings.js.map +1 -0
  24. package/dist/errors.d.ts +18 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +42 -0
  27. package/dist/errors.js.map +1 -0
  28. package/dist/filters.d.ts +11 -0
  29. package/dist/filters.d.ts.map +1 -0
  30. package/dist/filters.js +66 -0
  31. package/dist/filters.js.map +1 -0
  32. package/dist/index.d.ts +52 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +110 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/params.d.ts +21 -0
  37. package/dist/params.d.ts.map +1 -0
  38. package/dist/params.js +50 -0
  39. package/dist/params.js.map +1 -0
  40. package/dist/query.d.ts +23 -0
  41. package/dist/query.d.ts.map +1 -0
  42. package/dist/query.js +81 -0
  43. package/dist/query.js.map +1 -0
  44. package/dist/registry/huggingface.d.ts +24 -0
  45. package/dist/registry/huggingface.d.ts.map +1 -0
  46. package/dist/registry/huggingface.js +97 -0
  47. package/dist/registry/huggingface.js.map +1 -0
  48. package/dist/registry/ollama.d.ts +3 -0
  49. package/dist/registry/ollama.d.ts.map +1 -0
  50. package/dist/registry/ollama.js +78 -0
  51. package/dist/registry/ollama.js.map +1 -0
  52. package/dist/registry/provider.d.ts +12 -0
  53. package/dist/registry/provider.d.ts.map +1 -0
  54. package/dist/registry/provider.js +3 -0
  55. package/dist/registry/provider.js.map +1 -0
  56. package/dist/scorer.d.ts +45 -0
  57. package/dist/scorer.d.ts.map +1 -0
  58. package/dist/scorer.js +161 -0
  59. package/dist/scorer.js.map +1 -0
  60. package/dist/types.d.ts +147 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +8 -0
  63. package/dist/types.js.map +1 -0
  64. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # smallm
2
+
3
+ Find the right small language model (SLM) for a task, based on given context.
4
+
5
+ `smallm` does a **live lookup against the HuggingFace public API** and runs a **rule-based scoring engine** (task match, size fit, context window, domain tag) to rank candidates — no embeddings, no local registry, no config files.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install smallm
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { findModels } from "smallm";
17
+
18
+ const results = await findModels({
19
+ task: "chat", // "summarize" | "classify" | "extract" | "chat" | "code" | "translate" | string
20
+ contextLength: 8192, // required — hard filter, in tokens
21
+ maxParamsB: 7, // optional — hard filter, "under 7B"
22
+ domain: "medical", // optional — used in scoring only
23
+ hardware: "gpu-low", // optional — required (along with maxLatencyMs) to enforce latency
24
+ maxLatencyMs: 2000, // optional — v0.2: real hard filter when benchmark data exists
25
+ scoringMode: "rule", // optional — v0.3: "rule" (default) | "embedding" | "hybrid"
26
+ limit: 5, // optional — defaults to 5
27
+ });
28
+
29
+ console.log(results);
30
+ // [
31
+ // {
32
+ // name: "org/some-7b-chat-model",
33
+ // provider: "huggingface",
34
+ // paramsB: 6.5,
35
+ // contextWindow: null, // see "Known limitations" below
36
+ // reasonWhy: "Strong task match, sufficient context window, and comfortably within your parameter limit.",
37
+ // score: 87,
38
+ // scoringMode: "rule"
39
+ // },
40
+ // ...
41
+ // ]
42
+ ```
43
+
44
+ ### Multi-provider (v0.4)
45
+
46
+ ```ts
47
+ // Query a locally running Ollama installation instead of (or alongside) HuggingFace.
48
+ await findModels({ task: "chat", contextLength: 4096, providers: ["ollama"] });
49
+ await findModels({ task: "chat", contextLength: 4096, providers: ["huggingface", "ollama"] });
50
+
51
+ // Structured hardware — widened, not replacing the original string enum.
52
+ await findModels({ task: "chat", contextLength: 4096, hardware: { type: "gpu", vramGB: 8 } });
53
+ await findModels({ task: "chat", contextLength: 4096, hardware: "gpu-low" }); // still works
54
+ ```
55
+
56
+ **Prerequisite for `providers: ["ollama"]`:** Ollama must be running locally (`ollama serve`, or the desktop app) and reachable at `http://localhost:11434`. If it isn't, `smallm` doesn't throw or crash the whole call — it logs a warning and simply returns no Ollama candidates, so a mixed `["huggingface", "ollama"]` query still returns your HuggingFace results.
57
+
58
+ **Important:** `HardwareSpec.vramGB` filtering uses an **estimated** footprint (~2GB VRAM per 1B params, a common fp16 rule of thumb) — not a measured or authoritative figure. It's meant to rule out obviously-too-large models, not to guarantee something fits.
59
+
60
+ ### Scoring modes (v0.3)
61
+
62
+ ```ts
63
+ // Default — identical to v0.1/v0.2 behavior. Matches on exact task/pipeline_tag mapping.
64
+ await findModels({ task: "chat", contextLength: 4096, scoringMode: "rule" });
65
+
66
+ // Scores purely on text similarity between your task string and each model's
67
+ // available text (id, pipeline_tag, tags) — useful for free-text tasks that
68
+ // don't map neatly onto the fixed task enum.
69
+ await findModels({ task: "pull key dates and amounts from scanned invoices", contextLength: 4096, scoringMode: "embedding" });
70
+
71
+ // Blends both: hybridScore = ruleScore * 0.60 + embeddingScore * 0.40 (locked weights — rule score still dominates).
72
+ await findModels({ task: "chat", contextLength: 4096, scoringMode: "hybrid" });
73
+ ```
74
+
75
+ ### Multi-provider + structured hardware (v0.4)
76
+
77
+ ```ts
78
+ // Query Ollama's locally installed models instead of (or alongside) HuggingFace.
79
+ // Requires a running local Ollama installation — see "Ollama prerequisite" below.
80
+ await findModels({ task: "chat", contextLength: 4096, providers: ["ollama"] });
81
+
82
+ await findModels({ task: "chat", contextLength: 4096, providers: ["huggingface", "ollama"] });
83
+
84
+ // Structured hardware spec — widens (doesn't replace) the original string enum.
85
+ await findModels({
86
+ task: "chat",
87
+ contextLength: 4096,
88
+ hardware: { type: "gpu", vramGB: 8 }, // excludes models estimated to not fit
89
+ });
90
+
91
+ // Old string-enum form still works exactly as before:
92
+ await findModels({ task: "chat", contextLength: 4096, hardware: "gpu-low" });
93
+ ```
94
+
95
+ **Ollama prerequisite:** `providers: ["ollama"]` (or including `"ollama"` in a multi-provider list) expects a local Ollama installation running at `http://localhost:11434`. If it's not running or not installed, `smallm` doesn't throw or crash the whole query — it logs a warning and simply contributes zero Ollama candidates, so a mixed `["huggingface", "ollama"]` query still returns HuggingFace results normally. Ollama itself is never installed, started, or asked to download models by this package — it only queries what's already available locally.
96
+
97
+ **`vramGB` filtering is a heuristic, not a measured constraint.** There's no standard "how much VRAM does model X need" figure available from either provider's listing API, so `smallm` estimates it from `paramsB` using a commonly-cited rule of thumb (~2GB VRAM per 1B parameters, roughly fp16 inference). A model quantized to 4-bit would actually need much less — this estimate is conservative/pessimistic in that case. Treat `vramGB` filtering as a rough guide, not a guarantee.
98
+
99
+ ### Handling errors (v0.2)
100
+
101
+ ```ts
102
+ import { findModels, ValidationError, RateLimitError, HFApiError } from "smallm";
103
+
104
+ try {
105
+ await findModels({ task: "chat", contextLength: 4096 });
106
+ } catch (err) {
107
+ if (err instanceof ValidationError) {
108
+ // your query was malformed — fix it, retrying won't help
109
+ } else if (err instanceof RateLimitError) {
110
+ // HuggingFace rate-limited us — smallm already retried 3x with backoff before this threw
111
+ } else if (err instanceof HFApiError) {
112
+ // some other non-2xx from HuggingFace
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## API
118
+
119
+ ### `findModels(query: ModelQuery): Promise<ModelMatch[]>`
120
+
121
+ The single public entry point. Validates the query, fetches live candidates from HuggingFace (cached, retrying transient failures), applies hard filters, scores and ranks the survivors, and returns the top N.
122
+
123
+ ### `ModelQuery`
124
+
125
+ | Field | Type | Required | Notes |
126
+ |---|---|---|---|
127
+ | `task` | `"summarize" \| "classify" \| "extract" \| "chat" \| "code" \| "translate" \| string` | yes | |
128
+ | `contextLength` | `number` | yes | Hard filter (tokens) |
129
+ | `hardware` | `"cpu" \| "gpu-low" \| "gpu-high" \| { type: "cpu" \| "gpu"; vramGB?: number }` | no | String enum: required alongside `maxLatencyMs` to enforce latency filtering (v0.2). Object form (v0.4): `vramGB` enables the estimated-footprint hard filter. |
130
+ | `domain` | `string` | no | Used in scoring, not filtering |
131
+ | `maxParamsB` | `number` | no | Hard filter, e.g. `7` = "under 7B" |
132
+ | `maxLatencyMs` | `number` | no | **v0.2:** real hard filter when a benchmark entry exists for the (model, string-enum `hardware`) pair. No entry = not enforced. |
133
+ | `scoringMode` | `"rule" \| "embedding" \| "hybrid"` | no | **v0.3:** which scoring strategy to use. Defaults to `"rule"`. |
134
+ | `providers` | `("huggingface" \| "ollama")[]` | no | **v0.4:** which registries to query. Defaults to `["huggingface"]`. |
135
+ | `limit` | `number` | no | Defaults to `5` |
136
+ | `cacheOptions` | `{ dir?: string; ttlMs?: number }` | no | **v0.2:** configure the file-based cache. Defaults: OS temp dir + `/smallm-cache`, 5-minute TTL |
137
+
138
+ ### `ModelMatch`
139
+
140
+ | Field | Type | Notes |
141
+ |---|---|---|
142
+ | `name` | `string` | Provider-specific model id |
143
+ | `provider` | `"huggingface" \| "ollama"` | **v0.4:** widened from the literal `"huggingface"` |
144
+ | `paramsB` | `number \| null` | `null` if size couldn't be detected |
145
+ | `contextWindow` | `number \| null` | `null` if unknown (see limitations) |
146
+ | `reasonWhy` | `string` | Generated from the scoring breakdown |
147
+ | `score` | `number` | 0–100 |
148
+ | `scoringMode` | `"rule" \| "embedding" \| "hybrid"` | **v0.3:** which mode produced this result's score |
149
+
150
+ ### Errors (v0.2)
151
+
152
+ All thrown errors extend `SmallmError`:
153
+
154
+ - `ValidationError` — bad `ModelQuery` input. Never retried.
155
+ - `HFApiError` — non-2xx response from HuggingFace (base class; carries `.status`).
156
+ - `RateLimitError` — `HFApiError` subclass specifically for HTTP 429.
157
+
158
+ `findModels` retries transient HuggingFace failures (429 and any 5xx) up to 3 times with exponential backoff (500ms / 1000ms / 2000ms) before throwing.
159
+
160
+ ## How scoring works
161
+
162
+ Hard filters run **before** any scoring — a model that fails a hard filter never gets scored, regardless of `scoringMode` or which `providers` it came from:
163
+
164
+ - `maxParamsB`, `contextLength`, `task` (MVP)
165
+ - `maxLatencyMs` when a benchmark entry exists for the (model, string-enum `hardware`) pair (v0.2)
166
+ - `hardware.vramGB` when an estimated footprint exceeds it (v0.4, heuristic — see "Known limitations")
167
+
168
+ Survivors are scored. In `"rule"` mode (the default) that's four weighted components:
169
+
170
+ - **Task match** — 50%
171
+ - **Context window fit** — 20%
172
+ - **Size fit** — 15%
173
+ - **Domain match** — 15%
174
+
175
+ `"embedding"` mode scores purely on text similarity instead; `"hybrid"` blends both 60/40 (rule/embedding). See "Scoring modes (v0.3)" above.
176
+
177
+ Unknown metadata (e.g. undetectable param count, no benchmark entry, unreachable Ollama) is never punished or excluded — it gets a neutral mid-range score for that component only, or is simply not filtered on.
178
+
179
+ ## Known limitations
180
+
181
+ - **Embedding mode uses lexical, not semantic, similarity.** The v0.3 `"embedding"`/`"hybrid"` scoring modes are backed by a small, local, dependency-free character-trigram hashing scheme (see `src/embeddings.ts`) — not a downloaded neural embedding model. It's genuinely offline and free, but two phrases that are semantically close yet share few characters (e.g. "extract key-value pairs from invoices" vs. "structured data extraction") will score lower than a real embedding model would give them.
182
+ - **`vramGB` filtering is a heuristic, not a measured constraint.** Estimated from `paramsB` at ~2GB/1B params (roughly fp16). Quantized models need much less — this estimate skews conservative in that case. See "Multi-provider + structured hardware (v0.4)" above.
183
+ - **Benchmark data is illustrative, not measured.** The shipped `src/data/benchmarks.json` contains a small set of placeholder latency numbers for demonstration and testing — not real measured inference latency. Replace it with genuinely benchmarked data before relying on `maxLatencyMs` exclusions in production.
184
+ - `contextWindow` is currently always `null` for every provider — neither HuggingFace's list endpoint nor Ollama's `/api/tags` returns it, and the pipeline doesn't do a per-model detail fetch. This means the context-length hard filter rarely excludes anything, and the context-fit score is always neutral. Tracked as a possible future addition.
185
+ - Ollama candidates never have a `pipeline_tag`, so the task hard filter always lets them through (same "unknown, don't exclude" rule as everywhere else) — task relevance for Ollama results relies entirely on scoring, not filtering.
186
+ - Sub-billion-parameter models (e.g. `125M`) aren't detected by the size regex and report `paramsB: null`.
187
+ - File-based cache only — no database engine. Cache entries are per-process-agnostic (survive restarts) but there's no cross-machine sharing.
188
+
189
+ ## CLI (v0.5, optional)
190
+
191
+ `smallm` is library-first, but ships an optional CLI as a thin wrapper around `findModels()`. Every flag maps 1:1 to a `ModelQuery` field — the CLI contains no scoring, filtering, or provider logic of its own; it only parses flags and formats output. If you can do it via `findModels()`, you can do it via the CLI, and nothing else.
192
+
193
+ ```bash
194
+ npx smallm find --task summarize --context 4096
195
+ npx smallm find --task chat --context 8192 --max-params 7 --json
196
+ npx smallm find --task chat --context 4096 --providers huggingface,ollama --hardware-type gpu --hardware-vram 8
197
+ ```
198
+
199
+ Default output is a table:
200
+
201
+ ```
202
+ NAME PROVIDER PARAMS(B) SCORE MODE REASON
203
+ ------------------------------------------ ------------ ---------- ------ ---------- --------------------------------------------------
204
+ org/some-7b-chat-model huggingface 6.5 87 rule Strong task match, sufficient context window, and…
205
+ ```
206
+
207
+ Pass `--json` for the raw `ModelMatch[]` instead — identical to what `findModels()` returns programmatically:
208
+
209
+ ```bash
210
+ npx smallm find --task chat --context 4096 --json | jq '.[0].name'
211
+ ```
212
+
213
+ ### Flags
214
+
215
+ | Flag | Maps to | Notes |
216
+ |---|---|---|
217
+ | `--task <task>` | `task` | Required |
218
+ | `--context <tokens>` | `contextLength` | Required |
219
+ | `--max-params <billions>` | `maxParamsB` | |
220
+ | `--domain <domain>` | `domain` | |
221
+ | `--hardware <cpu\|gpu-low\|gpu-high>` | `hardware` (string form) | |
222
+ | `--hardware-type <cpu\|gpu>` + `--hardware-vram <gb>` | `hardware` (`HardwareSpec` form) | Takes precedence over `--hardware` if both given |
223
+ | `--max-latency <ms>` | `maxLatencyMs` | |
224
+ | `--scoring-mode <rule\|embedding\|hybrid>` | `scoringMode` | |
225
+ | `--providers <list>` | `providers` | Comma-separated, e.g. `huggingface,ollama` |
226
+ | `--limit <n>` | `limit` | |
227
+ | `--cache-dir <path>` / `--cache-ttl <ms>` | `cacheOptions` | |
228
+ | `--json` | — (output mode only) | Not a `ModelQuery` field — controls formatting, not what's queried |
229
+
230
+ Run `npx smallm find --help` (or with no arguments) to see this from the terminal. Invalid input (a missing `--task`, a bad `--scoring-mode` value, etc.) isn't validated by the CLI itself — it's passed straight to `findModels()`, which throws the same typed errors (see "Handling errors" above) that the CLI then prints cleanly with a non-zero exit code.
231
+
232
+ ## Development
233
+
234
+ ```bash
235
+ npm install
236
+ npm run build # compile TypeScript -> dist/ (also chmods dist/cli.js executable)
237
+ npm test # run the unit + integration test suite
238
+ npm link # optional: try the CLI locally as the `smallm` command
239
+ ```
240
+
241
+ ## Changelog
242
+
243
+ See [CHANGELOG.md](./CHANGELOG.md).
244
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Looks up measured latency for a (model, hardware) pair.
3
+ * Returns null if no benchmark entry exists — per Section on unknown-value
4
+ * handling, "no data" must never be treated as "too slow." Callers (filters.ts)
5
+ * must let the model through when this returns null.
6
+ */
7
+ export declare function lookupLatencyMs(modelId: string, hardware: string): number | null;
8
+ /** Exposed for tests/debugging — total number of loaded benchmark entries. */
9
+ export declare function benchmarkDatasetSize(): number;
10
+ //# sourceMappingURL=benchmark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark.d.ts","sourceRoot":"","sources":["../src/benchmark.ts"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhF;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.lookupLatencyMs = lookupLatencyMs;
7
+ exports.benchmarkDatasetSize = benchmarkDatasetSize;
8
+ const benchmarks_json_1 = __importDefault(require("./data/benchmarks.json"));
9
+ /**
10
+ * Loaded once at module init from the checked-in dataset.
11
+ * NOTE: the shipped src/data/benchmarks.json numbers are illustrative
12
+ * placeholders for this build — replace with real measured data before
13
+ * relying on maxLatencyMs exclusions in production.
14
+ */
15
+ const ENTRIES = benchmarks_json_1.default.entries;
16
+ /** Keyed by `${modelId}::${hardware}` for O(1) lookup. */
17
+ const INDEX = new Map(ENTRIES.map((entry) => [benchmarkKey(entry.modelId, entry.hardware), entry]));
18
+ function benchmarkKey(modelId, hardware) {
19
+ return `${modelId}::${hardware}`;
20
+ }
21
+ /**
22
+ * Looks up measured latency for a (model, hardware) pair.
23
+ * Returns null if no benchmark entry exists — per Section on unknown-value
24
+ * handling, "no data" must never be treated as "too slow." Callers (filters.ts)
25
+ * must let the model through when this returns null.
26
+ */
27
+ function lookupLatencyMs(modelId, hardware) {
28
+ const entry = INDEX.get(benchmarkKey(modelId, hardware));
29
+ return entry ? entry.avgLatencyMs : null;
30
+ }
31
+ /** Exposed for tests/debugging — total number of loaded benchmark entries. */
32
+ function benchmarkDatasetSize() {
33
+ return ENTRIES.length;
34
+ }
35
+ //# sourceMappingURL=benchmark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark.js","sourceRoot":"","sources":["../src/benchmark.ts"],"names":[],"mappings":";;;;;;;AACA,6EAAmD;AAEnD;;;;;GAKG;AACH,MAAM,OAAO,GAAqB,yBAAa,CAAC,OAA2B,CAAC;AAE5E,0DAA0D;AAC1D,MAAM,KAAK,GAAgC,IAAI,GAAG,CAChD,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC,CAC7E,CAAC;AAEF,SAAS,YAAY,CAAC,OAAe,EAAE,QAAgB;IACrD,OAAO,GAAG,OAAO,KAAK,QAAQ,EAAE,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,yBAAgC,OAAe,EAAE,QAAgB;IAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IACzD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3C,CAAC;AAED,8EAA8E;AAC9E;IACE,OAAO,OAAO,CAAC,MAAM,CAAC;AACxB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { CacheOptions, HFModel } from "./types";
2
+ /**
3
+ * File-based TTL cache — same conceptual interface as MVP's in-memory
4
+ * TTLCache (get/set/clear), but each entry is a JSON file on disk, so it
5
+ * survives process restarts. No database engine, per the v0.2 guide's
6
+ * explicit "don't introduce SQLite/Postgres/etc." rule — this is still a
7
+ * small library, not a service.
8
+ */
9
+ export declare class FileTTLCache<T> {
10
+ private dir;
11
+ private ttlMs;
12
+ constructor(options?: CacheOptions);
13
+ /** Maps a cache key to a safe filename (keys can contain arbitrary characters). */
14
+ private filePathFor;
15
+ get(key: string): T | undefined;
16
+ set(key: string, value: T): void;
17
+ clear(): void;
18
+ private deleteFile;
19
+ }
20
+ /** Sets (or resets) the cache options used by fetchCandidateModelsCached. */
21
+ export declare function configureCandidateModelsCache(options?: CacheOptions): void;
22
+ /**
23
+ * (v0.2) Cached wrapper around fetchCandidateModels, backed by the
24
+ * file-based cache instead of MVP's in-memory Map. Kept for backward
25
+ * compatibility (this exact function existed pre-v0.4) — built on the
26
+ * generic getOrFetchCached below, key-namespaced to "huggingface" so it
27
+ * can't collide with other providers' cache entries.
28
+ */
29
+ export declare function fetchCandidateModelsCached(task: string): Promise<HFModel[]>;
30
+ /**
31
+ * (v0.4) Generic cached-fetch helper, used by index.ts to cache each
32
+ * requested provider's candidate list independently (keyed by
33
+ * `${providerName}:${task}`, so HuggingFace and Ollama results for the
34
+ * same task never collide). Not provider-specific itself — any async
35
+ * producer of RawModel[] can use this.
36
+ */
37
+ export declare function getOrFetchCached(key: string, fetchFn: () => Promise<HFModel[]>): Promise<HFModel[]>;
38
+ /** Exposed for tests that need to reset cache state between runs. */
39
+ export declare function clearCandidateModelsCache(): void;
40
+ //# sourceMappingURL=cache-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-file.d.ts","sourceRoot":"","sources":["../src/cache-file.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAWhD;;;;;;GAMG;AACH,qBAAa,YAAY,CAAC,CAAC;IACzB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,KAAK,CAAS;IAEtB,YAAY,OAAO,GAAE,YAAiB,EAIrC;IAED,mFAAmF;IACnF,OAAO,CAAC,WAAW;IAKnB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAyB9B;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAI/B;IAED,KAAK,IAAI,IAAI,CAYZ;IAED,OAAO,CAAC,UAAU;CAOnB;AAOD,6EAA6E;AAC7E,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,YAAiB,GAAG,IAAI,CAE9E;AASD;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAEjF;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,GAChC,OAAO,CAAC,OAAO,EAAE,CAAC,CAQpB;AAED,qEAAqE;AACrE,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileTTLCache = void 0;
37
+ exports.configureCandidateModelsCache = configureCandidateModelsCache;
38
+ exports.fetchCandidateModelsCached = fetchCandidateModelsCached;
39
+ exports.getOrFetchCached = getOrFetchCached;
40
+ exports.clearCandidateModelsCache = clearCandidateModelsCache;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ const crypto = __importStar(require("crypto"));
45
+ const huggingface_1 = require("./registry/huggingface");
46
+ const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes — same default as MVP's in-memory cache
47
+ const DEFAULT_DIR = path.join(os.tmpdir(), "smallm-cache");
48
+ /**
49
+ * File-based TTL cache — same conceptual interface as MVP's in-memory
50
+ * TTLCache (get/set/clear), but each entry is a JSON file on disk, so it
51
+ * survives process restarts. No database engine, per the v0.2 guide's
52
+ * explicit "don't introduce SQLite/Postgres/etc." rule — this is still a
53
+ * small library, not a service.
54
+ */
55
+ class FileTTLCache {
56
+ constructor(options = {}) {
57
+ this.dir = options.dir ?? DEFAULT_DIR;
58
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
59
+ fs.mkdirSync(this.dir, { recursive: true });
60
+ }
61
+ /** Maps a cache key to a safe filename (keys can contain arbitrary characters). */
62
+ filePathFor(key) {
63
+ const hash = crypto.createHash("sha256").update(key).digest("hex");
64
+ return path.join(this.dir, `${hash}.json`);
65
+ }
66
+ get(key) {
67
+ const filePath = this.filePathFor(key);
68
+ let raw;
69
+ try {
70
+ raw = fs.readFileSync(filePath, "utf-8");
71
+ }
72
+ catch {
73
+ return undefined; // file doesn't exist — not cached
74
+ }
75
+ let entry;
76
+ try {
77
+ entry = JSON.parse(raw);
78
+ }
79
+ catch {
80
+ // Corrupted cache file — treat as a miss rather than crashing the caller.
81
+ this.deleteFile(filePath);
82
+ return undefined;
83
+ }
84
+ if (Date.now() > entry.expiresAt) {
85
+ this.deleteFile(filePath);
86
+ return undefined;
87
+ }
88
+ return entry.value;
89
+ }
90
+ set(key, value) {
91
+ const filePath = this.filePathFor(key);
92
+ const entry = { value, expiresAt: Date.now() + this.ttlMs };
93
+ fs.writeFileSync(filePath, JSON.stringify(entry), "utf-8");
94
+ }
95
+ clear() {
96
+ let files;
97
+ try {
98
+ files = fs.readdirSync(this.dir);
99
+ }
100
+ catch {
101
+ return;
102
+ }
103
+ for (const file of files) {
104
+ if (file.endsWith(".json")) {
105
+ this.deleteFile(path.join(this.dir, file));
106
+ }
107
+ }
108
+ }
109
+ deleteFile(filePath) {
110
+ try {
111
+ fs.unlinkSync(filePath);
112
+ }
113
+ catch {
114
+ // already gone — fine
115
+ }
116
+ }
117
+ }
118
+ exports.FileTTLCache = FileTTLCache;
119
+ // Module-level singleton, matching MVP's cache.ts pattern. Options are
120
+ // applied on first use; call configureCandidateModelsCache() before the
121
+ // first findModels() call if you need non-default dir/ttl.
122
+ let candidateModelsCache;
123
+ /** Sets (or resets) the cache options used by fetchCandidateModelsCached. */
124
+ function configureCandidateModelsCache(options = {}) {
125
+ candidateModelsCache = new FileTTLCache(options);
126
+ }
127
+ function getCache() {
128
+ if (!candidateModelsCache) {
129
+ candidateModelsCache = new FileTTLCache();
130
+ }
131
+ return candidateModelsCache;
132
+ }
133
+ /**
134
+ * (v0.2) Cached wrapper around fetchCandidateModels, backed by the
135
+ * file-based cache instead of MVP's in-memory Map. Kept for backward
136
+ * compatibility (this exact function existed pre-v0.4) — built on the
137
+ * generic getOrFetchCached below, key-namespaced to "huggingface" so it
138
+ * can't collide with other providers' cache entries.
139
+ */
140
+ async function fetchCandidateModelsCached(task) {
141
+ return getOrFetchCached(`huggingface:${task}`, () => (0, huggingface_1.fetchCandidateModels)(task));
142
+ }
143
+ /**
144
+ * (v0.4) Generic cached-fetch helper, used by index.ts to cache each
145
+ * requested provider's candidate list independently (keyed by
146
+ * `${providerName}:${task}`, so HuggingFace and Ollama results for the
147
+ * same task never collide). Not provider-specific itself — any async
148
+ * producer of RawModel[] can use this.
149
+ */
150
+ async function getOrFetchCached(key, fetchFn) {
151
+ const cache = getCache();
152
+ const cached = cache.get(key);
153
+ if (cached)
154
+ return cached;
155
+ const result = await fetchFn();
156
+ cache.set(key, result);
157
+ return result;
158
+ }
159
+ /** Exposed for tests that need to reset cache state between runs. */
160
+ function clearCandidateModelsCache() {
161
+ getCache().clear();
162
+ }
163
+ //# sourceMappingURL=cache-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-file.js","sourceRoot":"","sources":["../src/cache-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { HFModel } from "./types";
2
+ /**
3
+ * Generic in-memory TTL cache. No disk persistence — out of scope for MVP
4
+ * (Section 2.2). Its only job is avoiding duplicate/rate-limited calls for
5
+ * repeated identical queries within the TTL window.
6
+ */
7
+ export declare class TTLCache<T> {
8
+ private ttlMs;
9
+ private store;
10
+ constructor(ttlMs?: number);
11
+ get(key: string): T | undefined;
12
+ set(key: string, value: T): void;
13
+ clear(): void;
14
+ get size(): number;
15
+ }
16
+ /**
17
+ * Cached wrapper around fetchCandidateModels. Same task within the TTL
18
+ * window returns the cached list instead of hitting HuggingFace again.
19
+ */
20
+ export declare function fetchCandidateModelsCached(task: string): Promise<HFModel[]>;
21
+ /** Exposed for tests that need to reset cache state between runs. */
22
+ export declare function clearCandidateModelsCache(): void;
23
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAUlC;;;;GAIG;AACH,qBAAa,QAAQ,CAAC,CAAC;IAGT,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,KAAK,CAAoC;IAEjD,YAAoB,KAAK,GAAE,MAAuB,EAAI;IAEtD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAQ9B;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAE/B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAMD;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAOjF;AAED,qEAAqE;AACrE,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
package/dist/cache.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TTLCache = void 0;
4
+ exports.fetchCandidateModelsCached = fetchCandidateModelsCached;
5
+ exports.clearCandidateModelsCache = clearCandidateModelsCache;
6
+ const huggingface_1 = require("./registry/huggingface");
7
+ const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
8
+ /**
9
+ * Generic in-memory TTL cache. No disk persistence — out of scope for MVP
10
+ * (Section 2.2). Its only job is avoiding duplicate/rate-limited calls for
11
+ * repeated identical queries within the TTL window.
12
+ */
13
+ class TTLCache {
14
+ constructor(ttlMs = DEFAULT_TTL_MS) {
15
+ this.ttlMs = ttlMs;
16
+ this.store = new Map();
17
+ }
18
+ get(key) {
19
+ const entry = this.store.get(key);
20
+ if (!entry)
21
+ return undefined;
22
+ if (Date.now() > entry.expiresAt) {
23
+ this.store.delete(key);
24
+ return undefined;
25
+ }
26
+ return entry.value;
27
+ }
28
+ set(key, value) {
29
+ this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });
30
+ }
31
+ clear() {
32
+ this.store.clear();
33
+ }
34
+ get size() {
35
+ return this.store.size;
36
+ }
37
+ }
38
+ exports.TTLCache = TTLCache;
39
+ // Module-level singleton cache, keyed by task string (fetchCandidateModels
40
+ // only varies by task, since that's what maps to pipeline_tag).
41
+ const candidateModelsCache = new TTLCache();
42
+ /**
43
+ * Cached wrapper around fetchCandidateModels. Same task within the TTL
44
+ * window returns the cached list instead of hitting HuggingFace again.
45
+ */
46
+ async function fetchCandidateModelsCached(task) {
47
+ const cached = candidateModelsCache.get(task);
48
+ if (cached)
49
+ return cached;
50
+ const result = await (0, huggingface_1.fetchCandidateModels)(task);
51
+ candidateModelsCache.set(task, result);
52
+ return result;
53
+ }
54
+ /** Exposed for tests that need to reset cache state between runs. */
55
+ function clearCandidateModelsCache() {
56
+ candidateModelsCache.clear();
57
+ }
58
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":";;;;;AACA,wDAA8D;AAE9D,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,YAAY;AAOlD;;;;GAIG;AACH;IAGE,YAAoB,KAAK,GAAW,cAAc;QAA9B,UAAK,GAAL,KAAK,CAAyB;QAF1C,UAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEI,CAAC;IAEtD,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF;;AAED,2EAA2E;AAC3E,gEAAgE;AAChE,MAAM,oBAAoB,GAAG,IAAI,QAAQ,EAAa,CAAC;AAEvD;;;GAGG;AACI,KAAK,qCAAqC,IAAY;IAC3D,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,MAAM,GAAG,MAAM,IAAA,kCAAoB,EAAC,IAAI,CAAC,CAAC;IAChD,oBAAoB,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,qEAAqE;AACrE;IACE,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC"}