Run pplx-decider-flash-v1.2 on your Mac from the menu bar and call the Decisions API at 127.0.0.1 with no API key.
Some of the content you want to classify can’t leave your Mac: customer exports, meeting notes, screenshots of internal tools, logs with tokens in them. Decider for Mac runs pplx-decider-flash-v1.2, Perplexity’s 4B decision model, on your Mac and serves the Decisions API at http://127.0.0.1:8080/v1/decisions. The model reads your content on your machine. There’s no API key and no usage bill.You send the same JSON as the hosted Decisions API: content as state, a named question, and you get back probabilities. Build and test against your Mac, then point the same code at https://api.perplexity.ai/v1/decisions when you need hosted scale or the larger pplx-decider-v1.2 model.
Download Decider for Mac
Decider.dmg for Macs with Apple silicon running macOS 13 or later. The app downloads the 5.8 GB model on first launch.
Open the DMG and drag Decider to your Applications folder.
3
Open Decider and wait for the model to download
Open Decider from Applications. Its icon appears in the menu bar, not the Dock, and the setup window shows the one-time model download (about 5.8 GB). When the download finishes, the window shows Ready to use and the menu shows Ready. Closing the setup window keeps Decider running; click the menu bar icon to see the status or reopen the window.If Decider asks for a Hugging Face token, paste one with read access. You can create one in your Hugging Face token settings. If you already signed in with hf auth login, Decider uses that login instead.
The setup window after the model is downloaded and the local API is ready.
When the menu shows Ready, run this in Terminal. It asks which team should handle a support message:
curl -sS http://127.0.0.1:8080/v1/decisions \ -H 'Content-Type: application/json' \ -d '{ "model": "pplx-decider-flash-v1.2", "state": "I was charged twice for my order last week.", "questions": { "route": { "type": "choice", "instructions": "Which team should handle this message?", "criteria": {"billing": null, "shipping": null, "technical": null} } } }'
The answer for route has choice (the most likely team), probabilities (one value per team), and confidence. See the Decisions API quickstart for how to read each answer type and Question types for noul (yes or no), choice (one of your options), and score (a level on your rubric).The menu’s Copy Example curl gives you a ready-to-run request, and Copy API URL gives you the address.Send one question per request. pplx-decider-flash-v1.2 answers each question on its own; see Grouping questions in one request.
Every example below uses only what comes with macOS: Python 3 from the Command Line Tools, plus the built-in screencapture and sips commands. If python3 isn’t installed, macOS offers to install the Command Line Tools the first time you run it. Each script sends one question per request and reads one answer.
You have a folder of exported support emails, meeting notes, or user feedback, and you want a label on every file without uploading any of it. This script sends each .txt file in a folder to Decider and writes the labels to labels.csv.Save it as label_folder.py and run python3 label_folder.py ~/path/to/folder:
import csvimport jsonimport sysimport urllib.requestfrom pathlib import PathURL = "http://127.0.0.1:8080/v1/decisions"QUESTION = { "type": "choice", "instructions": "What is this message mainly about?", "criteria": { "bug": "Something is broken or not working as expected", "feature_request": "A request for something new", "billing": "Charges, refunds, plans, or invoices", "other": None, },}def decide(state, question): body = json.dumps({ "model": "pplx-decider-flash-v1.2", "state": state, "questions": {"q": question}, }).encode() request = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(request, timeout=60) as response: return json.load(response)["answers"]["q"]folder = Path(sys.argv[1]).expanduser()with open("labels.csv", "w", newline="") as out: writer = csv.writer(out) writer.writerow(["file", "label", "confidence"]) for path in sorted(folder.glob("*.txt")): answer = decide(path.read_text(errors="replace"), QUESTION) writer.writerow([path.name, answer["choice"], round(answer["confidence"], 3)]) print(path.name, answer["choice"], round(answer["confidence"], 3))
Sort labels.csv by confidence and read the lowest rows first. Those are the files where the model was least sure, which is where your category descriptions need work. Edit the descriptions in QUESTION and run it again. Nothing stops you from running it fifty times.
A regex scanner catches secrets with a known format, like an AWS key or a private key header. It misses the ones without a format: a database password in a config file or a token pasted into a comment. This pre-commit hook runs both checks on the lines you’re adding. A pattern match blocks the commit on its own, and Decider is asked about everything else.Save this as .git/hooks/pre-commit in your repository and run chmod +x .git/hooks/pre-commit:
#!/usr/bin/env python3import jsonimport reimport subprocessimport sysimport urllib.requestURL = "http://127.0.0.1:8080/v1/decisions"THRESHOLD = 0.2QUESTION = { "type": "noul", "instructions": "Does this text contain a real password, API key, access token, or private key? Placeholders like YOUR_API_KEY and environment variable names don't count.",}PATTERNS = { "private key": r"-----BEGIN [A-Z ]*PRIVATE KEY-----", "AWS access key": r"\b(AKIA|ASIA)[0-9A-Z]{16}\b", "GitHub token": r"\b(gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})", "Slack token": r"\bxox[abprs]-[A-Za-z0-9-]{10,}", "Perplexity API key": r"\bpplx-[A-Za-z0-9]{32,}",}def git(*args): return subprocess.run(["git", *args], capture_output=True, check=True).stdout.decode(errors="replace")def added_lines(path): diff = git("diff", "--cached", "--unified=0", "--no-color", "--", path) hunks = diff.split("\n@@", 1)[1] if "\n@@" in diff else "" return "\n".join(line[1:] for line in hunks.splitlines() if line.startswith("+"))def p_secret(text): body = json.dumps({ "model": "pplx-decider-flash-v1.2", "state": text, "questions": {"secret": QUESTION}, }).encode() request = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(request, timeout=60) as response: return json.load(response)["answers"]["secret"]["noul"]problems = []for path in git("diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z").split("\0"): text = added_lines(path) if path else "" if not text.strip(): continue for label, pattern in PATTERNS.items(): if re.search(pattern, text): problems.append(f"{path}: matches the {label} pattern") try: p = p_secret(text) except Exception as error: problems.append(f"{path}: not checked, Decider didn't answer ({error})") continue if p >= THRESHOLD: problems.append(f"{path}: probability of a secret {p:.2f}")if problems: print("Commit blocked:\n " + "\n ".join(problems)) sys.exit(1)
Each staged file’s added lines go to Decider as one noul question, and the commit stops if the probability of a secret is THRESHOLD or higher. A false positive costs you a second look, but a missed secret ends up in your Git history, so the cutoff starts low at 0.2. Run the hook on commits you’ve already reviewed and raise the cutoff only if it flags too many clean ones.The hook fails closed. If Decider isn’t running or can’t answer for a file, the commit stops and names the file. When you’ve checked a flagged file yourself, commit with git commit --no-verify to skip the hook once.The hook doesn’t read binary files, and it only sees what you commit from this Mac. For full coverage, run a dedicated secret scanner in CI as well.
Screenshots pile up: receipts, error dialogs, chat threads, code. Decider reads images, so it can sort them by what’s in them. This script asks one choice question about each image and moves it into a folder named after the answer.Save it as sort_screenshots.py and run python3 sort_screenshots.py ~/Desktop/screenshots:
import base64import jsonimport subprocessimport sysimport tempfileimport urllib.requestfrom pathlib import PathURL = "http://127.0.0.1:8080/v1/decisions"QUESTION = { "type": "choice", "instructions": "What does this screenshot mainly show?", "criteria": { "receipt": "A receipt, invoice, order confirmation, or payment", "error": "An error message, stack trace, or crash dialog", "conversation": "A chat, email, or social media thread", "code": "Source code, a terminal, or a diff", "other": None, },}def classify(image_path): small = Path(tempfile.mkdtemp()) / image_path.name subprocess.run(["sips", "-Z", "1440", str(image_path), "--out", str(small)], check=True, capture_output=True) mime = "image/png" if image_path.suffix.lower() == ".png" else "image/jpeg" image_url = f"data:{mime};base64," + base64.b64encode(small.read_bytes()).decode() body = json.dumps({ "model": "pplx-decider-flash-v1.2", "state": {"type": "image_url", "image_url": {"url": image_url}}, "questions": {"q": QUESTION}, }).encode() request = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(request, timeout=120) as response: return json.load(response)["answers"]["q"]folder = Path(sys.argv[1]).expanduser()for path in sorted(folder.iterdir()): if path.suffix.lower() not in {".png", ".jpg", ".jpeg"}: continue answer = classify(path) target = folder / answer["choice"] target.mkdir(exist_ok=True) path.rename(target / path.name) print(f"{path.name} -> {answer['choice']}/ ({answer['confidence']:.2f})")
sips -Z 1440 ... --out writes a copy with its longest side at 1440 pixels, which keeps every image inside the image size limit and keeps the request small. Your originals move, but they’re never resized. Add a category by adding a line to criteria. Your screenshots never leave your Mac.
Decider can answer questions about what’s on your screen right now. This script takes a screenshot, shrinks it, and asks whether an error is showing.Save it as check_screen.py and run python3 check_screen.py:
screencapture -x saves the screen without the shutter sound, and sips -Z 1440 shrinks the file in place. The first time you run it, macOS asks you to allow Screen Recording for Terminal under System Settings > Privacy & Security. Without that permission, the screenshot contains only your desktop background.Run it in a loop to watch a long build or a flaky deploy, and change the question to check for whatever you care about. The screenshots never leave your Mac.
When you need hosted throughput or the larger pplx-decider-v1.2 model, keep the request and change two things: the URL and an Authorization header with your API key.
import jsonimport osimport urllib.requestLOCAL = "http://127.0.0.1:8080/v1/decisions"HOSTED = "https://api.perplexity.ai/v1/decisions"def decide(state, question, hosted=False): headers = {"Content-Type": "application/json"} if hosted: headers["Authorization"] = f"Bearer {os.environ['PERPLEXITY_API_KEY']}" body = json.dumps({ "model": "pplx-decider-flash-v1.2", "state": state, "questions": {"q": question}, }).encode() request = urllib.request.Request(HOSTED if hosted else LOCAL, data=body, headers=headers) with urllib.request.urlopen(request, timeout=60) as response: return json.load(response)["answers"]["q"]question = {"type": "noul", "instructions": "Does this review report a product defect?"}review = "The battery stopped charging after two weeks."print("local: ", decide(review, question)["noul"])print("hosted:", decide(review, question, hosted=True)["noul"])
The weights on your Mac are a q8_0 GGUF build of the model, not the hosted deployment. If you tuned a threshold locally, check it on a sample of your data against the hosted model before you rely on it in production.
Whether the model is loaded. Ready (text + images) means you can send text and image requests.
Address
The local endpoint. On the default port it’s http://127.0.0.1:8080/v1/decisions.
Retry
Tries to load the model again. It’s grayed out while Decider is ready.
Set Up Decider…
Reopens the setup window.
Copy API URL
Copies the endpoint address.
Copy Example curl
Copies a ready-to-run curl request.
Port
Picks the port: 8080 (default), 8000, 8888, 9000, 11435, or Custom… for any other. If you change it, use the new port instead of 8080 in every example on this page.
Start at Login
Starts Decider when you log in to your Mac.
Set Hugging Face Token…
Sets the token Decider uses to download the model from Hugging Face.
Open Config File opens ~/Library/Application Support/ai.perplexity.decider/config.toml. You don’t need to edit it: the menu sets the port for you. These are the defaults:
host and port are the address the server listens on; Port in the menu sets the port. Leave host at 127.0.0.1. The [model] table names the Hugging Face repo and the two files Decider downloads, which are the files you see under Show Model Files. Leave the other keys at their defaults.