Zero-Shot Classification
MLX
Safetensors
qwen3_5
clef
cloudflare
systemone
structured-output
classification
multimodal
custom-code
decision-model
8-bit precision
Instructions to use mlx-community/clef-8bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use mlx-community/clef-8bit with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download mlx-community/clef-8bit --local-dir clef-8bit
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
clef_mlx.py: add 'predict' CLI and 'serve' (local POST /v1/systemone server); card: CLI and server docs
Browse files- README.md +38 -0
- clef_mlx.py +202 -0
README.md
CHANGED
|
@@ -83,6 +83,44 @@ model.predict({
|
|
| 83 |
|
| 84 |
See the [original model card](https://huggingface.co/Cloudflare/clef) for the input format, question types, and benchmarks.
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
## Limitations
|
| 87 |
|
| 88 |
- **Not a chat model.** Text generation tools (`mlx_lm.generate`, `mlx_vlm.generate`, LM Studio, Ollama) load
|
|
|
|
| 83 |
|
| 84 |
See the [original model card](https://huggingface.co/Cloudflare/clef) for the input format, question types, and benchmarks.
|
| 85 |
|
| 86 |
+
## Try it from the command line
|
| 87 |
+
|
| 88 |
+
`clef_mlx.py` runs on its own. From the downloaded repo it uses that repo by default; elsewhere pass
|
| 89 |
+
`--model mlx-community/clef-8bit`.
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
cd "$(hf download mlx-community/clef-8bit --quiet)"
|
| 93 |
+
python clef_mlx.py predict \
|
| 94 |
+
--state "Checkout has been failing for every customer for the last hour." \
|
| 95 |
+
--questions '{"urgent": {"type": "noul", "instructions": "Is this urgent?"},
|
| 96 |
+
"team": {"type": "choice", "criteria": {"billing": "Payments", "technical": "Outages and errors"}}}'
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
`--questions` takes JSON, a `.json` file, or `-` for stdin; `--request` takes a whole request body instead;
|
| 100 |
+
`--image photo.jpg` attaches an image (repeatable). The output is the SystemOne response as JSON.
|
| 101 |
+
|
| 102 |
+
## Run a local SystemOne server
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
python clef_mlx.py serve --port 8000 # http://127.0.0.1:8000, local only by default
|
| 106 |
+
curl http://127.0.0.1:8000/v1/systemone -H "Content-Type: application/json" -d '{
|
| 107 |
+
"model": "clef",
|
| 108 |
+
"state": "Checkout has been failing for every customer for the last hour.",
|
| 109 |
+
"questions": {"urgent": {"type": "noul", "instructions": "Is this urgent?"}}
|
| 110 |
+
}'
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
- `POST /v1/systemone` takes the same request body as the Jev/SystemOne API and returns the same response
|
| 114 |
+
(plus `usage.latency_ms`), so existing SystemOne clients can point at your laptop. `GET /health` and
|
| 115 |
+
`GET /v1/models` are also available.
|
| 116 |
+
- Images go in `images` as data URLs, base64, or http(s) URLs. Videos aren't supported over HTTP.
|
| 117 |
+
- Errors return JSON: `400` for invalid requests, `413` with "maximum context length" for inputs that don't fit.
|
| 118 |
+
Add `"truncate": false` to a request (or start with `--no-truncate`) to get the `413` instead of truncation.
|
| 119 |
+
- Requests run one at a time on the GPU. There is no authentication: keep the default `127.0.0.1` binding
|
| 120 |
+
unless you put your own proxy in front.
|
| 121 |
+
- For the [Decision Index](https://github.com/apolinario/decision-index), start with `--no-truncate` and use
|
| 122 |
+
its `http` engine: `python -m decision_index run --engine http --option base_url=http://127.0.0.1:8000`.
|
| 123 |
+
|
| 124 |
## Limitations
|
| 125 |
|
| 126 |
- **Not a chat model.** Text generation tools (`mlx_lm.generate`, `mlx_vlm.generate`, LM Studio, Ollama) load
|
clef_mlx.py
CHANGED
|
@@ -8,6 +8,11 @@ Torch-free. The backbone is loaded with mlx-vlm (text + images/video) or mlx-lm
|
|
| 8 |
model = clef_mlx.load("mlx-community/clef-flash-4bit")
|
| 9 |
model.predict(record) # {question_id: {option_id: probability}}
|
| 10 |
model.systemone(request) # Jev/SystemOne /v1/systemone response body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
|
@@ -573,3 +578,200 @@ def load(path: str | Path, head_dtype=mx.bfloat16, backend: str = "auto") -> Cle
|
|
| 573 |
head.set_dtype(head_dtype)
|
| 574 |
mx.eval(head.parameters())
|
| 575 |
return ClefMLX(backbone, tokenizer, head, processor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
model = clef_mlx.load("mlx-community/clef-flash-4bit")
|
| 9 |
model.predict(record) # {question_id: {option_id: probability}}
|
| 10 |
model.systemone(request) # Jev/SystemOne /v1/systemone response body
|
| 11 |
+
|
| 12 |
+
Command line (from a downloaded repo, --model defaults to that repo):
|
| 13 |
+
|
| 14 |
+
python clef_mlx.py predict --state "..." --questions '{"q": {"type": "noul"}}'
|
| 15 |
+
python clef_mlx.py serve --port 8000 # POST /v1/systemone on 127.0.0.1
|
| 16 |
"""
|
| 17 |
|
| 18 |
from __future__ import annotations
|
|
|
|
| 578 |
head.set_dtype(head_dtype)
|
| 579 |
mx.eval(head.parameters())
|
| 580 |
return ClefMLX(backbone, tokenizer, head, processor)
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# --------------------------------------------------------------------------- CLI and server
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
def _default_model() -> str | None:
|
| 587 |
+
"""When run from inside a downloaded repo, default to that repo."""
|
| 588 |
+
here = Path(__file__).resolve().parent
|
| 589 |
+
return str(here) if (here / "joint_head.safetensors").exists() else None
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
def _json_arg(value: str) -> Any:
|
| 593 |
+
"""A JSON string, a path to a JSON file, or '-' for stdin."""
|
| 594 |
+
if value == "-":
|
| 595 |
+
return json.load(__import__("sys").stdin)
|
| 596 |
+
try:
|
| 597 |
+
candidate = Path(value).expanduser()
|
| 598 |
+
if candidate.is_file():
|
| 599 |
+
return json.loads(candidate.read_text())
|
| 600 |
+
except OSError: # e.g. inline JSON too long to be a file name
|
| 601 |
+
pass
|
| 602 |
+
return json.loads(value)
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def _state_arg(value: str) -> Any:
|
| 606 |
+
"""State text; JSON objects/arrays are parsed so they are rendered like the reference does."""
|
| 607 |
+
stripped = value.strip()
|
| 608 |
+
if stripped[:1] in "{[":
|
| 609 |
+
try:
|
| 610 |
+
return json.loads(stripped)
|
| 611 |
+
except json.JSONDecodeError:
|
| 612 |
+
pass
|
| 613 |
+
return value
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
def _decode_image(item: Any):
|
| 617 |
+
"""An image from a data URL, raw base64 or an http(s) URL (no local paths over HTTP)."""
|
| 618 |
+
import base64
|
| 619 |
+
import io
|
| 620 |
+
import urllib.request
|
| 621 |
+
|
| 622 |
+
from PIL import Image
|
| 623 |
+
|
| 624 |
+
if not isinstance(item, str) or not item:
|
| 625 |
+
raise ValueError("images must be strings: data URLs, base64, or http(s) URLs")
|
| 626 |
+
if item.startswith(("http://", "https://")):
|
| 627 |
+
with urllib.request.urlopen(item, timeout=30) as response:
|
| 628 |
+
data = response.read()
|
| 629 |
+
else:
|
| 630 |
+
if item.startswith("data:"):
|
| 631 |
+
item = item.split(",", 1)[-1]
|
| 632 |
+
try:
|
| 633 |
+
data = base64.b64decode(item, validate=True)
|
| 634 |
+
except ValueError:
|
| 635 |
+
raise ValueError("could not decode image: expected a data URL, base64, or http(s) URL") from None
|
| 636 |
+
return Image.open(io.BytesIO(data)).convert("RGB")
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
def _cmd_predict(args) -> None:
|
| 640 |
+
import sys
|
| 641 |
+
|
| 642 |
+
if args.request:
|
| 643 |
+
request = _json_arg(args.request)
|
| 644 |
+
else:
|
| 645 |
+
if args.state is None or args.questions is None:
|
| 646 |
+
sys.exit("predict needs --state and --questions, or --request")
|
| 647 |
+
request = {"state": _state_arg(args.state), "questions": _json_arg(args.questions)}
|
| 648 |
+
request.setdefault("model", args.name or Path(args.model).name)
|
| 649 |
+
if args.image:
|
| 650 |
+
from PIL import Image
|
| 651 |
+
|
| 652 |
+
request["images"] = [Image.open(Path(p).expanduser()).convert("RGB") for p in args.image]
|
| 653 |
+
model = load(args.model)
|
| 654 |
+
try:
|
| 655 |
+
response = model.systemone(request, max_length=args.max_length, truncate=not args.no_truncate)
|
| 656 |
+
except ValueError as error:
|
| 657 |
+
sys.exit(f"error: {error}")
|
| 658 |
+
print(json.dumps(response, indent=2, ensure_ascii=False))
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
def _cmd_serve(args) -> None:
|
| 662 |
+
import threading
|
| 663 |
+
import time
|
| 664 |
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 665 |
+
|
| 666 |
+
model = load(args.model)
|
| 667 |
+
served_name = args.name or Path(args.model).name
|
| 668 |
+
model.systemone({"model": served_name, "state": "warmup",
|
| 669 |
+
"questions": {"w": {"type": "noul", "instructions": "Is this a warmup?"}}})
|
| 670 |
+
lock = threading.Lock() # one GPU: run requests one at a time
|
| 671 |
+
max_body = args.max_body_mb * 1024 * 1024
|
| 672 |
+
|
| 673 |
+
class Handler(BaseHTTPRequestHandler):
|
| 674 |
+
server_version = "clef-mlx"
|
| 675 |
+
|
| 676 |
+
def _send(self, status: int, body: dict) -> None:
|
| 677 |
+
data = json.dumps(body, ensure_ascii=False).encode()
|
| 678 |
+
self.send_response(status)
|
| 679 |
+
self.send_header("Content-Type", "application/json")
|
| 680 |
+
self.send_header("Content-Length", str(len(data)))
|
| 681 |
+
self.end_headers()
|
| 682 |
+
self.wfile.write(data)
|
| 683 |
+
|
| 684 |
+
def _error(self, status: int, message: str) -> None:
|
| 685 |
+
self._send(status, {"error": {"message": message}})
|
| 686 |
+
|
| 687 |
+
def do_GET(self):
|
| 688 |
+
if self.path == "/health":
|
| 689 |
+
self._send(200, {"status": "ok", "model": served_name})
|
| 690 |
+
elif self.path == "/v1/models":
|
| 691 |
+
self._send(200, {"object": "list", "data": [{"id": served_name, "object": "model"}]})
|
| 692 |
+
else:
|
| 693 |
+
self._error(404, f"not found: {self.path}")
|
| 694 |
+
|
| 695 |
+
def do_POST(self):
|
| 696 |
+
if self.path != "/v1/systemone":
|
| 697 |
+
return self._error(404, f"not found: {self.path}")
|
| 698 |
+
length = int(self.headers.get("Content-Length") or 0)
|
| 699 |
+
if length > max_body:
|
| 700 |
+
return self._error(413, f"request body larger than {args.max_body_mb} MB")
|
| 701 |
+
try:
|
| 702 |
+
request = json.loads(self.rfile.read(length))
|
| 703 |
+
if not isinstance(request, dict):
|
| 704 |
+
raise ValueError("request body must be a JSON object")
|
| 705 |
+
if request.get("videos"):
|
| 706 |
+
raise ValueError("videos are not supported over HTTP; use clef_mlx.py from Python")
|
| 707 |
+
request.setdefault("model", served_name)
|
| 708 |
+
if request.get("images"):
|
| 709 |
+
request["images"] = [_decode_image(i) for i in request["images"]]
|
| 710 |
+
truncate = request.pop("truncate", not args.no_truncate)
|
| 711 |
+
except (json.JSONDecodeError, ValueError, OSError) as error:
|
| 712 |
+
return self._error(400, str(error))
|
| 713 |
+
started = time.perf_counter()
|
| 714 |
+
try:
|
| 715 |
+
with lock:
|
| 716 |
+
response = model.systemone(request, max_length=args.max_length, truncate=bool(truncate))
|
| 717 |
+
except ContextTooLong as error:
|
| 718 |
+
return self._error(413, f"maximum context length exceeded: {error}")
|
| 719 |
+
except (ValueError, KeyError, TypeError) as error:
|
| 720 |
+
return self._error(400, str(error))
|
| 721 |
+
except Exception as error: # noqa: BLE001 - report, keep serving
|
| 722 |
+
return self._error(500, f"{type(error).__name__}: {error}")
|
| 723 |
+
response["usage"]["latency_ms"] = round((time.perf_counter() - started) * 1000, 1)
|
| 724 |
+
self._send(200, response)
|
| 725 |
+
|
| 726 |
+
def log_message(self, fmt, *fmt_args):
|
| 727 |
+
if not args.quiet:
|
| 728 |
+
super().log_message(fmt, *fmt_args)
|
| 729 |
+
|
| 730 |
+
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
| 731 |
+
print(f"Serving {served_name} on http://{args.host}:{args.port}/v1/systemone "
|
| 732 |
+
f"(truncate={'off' if args.no_truncate else 'on'}, max_length={args.max_length})", flush=True)
|
| 733 |
+
try:
|
| 734 |
+
server.serve_forever()
|
| 735 |
+
except KeyboardInterrupt:
|
| 736 |
+
pass
|
| 737 |
+
finally:
|
| 738 |
+
server.server_close()
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
def main(argv: list[str] | None = None) -> None:
|
| 742 |
+
import argparse
|
| 743 |
+
|
| 744 |
+
default_model = _default_model()
|
| 745 |
+
parser = argparse.ArgumentParser(prog="clef_mlx.py", description="Run Clef locally with MLX.")
|
| 746 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 747 |
+
|
| 748 |
+
def common(p):
|
| 749 |
+
p.add_argument("--model", default=default_model, required=default_model is None,
|
| 750 |
+
help="Hub repo id or local dir (default: the repo this file is in)")
|
| 751 |
+
p.add_argument("--name", help="model name echoed in responses (default: the model dir/repo name)")
|
| 752 |
+
p.add_argument("--max-length", type=int, default=16384, help="prompt token limit (default 16384)")
|
| 753 |
+
p.add_argument("--no-truncate", action="store_true",
|
| 754 |
+
help="refuse over-length inputs instead of truncating the state")
|
| 755 |
+
|
| 756 |
+
p = sub.add_parser("predict", help="answer one request and print the SystemOne response")
|
| 757 |
+
common(p)
|
| 758 |
+
p.add_argument("--state", help="state text (JSON objects/arrays are parsed)")
|
| 759 |
+
p.add_argument("--questions", help="questions as JSON, a .json file, or '-' for stdin")
|
| 760 |
+
p.add_argument("--request", help="a full /v1/systemone request body as JSON, a .json file, or '-'")
|
| 761 |
+
p.add_argument("--image", action="append", help="image file to attach (repeatable)")
|
| 762 |
+
p.set_defaults(func=_cmd_predict)
|
| 763 |
+
|
| 764 |
+
p = sub.add_parser("serve", help="serve POST /v1/systemone over HTTP")
|
| 765 |
+
common(p)
|
| 766 |
+
p.add_argument("--host", default="127.0.0.1", help="bind address (default 127.0.0.1, local only)")
|
| 767 |
+
p.add_argument("--port", type=int, default=8000)
|
| 768 |
+
p.add_argument("--max-body-mb", type=int, default=64)
|
| 769 |
+
p.add_argument("--quiet", action="store_true", help="no per-request access log")
|
| 770 |
+
p.set_defaults(func=_cmd_serve)
|
| 771 |
+
|
| 772 |
+
args = parser.parse_args(argv)
|
| 773 |
+
args.func(args)
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
if __name__ == "__main__":
|
| 777 |
+
main()
|