open-classify 0.5.0 → 0.7.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/README.md +96 -88
- package/bin/open-classify.mjs +201 -0
- package/dist/src/aggregator.d.ts +7 -23
- package/dist/src/aggregator.js +108 -186
- package/dist/src/classifiers/{routing → model_tier}/manifest.json +2 -2
- package/dist/src/classifiers/{routing → model_tier}/prompt.md +1 -1
- package/dist/src/classifiers/preflight/manifest.json +9 -8
- package/dist/src/classifiers/preflight/prompt.md +12 -6
- package/dist/src/classifiers/prompt_injection/manifest.json +2 -3
- package/dist/src/classifiers.d.ts +12 -5
- package/dist/src/classifiers.js +32 -16
- package/dist/src/classify.d.ts +5 -3
- package/dist/src/classify.js +28 -8
- package/dist/src/config.d.ts +1 -3
- package/dist/src/config.js +1 -28
- package/dist/src/index.js +2 -3
- package/dist/src/manifest.d.ts +25 -70
- package/dist/src/ollama.d.ts +5 -6
- package/dist/src/ollama.js +17 -11
- package/dist/src/pipeline.d.ts +3 -2
- package/dist/src/pipeline.js +32 -94
- package/dist/src/stock-validation.js +8 -4
- package/docs/adding-a-classifier.md +50 -27
- package/docs/manifests.md +6 -6
- package/docs/resolver.md +20 -44
- package/docs/signals.md +18 -8
- package/open-classify.config.example.json +2 -7
- package/package.json +6 -1
- /package/{dist/src/classifiers → templates}/context_shift/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/context_shift/prompt.md +0 -0
- /package/{dist/src/classifiers → templates}/conversation_digest/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/conversation_digest/prompt.md +0 -0
- /package/{dist/src/classifiers → templates}/memory_retrieval_queries/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/memory_retrieval_queries/prompt.md +0 -0
- /package/{dist/src/classifiers → templates}/tools/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/tools/prompt.md +0 -0
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
# Adding a classifier
|
|
2
2
|
|
|
3
|
-
Every classifier —
|
|
3
|
+
Every classifier — bundled or your own — uses the same two-file layout. There is no separate "stock" vs "custom" distinction; the runtime only cares about which reserved fields a classifier opts into.
|
|
4
|
+
|
|
5
|
+
There are two places a classifier can live:
|
|
6
|
+
|
|
7
|
+
- **In your own app**, in a directory you pass to `extraClassifierDirs` (almost always `./classifiers/` after `npx open-classify init`). This is the right path when you've installed Open Classify as a dependency.
|
|
8
|
+
- **In this repo**, under `src/classifiers/<name>/`. Only do this when you're contributing a new mandatory built-in back to Open Classify.
|
|
9
|
+
|
|
10
|
+
Either way, the layout and contract are identical.
|
|
4
11
|
|
|
5
12
|
## 1. Create the directory
|
|
6
13
|
|
|
7
14
|
```
|
|
8
|
-
|
|
15
|
+
classifiers/<name>/
|
|
9
16
|
├── manifest.json
|
|
10
17
|
└── prompt.md
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
The directory name must match `manifest.json`'s `name` field.
|
|
20
|
+
The directory name must match `manifest.json`'s `name` field. Directories starting with `_` are skipped by the loader — that's the deactivation mechanism (`_topic_tags/` is inert; rename to `topic_tags/` to activate).
|
|
14
21
|
|
|
15
22
|
## 2. Write the manifest
|
|
16
23
|
|
|
17
|
-
Minimal example — a pure-custom classifier that emits tags.
|
|
24
|
+
Minimal example — a pure-custom classifier that emits tags. The runtime synthesizes a JSON example from your schema, so you don't need to write one.
|
|
18
25
|
|
|
19
26
|
```json
|
|
20
27
|
{
|
|
@@ -39,8 +46,6 @@ Minimal example — a pure-custom classifier that emits tags. You don't need to
|
|
|
39
46
|
}
|
|
40
47
|
```
|
|
41
48
|
|
|
42
|
-
If your classifier's behavior is nuanced enough that hand-picked examples would help the model (preflight is one), add an `output_schema.examples` array. The runtime validates each example against the composed schema at load time, so a broken example fails the build.
|
|
43
|
-
|
|
44
49
|
To also influence routing, opt into a reserved field:
|
|
45
50
|
|
|
46
51
|
```json
|
|
@@ -71,8 +76,9 @@ Rules:
|
|
|
71
76
|
- `name` must match the directory name.
|
|
72
77
|
- Reserved field names cannot appear in `output_schema.properties`; declare them in `reserved_fields` instead.
|
|
73
78
|
- `reason` and `certainty` are added to the composed schema by the runtime — don't declare them.
|
|
74
|
-
- `fallback` must validate against the composed schema.
|
|
79
|
+
- `fallback` must validate against the composed schema. Only `reason` and `certainty` are required in fallback; reserved fields and `output_schema.required` fields are exempt (a "no signal" fallback usually omits them).
|
|
75
80
|
- `output_schema.examples` (JSON Schema standard) must validate against the composed schema at load time, so a broken example fails the build, not the model call.
|
|
81
|
+
- **Name collisions throw.** Extras cannot override the mandatory built-ins (`preflight`, `model_tier`, `model_specialization`, `prompt_injection`). To customize one of those, use a custom `RunClassifier` to intercept it (see "Replacing the backend" below).
|
|
76
82
|
|
|
77
83
|
See [manifests.md](manifests.md) for the full field list.
|
|
78
84
|
|
|
@@ -90,24 +96,37 @@ Do not invent tags for vague or ambiguous messages.
|
|
|
90
96
|
|
|
91
97
|
Don't paste enum values for reserved fields — the runtime injects them with canonical wording so they never drift from `src/enums.ts`.
|
|
92
98
|
|
|
93
|
-
## 4.
|
|
99
|
+
## 4. Use it
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
npm run build # validates the manifest, composes the schema, copies assets
|
|
97
|
-
npm test
|
|
98
|
-
```
|
|
101
|
+
After `npx open-classify init`, your `classifiers/` directory already exists. Drop your folder in and point `createClassifier` at the parent dir:
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
```ts
|
|
104
|
+
import { createClassifier } from "open-classify";
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
const { classify } = createClassifier({
|
|
107
|
+
extraClassifierDirs: ["./classifiers"],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await classify({
|
|
111
|
+
messages: [{ role: "user", text: "Can you review the attached contract?" }],
|
|
112
|
+
});
|
|
103
113
|
|
|
104
|
-
```ts
|
|
105
|
-
const { classify } = createClassifier({ catalog });
|
|
106
|
-
const result = await classify(input);
|
|
107
114
|
const tags = result.classifier_outputs.topic_tags?.tags ?? [];
|
|
108
115
|
```
|
|
109
116
|
|
|
110
|
-
`
|
|
117
|
+
> Production tip: `"./classifiers"` resolves against `process.cwd()`, which is fine for `npm start` but breaks if the process launches from a different directory. For long-running services, resolve absolutely via `fileURLToPath(import.meta.url) + path.resolve(...)`.
|
|
118
|
+
|
|
119
|
+
If the manifest is malformed, `createClassifier` throws `ClassifierManifestError` at startup with the path and a specific reason — typos fail loud.
|
|
120
|
+
|
|
121
|
+
## Activating one of the bundled templates
|
|
122
|
+
|
|
123
|
+
`npx open-classify init` copies four templates (`tools`, `memory_retrieval_queries`, `conversation_digest`, `context_shift`) into your `classifiers/` directory as `_<name>/` — inactive because of the underscore prefix. To turn one on:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
mv classifiers/_tools classifiers/tools
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Edit `manifest.json` first if you need to (`tools` in particular ships with an opinionated `allowed_tools` list you'll almost certainly want to tailor). The reverse works on any classifier: rename `<name>/` → `_<name>/` to deactivate without deleting.
|
|
111
130
|
|
|
112
131
|
## Targeting the assistant response
|
|
113
132
|
|
|
@@ -117,7 +136,7 @@ Classifiers run against the user message by default. To run a classifier against
|
|
|
117
136
|
- `"assistant"` — only `inspect()` runs it.
|
|
118
137
|
- `"both"` — both passes run it.
|
|
119
138
|
|
|
120
|
-
Use `inspect()` from `createClassifier()` for the assistant-side pass. It returns a lean shape
|
|
139
|
+
Use `inspect()` from `createClassifier()` for the assistant-side pass. It returns a lean shape: `target_message_hash`, the `message` that was inspected, and `classifier_outputs`. No routing, no action, no block logic.
|
|
121
140
|
|
|
122
141
|
```ts
|
|
123
142
|
const { inspect } = createClassifier({ catalog });
|
|
@@ -130,9 +149,11 @@ const post = await inspect({
|
|
|
130
149
|
const risk = post.classifier_outputs.prompt_injection?.risk_level;
|
|
131
150
|
```
|
|
132
151
|
|
|
152
|
+
The built-in `prompt_injection` ships tagged `"both"` so it runs on both sides.
|
|
153
|
+
|
|
133
154
|
## Choosing the classifier model
|
|
134
155
|
|
|
135
|
-
|
|
156
|
+
In `open-classify.config.json`:
|
|
136
157
|
|
|
137
158
|
```json
|
|
138
159
|
{
|
|
@@ -146,9 +167,9 @@ For apps and OSS installs, prefer `open-classify.config.json`:
|
|
|
146
167
|
}
|
|
147
168
|
```
|
|
148
169
|
|
|
149
|
-
`runner.defaultModel` applies to every classifier without an override. `runner.models` is a flat map keyed by classifier name —
|
|
170
|
+
`runner.defaultModel` applies to every classifier without an override. `runner.models` is a flat map keyed by classifier name — works for built-ins, templates, and your own.
|
|
150
171
|
|
|
151
|
-
Classifier manifests may also carry an Ollama hint
|
|
172
|
+
Classifier manifests may also carry an Ollama hint:
|
|
152
173
|
|
|
153
174
|
```json
|
|
154
175
|
{
|
|
@@ -160,15 +181,17 @@ Config file and function options take precedence over manifest hints.
|
|
|
160
181
|
|
|
161
182
|
## Replacing the backend
|
|
162
183
|
|
|
163
|
-
For full backend control
|
|
184
|
+
For full backend control — including replacing a mandatory built-in like `preflight` — implement your own `RunClassifier` and pass it to `createClassifier`:
|
|
164
185
|
|
|
165
186
|
```ts
|
|
166
|
-
import {
|
|
187
|
+
import { createClassifier, type RunClassifier } from "open-classify";
|
|
167
188
|
|
|
168
189
|
const runClassifier: RunClassifier = async (name, input, signal) => {
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
if (name === "preflight") {
|
|
191
|
+
// call OpenAI / Anthropic / your own logic; return a ClassifierOutput.
|
|
192
|
+
}
|
|
193
|
+
// …handle other classifiers, or delegate to the Ollama runner you imported.
|
|
171
194
|
};
|
|
172
195
|
|
|
173
|
-
|
|
196
|
+
const { classify } = createClassifier({ runClassifier });
|
|
174
197
|
```
|
package/docs/manifests.md
CHANGED
|
@@ -17,7 +17,7 @@ The loader skips any top-level directory whose name starts with `_` (those are s
|
|
|
17
17
|
| Field | Required | Description |
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| `name` | yes | Classifier id. Must match the directory name. |
|
|
20
|
-
| `version` | yes | Contract version
|
|
20
|
+
| `version` | yes | Contract version string for this classifier. |
|
|
21
21
|
| `purpose` | yes | Human-readable description of the classifier's job. Treated as a hard scope boundary in the prompt. |
|
|
22
22
|
| `dispatch_order` | no | Non-negative integer scheduling priority. Lower runs first. Omit to schedule this classifier last (treated as +Infinity). Duplicate names are rejected; duplicate dispatch_orders are allowed and schedule adjacent. |
|
|
23
23
|
| `applies_to` | no | One of `"user"`, `"assistant"`, `"both"`. Controls which pipeline pass the classifier participates in: `classify()` runs `"user"` + `"both"`; `inspect()` runs `"assistant"` + `"both"`. Defaults to `"user"`. |
|
|
@@ -34,12 +34,12 @@ Reserved fields are well-known output keys the aggregator knows how to consume.
|
|
|
34
34
|
|
|
35
35
|
| Reserved field | Shape | What the aggregator does with it |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `final_reply` | `{ text: string ≤200 chars }` |
|
|
38
|
-
| `ack_reply` | `{ text: string ≤200 chars }` |
|
|
37
|
+
| `final_reply` | `{ text: string ≤200 chars }` | Sets `result.action = "reply"` and `result.reply`; caller returns it as the terminal reply |
|
|
38
|
+
| `ack_reply` | `{ text: string ≤200 chars }` | Sets `result.reply` (when action is `"route"`); caller shows it as an acknowledgement while downstream works |
|
|
39
39
|
| `model_tier` | one of `DOWNSTREAM_MODEL_TIER_VALUES` | Soft constraint for catalog resolver |
|
|
40
40
|
| `model_specialization` | one of `MODEL_SPECIALIZATION_VALUES` | Soft constraint for catalog resolver |
|
|
41
|
-
| `tools` | array of allowed tool ids | Sets `
|
|
42
|
-
| `risk_level` | one of `PROMPT_INJECTION_RISK_LEVEL_VALUES` | Surfaced in `
|
|
41
|
+
| `tools` | array of allowed tool ids | Sets `result.tools` |
|
|
42
|
+
| `risk_level` | one of `PROMPT_INJECTION_RISK_LEVEL_VALUES` | Surfaced in `result.prompt_injection`; `"high_risk"` or `"unknown"` triggers `action: "block"` |
|
|
43
43
|
|
|
44
44
|
`final_reply` and `ack_reply` are mutually exclusive — a single output may contain at most one.
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ When multiple classifiers emit the same reserved field, the highest-certainty co
|
|
|
49
49
|
|
|
50
50
|
```json
|
|
51
51
|
{
|
|
52
|
-
"name": "
|
|
52
|
+
"name": "model_tier",
|
|
53
53
|
"version": "1.0.0",
|
|
54
54
|
"purpose": "Recommend the downstream model tier.",
|
|
55
55
|
"dispatch_order": 20,
|
package/docs/resolver.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# Aggregation and model resolution
|
|
2
2
|
|
|
3
|
-
The aggregator merges classifier outputs into
|
|
3
|
+
The aggregator merges classifier outputs into a `PipelineResult` with a flat shape — no nested `audit` or `downstream` envelope.
|
|
4
4
|
|
|
5
|
-
## Certainty
|
|
5
|
+
## Certainty labels
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Per-classifier outputs carry `certainty` tags. The aggregator maps tags to scores:
|
|
7
|
+
Classifier outputs carry a `certainty` label. The aggregator maps labels to numeric scores for comparison and reporting:
|
|
10
8
|
|
|
11
9
|
```ts
|
|
12
10
|
{
|
|
@@ -21,23 +19,27 @@ Per-classifier outputs carry `certainty` tags. The aggregator maps tags to score
|
|
|
21
19
|
}
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
Labels stay in classifier prompts (the model understands them as semantic grades). Floats appear only in the final `PipelineResult` fields: `avg_certainty`, `min_certainty`, and `classifier_outputs[name].certainty`.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
## Reserved-field merging
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
When multiple classifiers emit the same reserved field, the aggregator picks the highest-certainty contributor. Ties are broken by manifest `dispatch_order` ascending (first wins). Classifiers without `dispatch_order` sort last for tie-break purposes.
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
There is no certainty threshold gate — the highest-certainty value always wins, regardless of score. Values below any particular threshold are still reported in `classifier_outputs` for the caller to inspect.
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
## Action
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
Every result has `action: "route" | "block" | "reply"`.
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
**`"reply"`** — `preflight` emitted `final_reply`. The classifier determined it can answer the message immediately; no downstream model is needed. `result.reply` contains the text. All other classifiers still ran.
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
**`"block"`** — something prevented routing. `result.block_reason` names the cause:
|
|
37
|
+
- `"prompt_injection"` — `risk_level` is `"high_risk"` or `"unknown"`, regardless of certainty. This takes priority over other causes.
|
|
38
|
+
- `"classification_error"` — one or more classifiers failed or timed out, or preflight provided no reply (which means the pipeline cannot fulfill its reply contract), or no model could be resolved.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
**`"route"`** — all classifiers succeeded and `result.model_id` names the downstream model to call.
|
|
41
|
+
|
|
42
|
+
Even on `"block"`, `model_id` and `reply` are populated when they can be (the caller may want to store them). `failed_classifiers` lists every classifier that errored or timed out.
|
|
41
43
|
|
|
42
44
|
## Model resolution
|
|
43
45
|
|
|
@@ -60,36 +62,10 @@ Within a pass, candidates are ranked:
|
|
|
60
62
|
3. larger `context_window`
|
|
61
63
|
4. earlier catalog order
|
|
62
64
|
|
|
63
|
-
If every pass returns no candidates, the resolver
|
|
64
|
-
|
|
65
|
-
## Resolution audit
|
|
65
|
+
If every pass returns no candidates, the resolver uses `catalog.default`. In practice the no-constraints pass always finds at least one model unless the catalog is empty, so the default-fallback path is defensive.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
{
|
|
71
|
-
constraints_used: { model_specialization?: ..., model_tier?: ... },
|
|
72
|
-
constraints_dropped: Array<{
|
|
73
|
-
axis: "model_specialization" | "model_tier",
|
|
74
|
-
reason: "low_confidence" | "no_match_relaxed" | "default_fallback"
|
|
75
|
-
}>,
|
|
76
|
-
confidences: { routing?: number },
|
|
77
|
-
fell_back_to_default: boolean,
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Drop reasons:
|
|
82
|
-
|
|
83
|
-
- `low_confidence` — a classifier emitted the axis but its certainty was below threshold.
|
|
84
|
-
- `no_match_relaxed` — the axis was requested but no model matched, so the resolver relaxed it.
|
|
85
|
-
- `default_fallback` — every pass failed; the resolver used `catalog.default`.
|
|
86
|
-
|
|
87
|
-
## Audit envelope
|
|
67
|
+
## Whole-run certainty summary
|
|
88
68
|
|
|
89
|
-
|
|
69
|
+
Every run includes `avg_certainty` and `min_certainty` at the top level of `PipelineResult`. These are the arithmetic mean and minimum certainty scores across all classifiers, including failed classifiers that fell back to their manifest fallback (which use `no_signal` and score `0`).
|
|
90
70
|
|
|
91
|
-
|
|
92
|
-
- `classifier_outputs[]` — every classifier's full output, in registry order, including `reason`, `certainty`, all reserved fields, and all custom fields
|
|
93
|
-
- `model_recommendation` with the resolution audit above
|
|
94
|
-
- `meta.classifiers[name]` — per-classifier full output plus `status` and `version`
|
|
95
|
-
- `meta.certainty.{min, avg}` — whole-run certainty summary
|
|
71
|
+
The pipeline does not block based on these values — the caller inspects them and decides whether to trust the result or fall back to a safer behavior.
|
package/docs/signals.md
CHANGED
|
@@ -14,7 +14,7 @@ type Certainty =
|
|
|
14
14
|
| "near_certain";
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The aggregator maps certainty tags to numeric scores.
|
|
17
|
+
The aggregator maps certainty tags to numeric scores. `classifier_outputs[name].certainty` is a float; `avg_certainty` and `min_certainty` on the top-level result are also floats. Certainty labels are internal to classifier prompts; floats are what callers see.
|
|
18
18
|
|
|
19
19
|
## Reserved fields
|
|
20
20
|
|
|
@@ -26,9 +26,9 @@ A manifest declares which reserved fields its classifier may emit via the `reser
|
|
|
26
26
|
{ text: string } // 1–200 chars; must contain at least one non-whitespace character
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Use only for tiny terminal answers (greetings, thanks, spelling, simple arithmetic). The text IS the complete answer
|
|
29
|
+
Use only for tiny terminal answers (greetings, thanks, spelling, simple arithmetic). The text IS the complete answer — nothing else happens after this. Mutually exclusive with `ack_reply`.
|
|
30
30
|
|
|
31
|
-
When emitted
|
|
31
|
+
When emitted, the pipeline sets `action: "reply"` and surfaces the text in `result.reply`. All other classifiers still run to completion.
|
|
32
32
|
|
|
33
33
|
### `ack_reply`
|
|
34
34
|
|
|
@@ -36,7 +36,9 @@ When emitted with sufficient certainty, the highest-certainty value is surfaced
|
|
|
36
36
|
{ text: string } // 1–200 chars; must contain at least one non-whitespace character
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
A brief acknowledgement to show while downstream work continues.
|
|
39
|
+
A brief, task-specific acknowledgement to show while downstream work continues. Mutually exclusive with `final_reply`.
|
|
40
|
+
|
|
41
|
+
When emitted (and the action is `"route"`), the text is surfaced in `result.reply`. This is the immediate response your UI can show while the downstream model works.
|
|
40
42
|
|
|
41
43
|
### `model_tier`
|
|
42
44
|
|
|
@@ -62,22 +64,30 @@ Soft constraint for the catalog resolver. The resolver picks the cheapest catalo
|
|
|
62
64
|
string[] // each id must appear in the manifest's allowed_tools list
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
Sets `
|
|
67
|
+
Sets `result.tools`. Any classifier emitting this reserved field must declare `allowed_tools` on its manifest — that menu of allowed ids becomes both the JSON Schema constraint and the prompt listing.
|
|
66
68
|
|
|
67
69
|
Common tool-id aliases (`browser`, `browsing`, `internet`, `web_browsing`, `web_search`) are normalized to `web` before validation, so the model can drift on phrasing without breaking.
|
|
68
70
|
|
|
71
|
+
`result.tools` is always an array (empty if no classifier emitted it or no tools were selected).
|
|
72
|
+
|
|
69
73
|
### `risk_level`
|
|
70
74
|
|
|
71
75
|
```ts
|
|
72
76
|
"normal" | "suspicious" | "high_risk" | "unknown"
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
Prompt-injection posture for the target message. Surfaced in `
|
|
79
|
+
Prompt-injection posture for the target message. Surfaced in `result.prompt_injection`.
|
|
80
|
+
|
|
81
|
+
`"high_risk"` and `"unknown"` trigger `action: "block"` with `block_reason: "prompt_injection"`, regardless of certainty. `"suspicious"` is advisory — the pipeline routes normally and the caller decides whether to act on it.
|
|
82
|
+
|
|
83
|
+
When the `prompt_injection` classifier fails (runtime error or timeout), it uses its fallback which does **not** include `risk_level`. The pipeline then blocks with `block_reason: "classification_error"`, not `"prompt_injection"` — a classifier failure is distinct from an assessed injection risk.
|
|
76
84
|
|
|
77
85
|
## Custom fields
|
|
78
86
|
|
|
79
|
-
Anything not in the reserved list lives in your manifest's `output_schema.properties`. The runtime validates each output against the composed schema (custom properties + reserved sub-schemas + `reason` + `certainty`) at runtime, and surfaces the full output on `result.classifier_outputs[name]
|
|
87
|
+
Anything not in the reserved list lives in your manifest's `output_schema.properties`. The runtime validates each output against the composed schema (custom properties + reserved sub-schemas + `reason` + `certainty`) at runtime, and surfaces the full output on `result.classifier_outputs[name]`.
|
|
88
|
+
|
|
89
|
+
`classifier_outputs[name]` contains all payload fields plus `reason` (string) and `certainty` (float). The raw certainty label is not exposed; only the float score.
|
|
80
90
|
|
|
81
91
|
## Picking between reserved-field contributors
|
|
82
92
|
|
|
83
|
-
When two classifiers declare the same reserved field, the aggregator picks the highest-certainty value
|
|
93
|
+
When two classifiers declare the same reserved field, the aggregator picks the highest-certainty value. Ties are broken by manifest `dispatch_order` ascending (first in registry order keeps the slot). Both classifiers' full outputs still appear in `classifier_outputs` regardless of which one "won" the slot.
|
|
@@ -11,15 +11,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"models": {
|
|
13
13
|
"preflight": "gemma4:e4b-it-q4_K_M",
|
|
14
|
-
"
|
|
14
|
+
"model_tier": "gemma4:e4b-it-q4_K_M",
|
|
15
15
|
"model_specialization": "gemma4:e4b-it-q4_K_M",
|
|
16
|
-
"
|
|
17
|
-
"prompt_injection": "gemma4:e4b-it-q4_K_M",
|
|
18
|
-
"memory_retrieval_queries": "gemma4:e4b-it-q4_K_M"
|
|
16
|
+
"prompt_injection": "gemma4:e4b-it-q4_K_M"
|
|
19
17
|
}
|
|
20
18
|
},
|
|
21
|
-
"aggregator": {
|
|
22
|
-
"certaintyThreshold": 0.65
|
|
23
|
-
},
|
|
24
19
|
"catalog": "downstream-models.json"
|
|
25
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-classify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Manifest-driven classifier runtime for routing user messages to downstream AI models",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Taylor Bayouth",
|
|
@@ -29,11 +29,16 @@
|
|
|
29
29
|
"default": "./dist/src/index.js"
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"open-classify": "./bin/open-classify.mjs"
|
|
34
|
+
},
|
|
32
35
|
"files": [
|
|
36
|
+
"bin",
|
|
33
37
|
"dist/src",
|
|
34
38
|
"docs",
|
|
35
39
|
"downstream-models.json",
|
|
36
40
|
"open-classify.config.example.json",
|
|
41
|
+
"templates",
|
|
37
42
|
"LICENSE",
|
|
38
43
|
"README.md"
|
|
39
44
|
],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|