open-classify 0.9.3 → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ A focused rewrite of the consumer-facing surface. The runtime is unchanged; what's new is how a consumer installs, configures, and customizes Open Classify.
6
+
7
+ ### Scaffold layout
8
+
9
+ The scaffold is now a single directory at the project root:
10
+
11
+ ```
12
+ your-project/
13
+ └── open-classify/
14
+ ├── config.json
15
+ ├── downstream-models.json
16
+ ├── README.md
17
+ └── classifiers/
18
+ └── README.md
19
+ ```
20
+
21
+ Previously, `init` wrote three things at the project root (`open-classify.config.json`, `downstream-models.json`, `classifiers/`) plus four `_<name>/` template folders inside `classifiers/`. The 1.0 scaffold keeps everything together in one folder, leaves `classifiers/` empty by default, and uses the new `eject` command for stock-classifier customization.
22
+
23
+ ### CLI
24
+
25
+ Subcommands:
26
+
27
+ - `init` — copy `open-classify/` into the current project. Re-run safe.
28
+ - `eject <name>` — copy a stock classifier (`tools`, `memory_retrieval_queries`, `conversation_digest`, `context_shift`) into `open-classify/classifiers/<name>/` so you can edit it.
29
+ - `doctor` — verify install, config, Ollama, classifiers.
30
+ - `try <message>` — one-shot smoke test.
31
+
32
+ Flags trimmed to `--yes`, `--force`, `--dry-run` across the board.
33
+
34
+ Removed:
35
+
36
+ - `uninstall` subcommand. `rm -rf open-classify/ && npm uninstall open-classify` is the documented path. Bundling it into a CLI created the npx "needs to install" prompt — the cure was worse than the disease.
37
+ - `init --minimal`, `init --no-install`, `init --package-manager`, `init --classifier-dir`. None of these earned their slot in the help text.
38
+ - `init`'s auto-install. Install the package first (`npm install open-classify`), then run `init`. Predictable; matches every other tool in the ecosystem.
39
+
40
+ ### Config schema
41
+
42
+ - Default path: `open-classify/config.json` (was `open-classify.config.json` at project root).
43
+ - `classifiers.stock` is now a `string[]` of names to enable (was a `Record<string, boolean>` map).
44
+ - All paths in the config (`catalog`, `classifiers.dirs`) resolve relative to the config file's directory — so the scaffold works regardless of where your server starts from.
45
+
46
+ ### Stock classifier customization
47
+
48
+ The `_<name>/` template-rename pattern is gone. Customizing a stock classifier is now an explicit `eject`:
49
+
50
+ ```sh
51
+ npx open-classify eject tools
52
+ ```
53
+
54
+ Copies the stock files into `open-classify/classifiers/tools/`. The runtime transparently prefers your local copy over the package version — a user classifier with the same name as a stock classifier always wins. To revert, delete the folder.
55
+
56
+ ### Package contents
57
+
58
+ Templates split into:
59
+
60
+ - `templates/scaffold/open-classify/` — copied wholesale by `init`
61
+ - `templates/stock/<name>/` — copied one at a time by `eject <name>`
62
+
63
+ Every scaffolded file is a real file in the package now, not an inlined JS string constant. The scaffolded `config.json`, README, and stock classifiers are all maintainable as their native file types.
64
+
65
+ ### Removed from the package
66
+
67
+ - Root `downstream-models.json` (moved into `templates/scaffold/open-classify/`)
68
+ - Root `open-classify.config.example.json` (the scaffold IS the example)
69
+
70
+ ### Migration from 0.9.x
71
+
72
+ If you were using a 0.9.x install:
73
+
74
+ ```sh
75
+ # Move existing files into the new layout
76
+ mkdir -p open-classify/classifiers
77
+ mv open-classify.config.json open-classify/config.json
78
+ mv downstream-models.json open-classify/downstream-models.json
79
+ mv classifiers/* open-classify/classifiers/ 2>/dev/null || true
80
+ rmdir classifiers 2>/dev/null || true
81
+
82
+ # Edit open-classify/config.json: change classifiers.stock from { "tools": true, ... }
83
+ # to ["tools", ...] — only list the names that were set to true.
84
+
85
+ # Update to 1.0
86
+ npm install open-classify@latest
87
+ ```
88
+
89
+ If you had renamed any `_<name>/` template folders to activate them (e.g. `_tools/` → `tools/`), they'll still work in the new layout — local classifiers always win over stock by name.
package/README.md CHANGED
@@ -19,7 +19,6 @@ normalize + trim classifier context
19
19
  ├─► preflight ─────────────► final_reply? / ack_reply?
20
20
  ├─► model_tier ────────────► model_tier?
21
21
  ├─► model_specialization ──► model_specialization?
22
- ├─► tools ─────────────────► tools?
23
22
  ├─► prompt_injection ─────► risk_level?
24
23
  └─► your own classifiers ──► any JSON-Schema-validated payload
25
24
  (all run in parallel, capped by maxConcurrency)
@@ -49,15 +48,21 @@ Prerequisites: Node 18+, [Ollama](https://ollama.com), and the default classifie
49
48
  ollama pull gemma4:e4b-it-q4_K_M
50
49
  ```
51
50
 
52
- **1. Scaffold (from your project root)**
51
+ **1. Install**
52
+
53
+ ```sh
54
+ npm install open-classify
55
+ ```
56
+
57
+ **2. Scaffold**
53
58
 
54
59
  ```sh
55
60
  npx open-classify init
56
61
  ```
57
62
 
58
- If the package isn't installed yet, `init` offers to add it. It writes `open-classify.config.json`, `downstream-models.json`, and a `classifiers/` directory. Re-run safe: existing files are skipped. Verify the install at any time with `npx open-classify doctor`.
63
+ This creates a single `open-classify/` directory in your project root with the config, model catalog, and a place for your own classifiers. Verify the setup at any time with `npx open-classify doctor`.
59
64
 
60
- **2. Use it**
65
+ **3. Use it**
61
66
 
62
67
  ```ts
63
68
  import { createClassifier } from "open-classify";
@@ -73,29 +78,51 @@ else if (result.action === "block") handleBlock(result.block_reason); // inj
73
78
  else callDownstream(result.model_id, result.tools, result.reply?.text); // route the real request
74
79
  ```
75
80
 
76
- `createClassifier()` looks for `open-classify.config.json` in the working directory, so the scaffolded layout works with no further wiring.
81
+ `createClassifier()` finds `open-classify/config.json` in the working directory no other wiring required.
82
+
83
+ ## Removal
84
+
85
+ ```sh
86
+ rm -rf open-classify/
87
+ npm uninstall open-classify
88
+ ```
77
89
 
78
- **3. Enable or customize optional classifiers**
90
+ That's it. The scaffold lives in one folder; removing it leaves no trace.
79
91
 
80
- Four mandatory base classifiers (`preflight`, `model_tier`, `model_specialization`, `prompt_injection`) always run from the package. Four more (`tools`, `memory_retrieval_queries`, `conversation_digest`, `context_shift`) are optional and default to off.
92
+ ## Optional stock classifiers
81
93
 
82
- You have two ways to use the optional ones:
94
+ Open Classify ships four optional stock classifiers: `tools`, `memory_retrieval_queries`, `conversation_digest`, `context_shift`. They're off by default. Enable one in `open-classify/config.json`:
83
95
 
84
96
  ```json
85
- { "classifiers": { "stock": { "tools": true } } }
97
+ {
98
+ "classifiers": {
99
+ "dirs": ["classifiers"],
100
+ "stock": ["tools"]
101
+ }
102
+ }
86
103
  ```
87
104
 
88
- Set the toggle to `true` to run the package-owned version. `npm update open-classify` keeps the prompt current.
105
+ The package-owned prompt is used, and `npm update open-classify` keeps it current.
89
106
 
90
- Or customize a local copy. `init` scaffolds editable templates in `classifiers/_<name>/` (inactive because of the underscore prefix). To take one over, keep the stock toggle off and rename the folder:
107
+ When you want to take a stock classifier over and edit its prompt:
91
108
 
92
109
  ```sh
93
- mv classifiers/_tools classifiers/tools
110
+ npx open-classify eject tools
94
111
  ```
95
112
 
96
- The same convention works in reverse: rename any active classifier `<name>/` `_<name>/` to deactivate without deleting.
113
+ That copies the stock files into `open-classify/classifiers/tools/`. The runtime transparently prefers your local copy over the package version, so this works whether or not `tools` is also listed in `classifiers.stock` local always wins on name. To revert: delete the folder. If `tools` is still listed in `classifiers.stock`, the package version takes over; otherwise the classifier stops running.
97
114
 
98
- To write a new classifier, drop a `<name>/manifest.json` + `<name>/prompt.md` in `classifiers/`. See [docs/adding-a-classifier.md](docs/adding-a-classifier.md).
115
+ ## Writing your own classifier
116
+
117
+ Drop a folder into `open-classify/classifiers/` with two files:
118
+
119
+ ```
120
+ open-classify/classifiers/topic_tags/
121
+ ├── manifest.json
122
+ └── prompt.md
123
+ ```
124
+
125
+ The folder name must match the manifest's `name`. The runtime picks it up on the next start. See [docs/adding-a-classifier.md](docs/adding-a-classifier.md) for the full reference.
99
126
 
100
127
  ### Classifying assistant output
101
128
 
@@ -150,17 +177,14 @@ Example result:
150
177
  "model_tier": { "model_tier": "frontier_strong", "reason": "...", "certainty": 0.88 },
151
178
  "model_specialization": { "model_specialization": "coding", "reason": "...", "certainty": 0.75 },
152
179
  "tools": { "tools": ["workspace"], "reason": "...", "certainty": 0.88 },
153
- "prompt_injection": { "risk_level": "normal", "reason": "...", "certainty": 0.97 },
154
- "memory_retrieval_queries": { "queries": ["user code review preferences"], "reason": "...", "certainty": 0.75 }
180
+ "prompt_injection": { "risk_level": "normal", "reason": "...", "certainty": 0.97 }
155
181
  }
156
182
  }
157
183
  ```
158
184
 
159
185
  ## Classifier model
160
186
 
161
- Every classifier bundled or your own uses the same two-file shape (`manifest.json` + `prompt.md`) and emits the same envelope: `{ reason, certainty, ...payload }`. Some payload fields are **reserved** (like `model_tier`, `final_reply`, `risk_level`); the aggregator knows how to consume them into the routing decision. Everything else passes through to the caller.
162
-
163
- Open Classify ships four mandatory base classifiers and four optional stock classifiers. The mandatory base classifiers always load from the package, can't be turned off, and are updated by `npm update open-classify`. Optional stock classifiers also live in the package, but are enabled by `open-classify.config.json`.
187
+ Open Classify ships four mandatory base classifiers that always run, plus four optional stock classifiers you can enable or eject.
164
188
 
165
189
  | Name | dispatch_order | Reserved fields | Bundled as | What the aggregator does with it |
166
190
  |---|---|---|---|---|
@@ -173,76 +197,7 @@ Open Classify ships four mandatory base classifiers and four optional stock clas
173
197
  | `conversation_digest` | 70 | — | optional stock | Passes through |
174
198
  | `context_shift` | 80 | — | optional stock | Passes through |
175
199
 
176
- For package-owned stock classifiers, `open-classify.config.json` is the on/off switch:
177
-
178
- ```json
179
- {
180
- "classifiers": {
181
- "stock": {
182
- "tools": true,
183
- "memory_retrieval_queries": false,
184
- "conversation_digest": false,
185
- "context_shift": false
186
- }
187
- }
188
- }
189
- ```
190
-
191
- For copied/custom classifiers in `classifiers/`, the directory-naming convention still applies: `_<name>/` is inactive; `<name>/` runs. Root project files are user-owned, so `init` skips existing config/classifier files unless you pass `--force`.
192
-
193
- > Need to customize `preflight`'s prompt or any other mandatory built-in? Use a custom `RunClassifier` (see [Bring your own backend](#bring-your-own-backend)) to intercept it, or fork the package.
194
-
195
- ## Adding your own classifier
196
-
197
- The two files are:
198
-
199
- ```
200
- classifiers/topic_tags/
201
- ├── manifest.json
202
- └── prompt.md
203
- ```
204
-
205
- `manifest.json` declares the output shape and a fallback for when the classifier errors:
206
-
207
- ```json
208
- {
209
- "name": "topic_tags",
210
- "version": "1.0.0",
211
- "purpose": "Tag the message with a small set of topic labels for analytics.",
212
- "dispatch_order": 70,
213
- "output_schema": {
214
- "required": ["tags"],
215
- "properties": {
216
- "tags": {
217
- "type": "array", "maxItems": 5,
218
- "items": { "type": "string", "minLength": 1, "maxLength": 40 }
219
- }
220
- }
221
- },
222
- "fallback": {
223
- "reason": "Classifier failed; no tags generated.",
224
- "certainty": "no_signal",
225
- "tags": []
226
- }
227
- }
228
- ```
229
-
230
- `prompt.md` is the classification rule in plain language. No need to write JSON examples — the runtime synthesizes one from your schema — and no need to paste enum values for reserved fields:
231
-
232
- ```markdown
233
- You are the topic_tags classifier.
234
-
235
- `tags` are short single-word topic labels (lowercase, no spaces). Use at most five.
236
- Return an empty array when no clear topic applies.
237
- ```
238
-
239
- Consume:
240
-
241
- ```ts
242
- const tags = result.classifier_outputs.topic_tags?.tags ?? [];
243
- ```
244
-
245
- Rules: `name` must match the directory name; reserved-field names can't appear in `output_schema.properties` (declare them under `reserved_fields` instead); `fallback` only needs `reason` and `certainty`; name collisions throw at startup. See [docs/adding-a-classifier.md](docs/adding-a-classifier.md) for the full reference.
200
+ To customize a mandatory built-in, use a custom `RunClassifier` (see [Bring your own backend](#bring-your-own-backend)).
246
201
 
247
202
  ## Using reserved fields in your own classifier
248
203
 
@@ -263,52 +218,9 @@ The runtime injects canonical sub-schemas and prompt fragments for each declared
263
218
 
264
219
  The available reserved fields are: `final_reply`, `ack_reply`, `model_tier`, `model_specialization`, `tools`, `risk_level`. The `tools` field additionally requires an `allowed_tools` array on the manifest listing the tool ids the classifier may pick from.
265
220
 
266
- ## Model catalog
267
-
268
- Classifiers never emit model ids. They emit constraints; your catalog maps constraints to concrete models.
269
-
270
- ```json
271
- {
272
- "models": [
273
- {
274
- "id": "gpt-5.5",
275
- "provider": "openai",
276
- "runtime": "api",
277
- "specializations": [
278
- "chat",
279
- "writing",
280
- "reasoning",
281
- "planning",
282
- "coding",
283
- "tool_use"
284
- ],
285
- "tier": "frontier_strong",
286
- "params_in_billions": null,
287
- "context_window": 1050000,
288
- "max_output_tokens": 128000,
289
- "input_tokens_cpm": 5,
290
- "cached_tokens_cpm": 0.5,
291
- "output_tokens_cpm": 30
292
- }
293
- ],
294
- "default": "gpt-5.5",
295
- "pricing_unit": "USD per 1M tokens"
296
- }
297
- ```
298
-
299
- The resolver picks the cheapest model matching `model_specialization` and `model_tier`, relaxing constraints in order when nothing fits. See [docs/resolver.md](docs/resolver.md) for ranking details.
300
-
301
- ## Input contract
302
-
303
- `classify({ messages })` — that's the whole input.
304
-
305
- - `messages` is chronological, oldest to newest, and must end with the user message you want classified.
306
- - Open Classify keeps whole messages only, drops oldest first to fit a 5,000-char budget, and caps history at 20 messages.
307
- - Unknown fields are rejected, not passed through.
308
-
309
221
  ## Configuration
310
222
 
311
- `npx open-classify init` writes a working `open-classify.config.json` for you. To customize, edit it directly — the full set of supported fields (with realistic example values) lives in [open-classify.config.example.json](open-classify.config.example.json).
223
+ `open-classify/config.json` supports:
312
224
 
313
225
  | Field | What it controls |
314
226
  |---|---|
@@ -317,9 +229,17 @@ The resolver picks the cheapest model matching `model_specialization` and `model
317
229
  | `runner.defaultModel` | Classifier model used when there is no per-classifier override. |
318
230
  | `runner.options` | Ollama generation options: `temperature`, `top_p`, `seed`, `num_ctx`. |
319
231
  | `runner.models` | Per-classifier model overrides. Flat map keyed by classifier name. |
320
- | `catalog` | Path to the downstream model catalog (relative to the config file). |
321
- | `classifiers.dirs` | Directories of user-owned classifiers to load. |
322
- | `classifiers.stock` | Toggles for package-owned optional stock classifiers. |
232
+ | `catalog` | Path to the downstream model catalog, relative to `open-classify/`. |
233
+ | `classifiers.dirs` | Directories of user-owned classifiers, relative to `open-classify/`. |
234
+ | `classifiers.stock` | Array of stock classifiers to enable. Members of `tools`, `memory_retrieval_queries`, `conversation_digest`, `context_shift`. |
235
+
236
+ ## Input contract
237
+
238
+ `classify({ messages })` — that's the whole input.
239
+
240
+ - `messages` is chronological, oldest to newest, and must end with the user message you want classified.
241
+ - Open Classify keeps whole messages only, drops oldest first to fit a 5,000-char budget, and caps history at 20 messages.
242
+ - Unknown fields are rejected, not passed through.
323
243
 
324
244
  ## Bring your own backend
325
245
 
@@ -345,7 +265,35 @@ const runClassifier: RunClassifier = async (name, input, signal) => {
345
265
  const { classify, inspect } = createClassifier({ runClassifier });
346
266
  ```
347
267
 
348
- For the lowest-level entry points, `classifyOpenClassifyInput(input, { runClassifier, catalog })` and `inspectOpenClassifyInput(input, { runClassifier })` skip the factory entirely.
268
+ For the lowest-level entry points, `classifyOpenClassifyInput(input, { runClassifier, catalog, registry })` and `inspectOpenClassifyInput(input, { runClassifier, registry })` skip the factory entirely.
269
+
270
+ ## Model catalog
271
+
272
+ Classifiers never emit model ids. They emit constraints; your catalog (`open-classify/downstream-models.json`) maps constraints to concrete models.
273
+
274
+ ```json
275
+ {
276
+ "models": [
277
+ {
278
+ "id": "gpt-5.5",
279
+ "provider": "openai",
280
+ "runtime": "api",
281
+ "specializations": ["chat", "writing", "reasoning", "planning", "coding", "tool_use"],
282
+ "tier": "frontier_strong",
283
+ "params_in_billions": null,
284
+ "context_window": 1050000,
285
+ "max_output_tokens": 128000,
286
+ "input_tokens_cpm": 5,
287
+ "cached_tokens_cpm": 0.5,
288
+ "output_tokens_cpm": 30
289
+ }
290
+ ],
291
+ "default": "gpt-5.5",
292
+ "pricing_unit": "USD per 1M tokens"
293
+ }
294
+ ```
295
+
296
+ The resolver picks the cheapest model matching `model_specialization` and `model_tier`, relaxing constraints in order when nothing fits. See [docs/resolver.md](docs/resolver.md) for ranking details.
349
297
 
350
298
  ## Further reading
351
299
 
@@ -353,6 +301,7 @@ For the lowest-level entry points, `classifyOpenClassifyInput(input, { runClassi
353
301
  - [docs/manifests.md](docs/manifests.md) — manifest reference
354
302
  - [docs/resolver.md](docs/resolver.md) — aggregation and model resolution
355
303
  - [docs/adding-a-classifier.md](docs/adding-a-classifier.md) — author guide
304
+ - [CHANGELOG.md](CHANGELOG.md) — release notes
356
305
 
357
306
  ## Contributing
358
307