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.
- package/LICENSE +15 -0
- package/README.md +244 -0
- package/dist/benchmark.d.ts +10 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +35 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/cache-file.d.ts +40 -0
- package/dist/cache-file.d.ts.map +1 -0
- package/dist/cache-file.js +163 -0
- package/dist/cache-file.js.map +1 -0
- package/dist/cache.d.ts +23 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +58 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +32 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +256 -0
- package/dist/cli.js.map +1 -0
- package/dist/data/benchmarks.json +61 -0
- package/dist/embeddings.d.ts +50 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +118 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/filters.d.ts +11 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +66 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/params.d.ts +21 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +50 -0
- package/dist/params.js.map +1 -0
- package/dist/query.d.ts +23 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +81 -0
- package/dist/query.js.map +1 -0
- package/dist/registry/huggingface.d.ts +24 -0
- package/dist/registry/huggingface.d.ts.map +1 -0
- package/dist/registry/huggingface.js +97 -0
- package/dist/registry/huggingface.js.map +1 -0
- package/dist/registry/ollama.d.ts +3 -0
- package/dist/registry/ollama.d.ts.map +1 -0
- package/dist/registry/ollama.js +78 -0
- package/dist/registry/ollama.js.map +1 -0
- package/dist/registry/provider.d.ts +12 -0
- package/dist/registry/provider.d.ts.map +1 -0
- package/dist/registry/provider.js +3 -0
- package/dist/registry/provider.js.map +1 -0
- package/dist/scorer.d.ts +45 -0
- package/dist/scorer.d.ts.map +1 -0
- package/dist/scorer.js +161 -0
- package/dist/scorer.js.map +1 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- 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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"}
|
package/dist/cache.d.ts
ADDED
|
@@ -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"}
|