open-classify 0.4.0 → 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 (62) hide show
  1. package/README.md +129 -86
  2. package/dist/src/aggregator.d.ts +11 -4
  3. package/dist/src/aggregator.js +108 -121
  4. package/dist/src/classifiers/{custom/context_shift → context_shift}/manifest.json +6 -11
  5. package/dist/src/classifiers/{custom/context_shift → context_shift}/prompt.md +1 -1
  6. package/dist/src/classifiers/{custom/conversation_digest → conversation_digest}/manifest.json +7 -12
  7. package/dist/src/classifiers/{custom/conversation_digest → conversation_digest}/prompt.md +2 -2
  8. package/dist/src/classifiers/{custom/memory_retrieval_queries → memory_retrieval_queries}/manifest.json +6 -11
  9. package/dist/src/classifiers/{custom/memory_retrieval_queries → memory_retrieval_queries}/prompt.md +2 -2
  10. package/dist/src/classifiers/{stock/model_specialization → model_specialization}/manifest.json +2 -2
  11. package/dist/src/classifiers/model_specialization/prompt.md +5 -0
  12. package/dist/src/classifiers/preflight/manifest.json +34 -0
  13. package/dist/src/classifiers/preflight/prompt.md +10 -0
  14. package/dist/src/classifiers/{stock/prompt_injection → prompt_injection}/manifest.json +6 -2
  15. package/dist/src/classifiers/prompt_injection/prompt.md +14 -0
  16. package/dist/src/classifiers/{stock/routing → routing}/manifest.json +2 -2
  17. package/dist/src/classifiers/routing/prompt.md +5 -0
  18. package/dist/src/classifiers/{stock/tools → tools}/manifest.json +3 -3
  19. package/dist/src/classifiers/tools/prompt.md +5 -0
  20. package/dist/src/classifiers.js +31 -29
  21. package/dist/src/classify.d.ts +9 -2
  22. package/dist/src/classify.js +26 -12
  23. package/dist/src/config.d.ts +1 -4
  24. package/dist/src/config.js +6 -34
  25. package/dist/src/index.d.ts +1 -0
  26. package/dist/src/index.js +1 -0
  27. package/dist/src/input.d.ts +4 -1
  28. package/dist/src/input.js +12 -10
  29. package/dist/src/manifest.d.ts +11 -7
  30. package/dist/src/pipeline.d.ts +9 -1
  31. package/dist/src/pipeline.js +51 -25
  32. package/dist/src/reserved-fields.d.ts +18 -0
  33. package/dist/src/reserved-fields.js +175 -0
  34. package/dist/src/stock-prompt.d.ts +9 -2
  35. package/dist/src/stock-prompt.js +165 -45
  36. package/dist/src/stock-validation.d.ts +16 -17
  37. package/dist/src/stock-validation.js +263 -236
  38. package/dist/src/stock.d.ts +24 -60
  39. package/dist/src/stock.js +7 -14
  40. package/docs/adding-a-classifier.md +74 -32
  41. package/docs/manifests.md +112 -71
  42. package/docs/resolver.md +25 -34
  43. package/docs/signals.md +39 -58
  44. package/open-classify.config.example.json +9 -11
  45. package/package.json +1 -1
  46. package/dist/src/classifiers/stock/preflight/manifest.json +0 -11
  47. package/dist/src/classifiers/stock/prompts/classifier-header.md +0 -4
  48. package/dist/src/classifiers/stock/prompts/custom-output.md +0 -7
  49. package/dist/src/classifiers/stock/prompts/model_specialization.md +0 -7
  50. package/dist/src/classifiers/stock/prompts/preflight-output.md +0 -10
  51. package/dist/src/classifiers/stock/prompts/preflight.md +0 -47
  52. package/dist/src/classifiers/stock/prompts/prompt-injection-output.md +0 -5
  53. package/dist/src/classifiers/stock/prompts/prompt_injection.md +0 -24
  54. package/dist/src/classifiers/stock/prompts/routing-output.md +0 -5
  55. package/dist/src/classifiers/stock/prompts/routing.md +0 -9
  56. package/dist/src/classifiers/stock/prompts/specialty.md +0 -12
  57. package/dist/src/classifiers/stock/prompts/tier.md +0 -7
  58. package/dist/src/classifiers/stock/prompts/tools-output.md +0 -11
  59. package/dist/src/classifiers/stock/prompts/tools.md +0 -10
  60. /package/dist/src/classifiers/{stock/prompts → _prompts}/base.md +0 -0
  61. /package/dist/src/classifiers/{stock/prompts → _prompts}/confidence.md +0 -0
  62. /package/dist/src/classifiers/{stock/prompts → _prompts}/reason.md +0 -0
package/README.md CHANGED
@@ -6,12 +6,10 @@
6
6
  Decide what should happen to a user message <em>before</em> it reaches your downstream model.
7
7
  </p>
8
8
 
9
- Open Classify is a pre-routing layer for AI products. It runs a small set of fast classifiers in parallel against the latest user message, then tells your app one of three things: **route** it, **reply** immediately, or **block** it.
9
+ Open Classify is a pre-routing layer for AI products. It runs a small set of fast classifiers in parallel against the latest user message, then returns a single decision envelope your app can act on: a downstream model recommendation, a tool exposure list, an optional acknowledgement, and any custom signals your own classifiers contribute.
10
10
 
11
11
  Use it when your frontier model should not be the first thing every request touches. Open Classify can handle tiny terminal replies before they hit an expensive model, recommend the right downstream model for the actual task, suggest what tools or context the downstream model should receive, and add a focused prompt-injection pass.
12
12
 
13
- The result is a small, auditable decision envelope your app can act on before spending the big tokens.
14
-
15
13
  ```
16
14
  message
17
15
 
@@ -20,28 +18,28 @@ normalize + trim classifier context
20
18
 
21
19
  ├─► preflight ─────────────► final_reply? / ack_reply?
22
20
  ├─► routing ───────────────► model_tier?
23
- ├─► model_specialization ──► specialization?
21
+ ├─► model_specialization ──► model_specialization?
24
22
  ├─► tools ─────────────────► tools?
25
23
  ├─► prompt_injection ─────► risk_level?
26
- └─► custom classifiers ────► JSON-Schema output
27
- (run in parallel)
24
+ └─► your own classifiers ──► any JSON-Schema-validated payload
25
+ (all run in parallel, capped by maxConcurrency)
28
26
 
29
27
 
30
28
  aggregator + model catalog
31
29
 
32
30
 
33
- route / reply / block
31
+ route
34
32
  ```
35
33
 
36
- Stock classifiers have fixed typed signals. Custom classifiers carry their own JSON-Schema-validated payload. The aggregator merges everything, resolves a concrete model from your catalog, and short-circuits when preflight has a terminal reply or prompt injection is detected.
34
+ Every classifier uses the same manifest shape and emits the same output envelope: `{ reason, certainty, ...payload }`. Some payload fields are **reserved** like `model_tier`, `final_reply`, and `risk_level` — and the aggregator knows how to consume them into a routing decision. Everything else is your classifier's own data and passes through to the caller untouched.
37
35
 
38
36
  ## Why Open Classify
39
37
 
40
- - **Spend frontier tokens only when they matter.** Simple greetings, thanks, spelling checks, and small arithmetic can return `action: "reply"` with `reply.text` and skip downstream work entirely.
41
- - **Keep the user interface responsive.** For complex work, preflight can return an `ack_reply` while your app routes the request to the real worker.
38
+ - **Spend frontier tokens only when they matter.** Simple greetings, thanks, spelling checks, and small arithmetic can be answered immediately via `audit.final_reply` without sending the request downstream.
39
+ - **Keep the user interface responsive.** For complex work, preflight can suggest an `ack_reply` while your app routes the request to the real worker.
42
40
  - **Pick the right model per message.** Classifiers emit soft constraints like tier and specialization; your catalog turns those into a concrete model optimized for cost, capability, and fit.
43
41
  - **Shape downstream context intentionally.** Built-in and custom classifiers can recommend tools, retrieval queries, summaries, or other context hints without passing the full conversation history back to the caller.
44
- - **Add another defensive layer.** The `prompt_injection` classifier can block instruction override attempts like “forget previous instructions” without treating ordinary tool requests as injection.
42
+ - **Add another defensive layer.** The `prompt_injection` classifier surfaces instruction-override attempts so your app can decide whether to continue.
45
43
 
46
44
  ## Install
47
45
 
@@ -56,7 +54,7 @@ Node 18+. The packaged runner is local Ollama and ships with `gemma4:e4b-it-q4_K
56
54
  ```ts
57
55
  import { createClassifier } from "open-classify";
58
56
 
59
- const classify = createClassifier();
57
+ const { classify, inspect } = createClassifier();
60
58
 
61
59
  const result = await classify({
62
60
  messages: [
@@ -64,30 +62,49 @@ const result = await classify({
64
62
  ],
65
63
  });
66
64
 
67
- if (result.action === "route") {
68
- // result.downstream.model_id is a concrete model from your catalog.
69
- // result.downstream.tools is the recommended tool exposure.
70
- // result.classifier_outputs holds any custom classifier payloads.
71
- }
65
+ // result.action is always "route". Use the audit envelope and the per-classifier
66
+ // outputs to decide what to do next.
67
+ const { model_id, target_message, tools } = result.downstream;
68
+ const ackReply = result.audit.ack_reply?.text;
69
+ const queries = result.classifier_outputs.memory_retrieval_queries?.queries;
72
70
  ```
73
71
 
74
- `createClassifier` builds the runner and loads the model catalog once. Reuse the returned `classify` function across your app — every call is a plain function invocation, no re-initialization.
72
+ `createClassifier` builds the runner and loads the model catalog once. Reuse the returned `classify` and `inspect` functions across your app — every call is a plain function invocation, no re-initialization.
75
73
 
76
- ## What you get back
74
+ ### Classifying assistant output
77
75
 
78
- Every call returns a `PipelineResult` with one of three `action` values:
76
+ `inspect()` is a lean second pass for the **assistant's reply**. It only runs classifiers tagged `applies_to: "both"` (or `"assistant"`) in their manifest, and returns just the per-classifier outputs — no routing, no model resolution, no audit envelope.
79
77
 
80
- | `action` | When | Key fields |
81
- |---|---|---|
82
- | `route` | Default — downstream work should continue | `downstream.{model_id, target_message, tools}`, `audit.ack_reply?` |
83
- | `reply` | Preflight had a tiny terminal reply | `reply.text` |
84
- | `block` | Prompt injection flagged confident `high_risk` / `unknown`, or the certainty gate fired | `reason.kind` plus prompt-injection or low-certainty details |
78
+ ```ts
79
+ const reply = await inspect({
80
+ messages: [
81
+ { role: "user", text: "Summarize the contract." },
82
+ { role: "assistant", text: "The contract has three notable risks…" },
83
+ ],
84
+ });
85
+
86
+ const risk = reply.classifier_outputs.prompt_injection?.risk_level;
87
+ ```
88
+
89
+ Use it for things like prompt-injection checks on model output, summarized slugs, or any classifier you want to apply post-hoc. The built-in `prompt_injection` classifier ships tagged `"both"`, so it runs in both passes; everything else is `"user"` by default. Tag your own classifiers with `applies_to` in their manifest to opt into either side.
85
90
 
86
- All three also carry `target_message_hash` (the stable 8-hex fingerprint of the target message), `classifier_outputs` (custom classifier payloads, keyed by name), and an `audit` block. Route results include the downstream target message, not the caller's message history. Short-circuit results include the firing classifier's audit context.
91
+ ## What you get back
87
92
 
88
- For complex requests, look for `audit.ack_reply` on `route` results. It is the immediate acknowledgement your UI can show while the downstream model works. For trivial requests, `result.reply.text` is the complete response and no downstream model is needed.
93
+ Every call returns a `PipelineResult`:
89
94
 
90
- Example `route` result:
95
+ | Field | What it is |
96
+ |---|---|
97
+ | `action` | Always `"route"` — the worker pool always runs every classifier and returns aggregated results |
98
+ | `target_message_hash` | Stable 8-hex fingerprint of the target message |
99
+ | `downstream.model_id` | Concrete model id chosen from your catalog |
100
+ | `downstream.target_message` | The sanitized target message that should be sent downstream |
101
+ | `downstream.tools` | The recommended tool exposure (may be `{ tools: [] }`) |
102
+ | `classifier_outputs[name]` | Each classifier's payload (reserved + custom fields) with `reason` and `certainty` stripped |
103
+ | `audit` | The full envelope: reserved-field slots, every classifier's full output, model resolution details, and run metadata |
104
+
105
+ For complex requests, look for `audit.ack_reply` — that's the immediate acknowledgement your UI can show while the downstream model works. For trivial requests, `audit.final_reply.text` is a tiny terminal answer your app can return directly without ever calling the downstream model. The pipeline never decides for you; the caller chooses what to act on.
106
+
107
+ Example result:
91
108
 
92
109
  ```json
93
110
  {
@@ -99,19 +116,21 @@ Example `route` result:
99
116
  "target_message": { "role": "user", "text": "...", "hash": "b11d5268" }
100
117
  },
101
118
  "classifier_outputs": {
119
+ "routing": { "model_tier": "frontier_strong" },
120
+ "model_specialization": { "model_specialization": "coding" },
121
+ "tools": { "tools": ["workspace"] },
122
+ "prompt_injection": { "risk_level": "normal" },
102
123
  "memory_retrieval_queries": { "queries": ["user code review preferences"] }
103
124
  },
104
125
  "audit": {
105
126
  "ack_reply": { "text": "Let me check." },
106
- "routing": { "model_tier": "frontier_strong" },
107
- "model_specialization": { "specialization": "coding" },
127
+ "routing": { "model_tier": "frontier_strong", "model_specialization": "coding" },
108
128
  "tools": { "tools": ["workspace"] },
129
+ "prompt_injection": { "risk_level": "normal" },
130
+ "classifier_outputs": [ /* every classifier's full output, with reason + certainty */ ],
109
131
  "model_recommendation": {
110
132
  "id": "gpt-5.5",
111
133
  "context_window": 1050000,
112
- "input_tokens_cpm": 5,
113
- "cached_tokens_cpm": 0.5,
114
- "output_tokens_cpm": 30,
115
134
  "resolution": { "...": "..." }
116
135
  },
117
136
  "meta": { "classifiers": { "...": "..." } }
@@ -119,65 +138,96 @@ Example `route` result:
119
138
  }
120
139
  ```
121
140
 
122
- ## Stock classifiers
123
-
124
- Stock classifiers are built in and have fixed, typed output shapes. Each one owns exactly one signal. Its manifest lives in `src/classifiers/stock/<name>/manifest.json`; the shared stock prompt building blocks live in `src/classifiers/stock/prompts/`.
125
-
126
- Every classifier prompt includes a shared header with its `Classifier` name, `Purpose`, and an instruction to treat that purpose as a hard scope boundary. In practice:
141
+ ## Classifier model
127
142
 
128
- - `routing` chooses only `model_tier`
129
- - `model_specialization` chooses only `specialization`
130
- - `prompt_injection` is only for prompt injection, not harmfulness, authorization, contradiction, feasibility, or freshness checks
143
+ Open Classify ships with eight built-in classifiers; all use the same manifest shape. There is no distinction between "stock" and "custom" — the runtime only cares about which **reserved fields** a classifier declares.
131
144
 
132
- | Name | Signal | Short-circuits? |
145
+ | Name | Reserved fields | What the aggregator does with it |
133
146
  |---|---|---|
134
- | `preflight` | `final_reply?` / `ack_reply?` | `final_reply` `reply` |
135
- | `routing` | `model_tier?` | no |
136
- | `model_specialization` | `specialization?` | no |
137
- | `tools` | `{ tools[] }` | no |
138
- | `prompt_injection` | `{ risk_level }` | confident `high_risk` or `unknown` → `block` |
147
+ | `preflight` | `final_reply`, `ack_reply` | Surfaces the highest-certainty reply in `audit.final_reply` / `audit.ack_reply` |
148
+ | `routing` | `model_tier` | Feeds the catalog resolver as a soft constraint |
149
+ | `model_specialization` | `model_specialization` | Feeds the catalog resolver as a soft constraint |
150
+ | `tools` | `tools` | Sets `downstream.tools` |
151
+ | `prompt_injection` | `risk_level` | Surfaces in `audit.prompt_injection` |
152
+ | `memory_retrieval_queries` | — | Passes through to `classifier_outputs.memory_retrieval_queries` |
153
+ | `conversation_digest` | — | Passes through |
154
+ | `context_shift` | — | Passes through |
139
155
 
140
- Each output must carry `reason` (≤120 chars) and `certainty` (`no_signal` through `near_certain`). The aggregator maps certainty tags to numeric scores and drops below-threshold signals; the default threshold is `0.65`.
156
+ Reserved fields are well-known output keys with canonical JSON Schemas and prompt fragments baked into the runtime. When you declare one in your manifest, you don't have to redeclare its enum values or shape the runtime injects them.
141
157
 
142
- ## Custom classifiers
158
+ ## Adding a classifier
143
159
 
144
- A custom classifier is two files in `src/classifiers/custom/<name>/`:
160
+ Every classifier is two files in `src/classifiers/<name>/`:
145
161
 
146
- `manifest.json`:
162
+ `manifest.json` describes the output shape and a fallback for when the classifier errors:
147
163
 
148
164
  ```json
149
165
  {
150
- "kind": "custom",
151
- "name": "memory_retrieval_queries",
166
+ "name": "topic_tags",
152
167
  "version": "1.0.0",
153
- "purpose": "Generate retrieval queries likely to surface helpful user-specific context for the downstream model.",
154
- "order": 60,
155
- "fallback": {
156
- "reason": "Classifier failed; no memory queries generated.",
157
- "certainty": "no_signal",
158
- "output": { "queries": [] }
159
- },
168
+ "purpose": "Tag the message with a small set of topic labels for analytics.",
169
+ "dispatch_order": 70,
160
170
  "output_schema": {
161
- "type": "object",
162
- "additionalProperties": false,
163
- "required": ["queries"],
171
+ "required": ["tags"],
164
172
  "properties": {
165
- "queries": {
173
+ "tags": {
166
174
  "type": "array", "maxItems": 5,
167
- "items": { "type": "string", "minLength": 1, "maxLength": 120 }
175
+ "items": { "type": "string", "minLength": 1, "maxLength": 40 }
168
176
  }
169
177
  }
178
+ },
179
+ "fallback": {
180
+ "reason": "Classifier failed; no tags generated.",
181
+ "certainty": "no_signal",
182
+ "tags": []
170
183
  }
171
184
  }
172
185
  ```
173
186
 
174
- `prompt.md`: your classifier-specific instructions.
187
+ `prompt.md` describes the classification rule in plain language. You don't need to write JSON examples — the runtime synthesizes one from your schema and shows it to the model — and you don't paste enum values for reserved fields. Just describe what each output key means and when to abstain:
188
+
189
+ ```markdown
190
+ You are the topic_tags classifier.
191
+
192
+ `tags` are short single-word topic labels (lowercase, no spaces). Use at most five.
193
+ Return an empty array when no clear topic applies.
194
+ Do not invent tags for vague or ambiguous messages.
195
+ ```
196
+
197
+ Rules:
198
+
199
+ - `name` must match the directory name.
200
+ - Reserved field names cannot appear in `output_schema.properties` — declare them in `reserved_fields` instead.
201
+ - `fallback` must validate against the composed schema; reserved fields are optional in fallback since "I failed" means "no signal."
202
+ - If you want hand-picked examples (preflight does this), add an `output_schema.examples` array. Each entry must validate against the composed schema at load time. Otherwise the runtime synthesizes a skeleton example for you.
175
203
 
176
- Custom classifiers receive the same shared `Classifier` + `Purpose` header and the same scope-boundary instruction, so keep the manifest `purpose` specific and operational.
204
+ Consume your output:
205
+
206
+ ```ts
207
+ const result = await classify(input);
208
+ const tags = result.classifier_outputs.topic_tags?.tags ?? [];
209
+ ```
177
210
 
178
- The runtime auto-discovers it, validates outputs against your schema, and surfaces them on `result.classifier_outputs.<name>`. No TypeScript edits required.
211
+ See [docs/adding-a-classifier.md](docs/adding-a-classifier.md) for a full walkthrough and [docs/manifests.md](docs/manifests.md) for the field reference.
179
212
 
180
- See [docs/adding-a-classifier.md](docs/adding-a-classifier.md) for a full walkthrough.
213
+ ## Using reserved fields in your own classifier
214
+
215
+ Any classifier can emit reserved fields. If you write your own `task_router` that emits `model_tier`, the aggregator will fold it into the model resolution alongside the built-in `routing` classifier — highest-certainty contributor wins, ties broken by manifest `dispatch_order` ascending.
216
+
217
+ ```json
218
+ {
219
+ "name": "task_router",
220
+ "version": "1.0.0",
221
+ "purpose": "Pick the downstream model tier and specialization for code-heavy tasks.",
222
+ "dispatch_order": 25,
223
+ "reserved_fields": ["model_tier", "model_specialization"],
224
+ "fallback": { "reason": "Classifier failed.", "certainty": "no_signal" }
225
+ }
226
+ ```
227
+
228
+ The runtime injects canonical sub-schemas and prompt fragments for each declared reserved field — the model is told the exact enum values it may emit. You don't paste enum values into `prompt.md`, and you don't have to hand-write a JSON example; the runtime synthesizes one from the schema and shows it to the model.
229
+
230
+ 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.
181
231
 
182
232
  ## Model catalog
183
233
 
@@ -212,9 +262,7 @@ Classifiers never emit model ids. They emit constraints; your catalog maps const
212
262
  }
213
263
  ```
214
264
 
215
- OpenAI's GPT-5.5 model page lists text and image input, text output, a 1,050,000-token context window, 128,000 max output tokens, and text-token pricing of $5.00 input, $0.50 cached input, and $30.00 output per 1M tokens. OpenAI does not publish parameter counts, so use `null` for `params_in_billions`. See the [GPT-5.5 model details](https://developers.openai.com/api/docs/models/gpt-5.5) for current pricing and capability details.
216
-
217
- The resolver picks the cheapest model matching `specialization` and `tier`, relaxing constraints in order when nothing fits, and reports what it dropped on `audit.model_recommendation.resolution`. See [docs/resolver.md](docs/resolver.md) for ranking details.
265
+ The resolver picks the cheapest model matching `model_specialization` and `model_tier`, relaxing constraints in order when nothing fits, and reports what it dropped on `audit.model_recommendation.resolution`. See [docs/resolver.md](docs/resolver.md) for ranking details.
218
266
 
219
267
  ## Input contract
220
268
 
@@ -244,24 +292,19 @@ cp open-classify.config.example.json open-classify.config.json
244
292
  "provider": "ollama",
245
293
  "defaultModel": "gemma4:e4b-it-q4_K_M",
246
294
  "models": {
247
- "stock": {
248
- "routing": "qwen2.5:7b-instruct-q4_K_M",
249
- "prompt_injection": "llama-guard3:8b"
250
- },
251
- "custom": {
252
- "memory_retrieval_queries": "qwen2.5:7b-instruct-q4_K_M"
253
- }
295
+ "routing": "qwen2.5:7b-instruct-q4_K_M",
296
+ "prompt_injection": "llama-guard3:8b",
297
+ "memory_retrieval_queries": "qwen2.5:7b-instruct-q4_K_M"
254
298
  }
255
299
  },
256
300
  "aggregator": {
257
- "certaintyThreshold": 0.65,
258
- "certaintyGate": "min_score"
301
+ "certaintyThreshold": 0.65
259
302
  },
260
303
  "catalog": "downstream-models.json"
261
304
  }
262
305
  ```
263
306
 
264
- `runner.provider` currently supports `"ollama"` only. `runner.defaultModel` applies to any classifier without an explicit entry. `runner.models.stock` configures built-in classifiers; `runner.models.custom` configures custom classifiers by manifest name. `aggregator.certaintyGate` can be `"min_score"` (lowest score across all stock and custom classifiers), `"avg_score"`, or `"off"`. The setup script and `loadOpenClassifyConfig()` read `open-classify.config.json`, or `OPEN_CLASSIFY_CONFIG` when you want a different path.
307
+ `runner.provider` currently supports `"ollama"` only. `runner.defaultModel` applies to any classifier without an explicit `runner.models` entry. `runner.models` is a flat map keyed by classifier name.
265
308
 
266
309
  ## Bring your own backend
267
310
 
@@ -284,15 +327,15 @@ const runClassifier: RunClassifier = async (name, input, signal) => {
284
327
  // call your provider of choice, return a ClassifierOutput
285
328
  };
286
329
 
287
- const classify = createClassifier({ runClassifier });
330
+ const { classify, inspect } = createClassifier({ runClassifier });
288
331
  ```
289
332
 
290
- For the lowest-level entry point, `classifyOpenClassifyInput(input, { runClassifier, catalog })` skips the factory entirely.
333
+ For the lowest-level entry points, `classifyOpenClassifyInput(input, { runClassifier, catalog })` and `inspectOpenClassifyInput(input, { runClassifier })` skip the factory entirely.
291
334
 
292
335
  ## Further reading
293
336
 
294
- - [docs/signals.md](docs/signals.md) — full signal contracts and validation rules
295
- - [docs/manifests.md](docs/manifests.md) — manifest reference (stock and custom)
337
+ - [docs/signals.md](docs/signals.md) — reserved field reference
338
+ - [docs/manifests.md](docs/manifests.md) — manifest reference
296
339
  - [docs/resolver.md](docs/resolver.md) — aggregation and model resolution
297
340
  - [docs/adding-a-classifier.md](docs/adding-a-classifier.md) — author guide
298
341
 
@@ -1,5 +1,6 @@
1
1
  import type { AggregatorConfig, Catalog, ClassifierRegistry, ClassifierResults, Envelope, ModelRecommendation, ModelRecommendationResolution } from "./manifest.js";
2
- import type { AckReplySignal, ModelSpecializationClassifierOutput, FinalReplySignal, RoutingClassifierOutput, RoutingSignal } from "./stock.js";
2
+ import type { AckReplySignal, Certainty, FinalReplySignal, RoutingSignal, ToolsSignal } from "./stock.js";
3
+ import type { DownstreamModelTier, ModelSpecialization } from "./enums.js";
3
4
  import type { ClassifierInput } from "./types.js";
4
5
  export declare const DEFAULT_CERTAINTY_THRESHOLD = 0.65;
5
6
  /** @deprecated Use DEFAULT_CERTAINTY_THRESHOLD. */
@@ -15,7 +16,13 @@ export declare function composeEnvelope(args: ComposeEnvelopeArgs): Envelope;
15
16
  export declare function certaintyThreshold(config: AggregatorConfig | undefined): number;
16
17
  export declare function resolveModelFromRouting(routing: RoutingSignal | undefined, catalog: Catalog, confidence: number | undefined, ignoredConstraints?: ModelRecommendationResolution["constraints_dropped"]): ModelRecommendation;
17
18
  export declare function resolveModel(results: Readonly<{
18
- routing?: RoutingClassifierOutput;
19
- model_specialization?: ModelSpecializationClassifierOutput;
19
+ routing?: {
20
+ model_tier?: DownstreamModelTier;
21
+ certainty?: Certainty;
22
+ };
23
+ model_specialization?: {
24
+ model_specialization?: ModelSpecialization;
25
+ certainty?: Certainty;
26
+ };
20
27
  }>, catalog: Catalog, threshold: number): ModelRecommendation;
21
- export type { FinalReplySignal, AckReplySignal };
28
+ export type { FinalReplySignal, AckReplySignal, ToolsSignal };