logit-classifier 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,354 @@
1
+ Metadata-Version: 2.5
2
+ Name: logit-classifier
3
+ Version: 0.1.0
4
+ Summary: Local zero-shot classifier for text and images. Declares options, returns a calibrated probability for each, generates no text. Accepts the TypeSafe System One request format.
5
+ Project-URL: Repository, https://github.com/Blakeem/logit-classifier
6
+ Project-URL: Bug Tracker, https://github.com/Blakeem/logit-classifier/issues
7
+ Author: Blake
8
+ License-Expression: GPL-3.0-or-later
9
+ License-File: LICENSE
10
+ Keywords: calibration,classification,comfyui,local-llm,logits,vision-language-model,zero-shot
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: numpy>=2.1.0
20
+ Provides-Extra: hf
21
+ Requires-Dist: accelerate>=1.0.0; extra == 'hf'
22
+ Requires-Dist: huggingface-hub[hf-xet]>=0.34.0; extra == 'hf'
23
+ Requires-Dist: pillow>=12.3.0; extra == 'hf'
24
+ Requires-Dist: safetensors>=0.4.5; extra == 'hf'
25
+ Requires-Dist: torch>=2.11.0; extra == 'hf'
26
+ Requires-Dist: torchvision>=0.26.0; extra == 'hf'
27
+ Requires-Dist: transformers>=4.57.0; extra == 'hf'
28
+ Provides-Extra: service
29
+ Requires-Dist: fastapi>=0.115.0; extra == 'service'
30
+ Requires-Dist: pillow>=12.3.0; extra == 'service'
31
+ Requires-Dist: uvicorn[standard]>=0.32.0; extra == 'service'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Logit Classifier
35
+
36
+ A local zero-shot classifier. One state and a set of questions go in, and a calibrated
37
+ probability per option comes out. No text is generated, since every answer is read from
38
+ the logits at a single token position.
39
+
40
+ The request format is TypeSafe's System One, so a Jev request body works unchanged. The
41
+ default model reads images as well as text.
42
+
43
+ ## Question Types
44
+
45
+ | Type | You declare | You get back |
46
+ |---|---|---|
47
+ | `choice` | a map of option name to description | the winning name, a probability per option, a confidence, and an abstain |
48
+ | `score` | an ordered list of level descriptions | a weighted level, a probability per level, the legend, and a confidence |
49
+ | `noul` | one statement, with optional true and false descriptions | one probability that the statement is true |
50
+
51
+ A `choice` holds up to 255 options. A `score` holds 2 to 10 levels. One request holds up
52
+ to 256 questions, and every question is answered against the same state in one pass.
53
+
54
+ A branch is one prompt whose last token position carries a distribution. A question needs
55
+ one branch, or several when it holds more than 52 options or scores each level alone.
56
+
57
+ `abstain` is how strongly the model wants none of your options. Every choice question is
58
+ offered an extra label named `none of these`, and that label's mass comes back as
59
+ `abstain` instead of in `probabilities`. So `probabilities` covers your own options and
60
+ sums to 1.
61
+
62
+ `confidence` measures how peaked the answer is, using TypeSafe's published formulas.
63
+
64
+ Ask one `noul` per fact when several answers can be true at once. A `choice` normalizes
65
+ over its options, so one winner takes nearly all the mass.
66
+
67
+ ## Install
68
+
69
+ The package needs Python 3.12 or newer.
70
+
71
+ ```
72
+ pip install "logit-classifier[hf,service]"
73
+ ```
74
+
75
+ | Extra | Adds | For |
76
+ |---|---|---|
77
+ | none | numpy | passing a model you already loaded |
78
+ | `hf` | torch, torchvision, transformers, accelerate, safetensors, huggingface-hub, pillow | loading the weights in this process |
79
+ | `service` | fastapi, uvicorn, pillow | the HTTP endpoint and the browser page |
80
+
81
+ The `hf` extra needs an NVIDIA GPU with at least 10 GB of free VRAM. The weights are
82
+ about 8 GB per model and download on first use.
83
+
84
+ They go to the Hugging Face cache by default. Point `models_dir` at a folder to keep them
85
+ beside your project instead, which is what the examples do.
86
+
87
+ ```python
88
+ Config(models_dir=Path("models"))
89
+ ```
90
+
91
+ `LOGIT_MODELS_DIR` sets the same folder for every process, and `HF_HOME` moves the
92
+ Hugging Face cache itself.
93
+
94
+ ## Models
95
+
96
+ | | Qwen/Qwen3-VL-4B-Instruct | Qwen/Qwen3-4B-Instruct-2507 |
97
+ |---|---|---|
98
+ | reads images | yes | no |
99
+ | better at | choice and score | noul |
100
+
101
+ `Qwen3-VL-4B-Instruct` is the default. Set `LOGIT_MODEL_ID` to use the other one. Any Qwen
102
+ chat model loads, and a model with no fitted temperature gets 2.5.
103
+
104
+ ## Python API
105
+
106
+ ```python
107
+ from logit_classifier import Classifier, Config, load_model, parse_request
108
+
109
+ config = Config()
110
+ backend = load_model("Qwen/Qwen3-VL-4B-Instruct", config)
111
+ classifier = Classifier(config, backend=backend)
112
+
113
+ request = parse_request({
114
+ "state": "the payment failed again",
115
+ "questions": {
116
+ "urgent": {"type": "noul", "instructions": "The message is urgent"},
117
+ },
118
+ })
119
+ response, diagnostics = classifier.classify(request)
120
+ print(response.to_dict())
121
+ ```
122
+
123
+ `load_model` loads the weights through the `hf` extra and returns a `Backend`.
124
+ `Classifier(Config())` calls it for you when you pass no backend.
125
+
126
+ Loading each model yourself is what lets one script compare several. The fitted
127
+ temperature follows the backend, so every model is scored with its own value.
128
+
129
+ ```python
130
+ for model_id in ("Qwen/Qwen3-VL-4B-Instruct", "Qwen/Qwen3-4B-Instruct-2507"):
131
+ classifier = Classifier(config, backend=load_model(model_id, config))
132
+ ```
133
+
134
+ ## HTTP API
135
+
136
+ ```
137
+ logit-classifier serve
138
+ ```
139
+
140
+ Run it as `python -m logit_classifier serve` when the scripts directory is not on PATH.
141
+
142
+ `POST /v1/systemone` takes one `state` and a map of questions.
143
+
144
+ ```json
145
+ {
146
+ "state": "I have been trying to connect my Stripe account for 3 days and it keeps failing.",
147
+ "questions": {
148
+ "department": {
149
+ "type": "choice",
150
+ "instructions": "Which team should handle this",
151
+ "criteria": {
152
+ "billing": "Payment or subscription issues",
153
+ "technical": "Bugs or integration problems",
154
+ "sales": null
155
+ }
156
+ },
157
+ "frustration": {
158
+ "type": "score",
159
+ "instructions": "How frustrated the customer appears",
160
+ "criteria": ["Calm", "Frustrated but civil", "Very angry"]
161
+ },
162
+ "is_urgent": { "type": "noul", "instructions": "The message conveys urgency" }
163
+ }
164
+ }
165
+ ```
166
+
167
+ Each answer comes back under the key you gave its question. The keys are never sent to
168
+ the model.
169
+
170
+ ```json
171
+ {
172
+ "model": "logit-classifier-0.1.0",
173
+ "answers": {
174
+ "department": {
175
+ "type": "choice",
176
+ "choice": "technical",
177
+ "confidence": 0.9518,
178
+ "probabilities": { "billing": 0.0237, "technical": 0.9679, "sales": 0.0084 },
179
+ "abstain": 0.0068
180
+ },
181
+ "frustration": {
182
+ "type": "score",
183
+ "score": 1.0199,
184
+ "confidence": 0.9583,
185
+ "legend": { "0": "Calm", "1": "Frustrated but civil", "2": "Very angry" },
186
+ "probabilities": { "0": 0.004, "1": 0.9722, "2": 0.0238 }
187
+ },
188
+ "is_urgent": { "type": "noul", "noul": 0.9924 }
189
+ },
190
+ "usage": { "input_tokens": 210, "output_tokens": 3 }
191
+ }
192
+ ```
193
+
194
+ Add `?diagnostics=1` to also return the label mass, the branch count, the latency and the
195
+ calibration settings.
196
+
197
+ `GET /v1/models` lists the served model. `GET /healthz` reports whether the weights are
198
+ loaded. Open `http://127.0.0.1:8077/` for a browser form that builds a request and draws
199
+ each probability as a bar.
200
+
201
+ ## Images
202
+
203
+ A `state` that is a mapping may carry one image under `image` or `screenshot`, as a file
204
+ path, a data URL or bare base64. Everything else in that mapping is rendered as text
205
+ beside the picture. The HTTP service accepts a data URL or base64, and a file path works
206
+ only in process.
207
+
208
+ ```json
209
+ { "state": { "image": "tile-3.png", "caption": "tile 3 of 6" },
210
+ "questions": { "sky": { "type": "noul", "instructions": "This crop shows the night sky" } } }
211
+ ```
212
+
213
+ The image is encoded once for the entire request, so asking several questions about one
214
+ picture costs little more than asking one.
215
+
216
+ A model with no vision tower rejects the request rather than ignoring the picture.
217
+
218
+ ## Examples
219
+
220
+ Each script runs on its own.
221
+
222
+ | Script | Shows |
223
+ |---|---|
224
+ | `examples/quickstart.py` | one request with all three question types |
225
+ | `examples/question_types.py` | every field each answer type returns |
226
+ | `examples/images.py` | a picture, and a picture with text beside it |
227
+ | `examples/compare_models.py` | two models answering the same questions in one command |
228
+ | `examples/http_client.py` | calling the service over HTTP |
229
+ | `examples/own_backend.py` | implementing the backend port, with no weights or GPU |
230
+
231
+ ## Configuration
232
+
233
+ Every setting reads from the environment at startup. `logit-classifier config` prints what
234
+ they produce.
235
+
236
+ | Variable | Default | Effect |
237
+ |---|---|---|
238
+ | `LOGIT_MODEL_ID` | `Qwen/Qwen3-VL-4B-Instruct` | any Qwen chat model, with or without vision |
239
+ | `LOGIT_MODELS_DIR` | the Hugging Face cache | where downloaded weights are kept |
240
+ | `LOGIT_DEVICE` | `cuda` | torch device |
241
+ | `LOGIT_TEMPERATURE` | the model's fitted value | divides the logits before the softmax |
242
+ | `LOGIT_PRIOR_DEBIAS` | `1` | set to `0` to skip the label prior |
243
+ | `LOGIT_BATCH_BRANCHES` | `1` | set to `0` for one forward pass per branch |
244
+ | `LOGIT_SCORE_METHOD` | `joint` | set to `independent` to judge each level alone |
245
+ | `LOGIT_ABSTAIN` | `1` | set to `0` to drop the `none of these` label below 52 options |
246
+ | `LOGIT_CALIBRATION_PATH` | `calibration.json` | where the service stores the running prior |
247
+
248
+ ## Calibration
249
+
250
+ Two corrections are applied before the softmax.
251
+
252
+ The label prior is the model's standing preference for the token `A` over `B`. It is
253
+ learned as a running mean over served requests, so it needs no labelled data or extra
254
+ forward passes. It stays unused until a bucket has 32 observations, so the first requests
255
+ after a cold start can answer a little differently from later ones.
256
+
257
+ The temperature is fitted per model and divides the logits. Both shipped values were
258
+ fitted on Banking77 rather than on your data, so refit yours when the absolute numbers
259
+ matter.
260
+
261
+ The library holds the prior in memory and writes no file. The service stores it in
262
+ `calibration.json`. Pass `Config(calibration_path=...)` to keep it across runs from the
263
+ library. A stored prior carries a fingerprint of the model and the prompt shape, and
264
+ changing either one starts the prior over.
265
+
266
+ ## Determinism
267
+
268
+ Nothing is sampled, so the same request returns bitwise identical logits.
269
+
270
+ Batch composition and padding length both change the low bits of a bfloat16 forward pass,
271
+ and both are pure functions of the request. So the same question asked inside two
272
+ different requests can differ slightly. Set `LOGIT_BATCH_BRANCHES=0` to score each branch
273
+ alone, which makes a question independent of the questions sent with it and costs one
274
+ forward pass per branch.
275
+
276
+ A forward pass needs several process-global torch settings held at known values. Torch
277
+ exposes none of them as a call argument, so the backend sets them around each pass and
278
+ puts the host's values back after. This way a host that shares the interpreter keeps its
279
+ own settings everywhere outside a pass.
280
+
281
+ ## Options Above 52
282
+
283
+ Options are labeled `A` to `Z` then `a` to `z`, which gives 52 single token labels. A
284
+ question with more options is split into groups, and each group carries an escape label so
285
+ the groups can be weighed against each other. Accuracy is lower on that path and `abstain`
286
+ is much weaker, so keep a question under 52 options where you can.
287
+
288
+ ## Speed
289
+
290
+ On a warm RTX 3090 Ti a short record takes about 140 ms whatever it asks, up to about 8
291
+ questions, because two forward passes dominate. So send every question for a record in one
292
+ request. Keep a record under about 8,000 tokens, since a longer state falls off the
293
+ prefill ceiling.
294
+
295
+ ## ComfyUI Node Packs
296
+
297
+ A node pack installs the bare package without extras, so pip is never asked to resolve
298
+ torch beside ComfyUI's own CUDA build. The pack then passes its own backend over a model
299
+ ComfyUI already loaded, and this library never imports torch or transformers.
300
+
301
+ A node pack uses its own backend in place of `load_model`. Everything after that is the
302
+ same call.
303
+
304
+ ```python
305
+ from logit_classifier import Backend, Classifier, Config, verify_backend
306
+
307
+ assert isinstance(my_backend, Backend)
308
+ label_ids = verify_backend(my_backend)
309
+ classifier = Classifier(Config(), backend=my_backend)
310
+ ```
311
+
312
+ `Backend` declares `model_id`, `label_ids` and `sees_images`, plus `render`, `encode`,
313
+ `encode_prefix` and `score`. `model_id` is the string the fitted temperature is looked up
314
+ by, so a backend over the text model must report the text model. `Backend` is runtime
315
+ checkable, so a host can check its own object before passing it. `verify_backend` proves
316
+ the render and label contracts and returns one token id per label.
317
+
318
+ `COMFY_SOCKET_TYPE` holds the ComfyUI socket name for a loaded classifier.
319
+ `examples/own_backend.py` implements the entire port.
320
+ `logit_classifier.backends.hf.HFBackend` is the transformers implementation to read
321
+ against.
322
+
323
+ ## Differences From Jev
324
+
325
+ This project matches the System One request and response format and the documented limits.
326
+ It is not affiliated with TypeSafe. Its numbers do not match Jev's.
327
+
328
+ Jev is trained for calibrated probabilities. This project reads them from a general model,
329
+ so the choices agree more often than the confidences do.
330
+
331
+ Jev judges a score level without its number or its neighbours. This project judges all
332
+ levels together by default. Set `LOGIT_SCORE_METHOD=independent` for the documented
333
+ behavior.
334
+
335
+ Jev publishes status codes but no error body. The error shape here is our own.
336
+
337
+ ## Development
338
+
339
+ ```
340
+ uv sync --all-extras
341
+ uv run pytest tests/test_unit.py
342
+ uv run pytest tests/test_model.py -m model
343
+ uv run ruff check .
344
+ uv run mypy
345
+ ```
346
+
347
+ The unit tests need no weights. The model tests load the GPU and take about 15 seconds.
348
+
349
+ `FINDINGS.md` is the measurement record. Every number there names the script in
350
+ `tests-AB/` that produced it.
351
+
352
+ ## License
353
+
354
+ GPL-3.0. See `LICENSE`.
@@ -0,0 +1,24 @@
1
+ logit_classifier/__init__.py,sha256=XwjWECGU5V8j6HdV_Eqd9XDeAFjXsHr2Fd4kax4NPeA,2124
2
+ logit_classifier/__main__.py,sha256=GHrjXTqm3z2G291fBuqXVvSQeUWgPqRgBY7He7f-mx4,240
3
+ logit_classifier/calibrate.py,sha256=shz-qAKNMDn5APZEpKiOsV4M9Dizk8UBW4i3_OrGNXU,5865
4
+ logit_classifier/classifier.py,sha256=LKs8mCOd6cyLz1AkBPgQpKxg89ogKuBEJAP3nKHHGug,14439
5
+ logit_classifier/cli.py,sha256=t0z_BPtuA1Nyy50w7Abb9HDdrJvTgovHxI6MElRXTo0,1795
6
+ logit_classifier/config.py,sha256=k18QD7egAG0JsgAi6EnlodbuZRmW1mdOMKQici1LYrc,7749
7
+ logit_classifier/deps.py,sha256=nofR5WjToaHXoWgaDPkq2Txm9B5FhwWvmCVeO3poauE,996
8
+ logit_classifier/errors.py,sha256=s0InrI-vx8MvjOxrJ-XriMT0h2EETqZR_Gpss_aejbY,573
9
+ logit_classifier/labels.py,sha256=av2tlxgY6glCuBdhSJX3gwEdoF1kPVEFws6-knvomXU,3418
10
+ logit_classifier/prompt.py,sha256=W0AE0rjTX721kuljnBIPHUPZl1jKVQJe757utL5KN-g,6379
11
+ logit_classifier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ logit_classifier/schema.py,sha256=jTVRyO7B-pTkydUYccCHvVoG8K6YjoHDpj6YUp2bDCY,9258
13
+ logit_classifier/scoring.py,sha256=MRCpUZbQBPwSXdOCFZxao8mC_NpZTrUqZc1ZNh0oqO8,4075
14
+ logit_classifier/service.py,sha256=b34FBAVUCzXWJhIV6RL9VxhFEG7aqCaZWdpFOwp7K80,5668
15
+ logit_classifier/vision.py,sha256=Z64ACRm9G1EWigSkQQpaAx7CA6kUOjHwJoKh7Gqv80Q,3549
16
+ logit_classifier/backends/__init__.py,sha256=wzfkDHPWgK3x1-ilAFih1ryJAqZ3M7VknGO_lEBJ9B0,224
17
+ logit_classifier/backends/base.py,sha256=dedOkmALap3QM1-Lr2asUnoib5kr0ZdNM9oVDZhHKls,5273
18
+ logit_classifier/backends/hf.py,sha256=OV3blHX2OZB-Fa1RsefwO57tqPfmv2hT6n89RTNq6js,19127
19
+ logit_classifier/web/index.html,sha256=wx8XQaEdaiL1RDcsGO5JQhYsJre5gsHOyjH2wLGAez4,17705
20
+ logit_classifier-0.1.0.dist-info/METADATA,sha256=GzAVNQ-eHHBAiwYUQD66MoQSxv7NGDZL_2AaRTDX0oQ,13877
21
+ logit_classifier-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
22
+ logit_classifier-0.1.0.dist-info/entry_points.txt,sha256=5umLAfkRklfpJgFgNhcNzz3xu0aHhtrJSpuxStIbwYI,63
23
+ logit_classifier-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
24
+ logit_classifier-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ logit-classifier = logit_classifier.cli:main