classifier.dev

dev.classifierPublished by classifier.devOfficial publisherv1.0.0Updated Oct 25, 2026

Sort up to 1,000 texts into your own labels with a calibrated confidence per answer. No API key.

VerifiedStreamable HTTPWeb executableData & AnalyticsAI & ML

Overview

AI-generated overview

Remote MCP server that sorts batches of up to 1,000 texts into your own labels, with a calibrated confidence per answer and no API key.

What you can do
Sort texts into exactly one of your own labels, with a confidence per answer, for triage, routing or filtering large batches without reading them all. Classify each text along several named dimensions, such as team, urgency and kind, in one request. Tag texts with every label that applies, each with an independent score and an optional cap on labels per text. Get only a histogram of how many texts landed on each label, or only the low-confidence items with their two most likely labels for manual review.
When to use it
Useful when an assistant has many short items to bucket — search results before opening them, tickets, log lines, changed files or feedback — and wants labels plus confidence rather than reading everything. Also fits tagging and corpus-shape questions where one label per item is not enough.
Requirements
A remote endpoint at classifier.dev over streamable HTTP; no key, signup or environment variables are declared for the MCP connection. The service is rate-limited per IP (3,000/min and 20,000/day on the fast tier, 200/min and 2,000/day on smart).
Before you install
Texts you submit are sent to a third party for classification; the README says request text is not written to analytics, but label names are kept in aggregate records for 90 days. Batches are capped at 1,000 items and per-IP quotas apply. Paid workspace keys, operator keys and long-document uploads described in the README involve billing and are not part of the declared MCP setup.

Installation

In SourceWeft

  1. Open classifier.dev in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.

Use with an agent

Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.

Read https://sourceweft.com/mcp/dev.classifier%2Fclassifier/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.

Read the install guide the agent follows

Other MCP clients

Add this to your client's mcpServers config.

{
  "mcpServers": {
    "classifier-dev": {
      "type": "http",
      "url": "https://classifier.dev/mcp"
    }
  }
}

README

classifier.dev

Zero-shot text classification. Plain text in, a label and a calibrated confidence out. No key, no signup. Up to a thousand texts per request.

curl https://classifier.dev/spam,not+spam/Win+a+free+iPhonespam
curl "https://classifier.dev/?labels=spam,not+spam&text=Win+a+free+iPhone"   # same call, query formspam

Single Cloudflare Worker. No database, no framework, no build step beyond esbuild.

CLI

npm i -g classifier-devclassify bug,feature,praise < feedback.txt

cli/ is a separate npm package (classifier-dev, bin classify): one dependency-free Node file, tests against a mock API (npm test), semver with its own CHANGELOG, released with npm run release patch|minor|major which tags cli-v<version> and lets .github/workflows/publish-cli.yml publish (needs an NPM_TOKEN repo secret). It talks to the API exactly like curl does.

Layout

src/index.ts    routing, validation, tiers, LLM fallback chain, analyticssrc/query.ts    the GET query form, read and written with nuqs; the URL an error suggestssrc/jev.ts      TypeSafe's Jev: packs inputs into requests, reads probabilitiessrc/limiter.ts  Durable Object: per-IP rate limitingsrc/report.ts   digest — Analytics Engine SQL -> Resend, flags model fallbackssrc/alerts.ts   every 15 minutes; emails only when something is wrongsrc/feedback.ts agent feedback, feedback.now protocol -> emailsrc/privacy.ts  keyed pseudonyms: nothing kept points back at a callersrc/cost.ts     per-request upstream spend, from the providers' own accountingsrc/docs.ts     the site (GET / and GET /benchmark), plain textsrc/home.ts     the same two documents rendered, for browsers onlysrc/ui.ts       the shared look: markdown in a terminalcli/            the `classify` command, published to npm as classifier-deveval/           benchmarks; read eval/README.md before quoting a numberfinish-dns.sh   one-shot DNS wiring, see belowwrangler.example.toml  the Worker config, minus the account-specific ids

The site

curl classifier.dev prints plain text, exactly as it always has. A browser sends Accept: text/html and gets the same document rendered — headings, bracketed links, copy buttons — from src/home.ts. Nothing is duplicated: the page is generated from DOCS and BENCHMARK at request time, so the text stays canonical and the two cannot drift. ?format=text opts out by hand, and both responses carry Vary: accept.

Agent feedback

Implements the feedback.now protocol (schema 1.1), so any agent that speaks it can report a problem without being told how:

GET  /.well-known/agent-feedback.json   what this host acceptsGET  /api/v1/policy                     categories, severities, limitsPOST /api/v1/feedback                   full structured reportPOST /api/v1/observations               lighter signalPOST /api/v1/feedback/{id}/attachments  more evidence, laterGET  /api/v1/receipts/{id}              did it land, and was it any good

Accepted submissions are emailed to REPORT_TO. Reports are kept in KV for 90 days. A repeat of the same domain + surface + category + title is stored and acknowledged as a duplicate but not emailed again, so one looping agent cannot empty itself into the inbox; the hourly budget is 100 per IP and the remainder comes back on every receipt.

Testimonials use signal.category: "testimonial". They require reporter.agent_type and reporter.agent_description, so praise arrives with enough context to understand what kind of agent benefited and how it works.

quality_score is a deterministic function of how complete the report is — an agent can read the rule and write a better one next time. Nothing here calls the classifier or Analytics Engine: this is where reports arrive saying those are broken, so it must work when they do not.

Deploy

Merging to main deploys. .github/workflows/deploy.yml typechecks, runs the Worker and CLI tests, runs wrangler deploy, and then asks the live service for /v1/health and one classification, so a deploy that uploads a broken Worker fails in CI rather than in somebody's terminal.

By hand, to try something before it is merged:

cp wrangler.example.toml wrangler.toml     # once, then fill in your own idsnpx wrangler deploy

wrangler.toml is gitignored and holds the two values that are specific to one Cloudflare account: account_id, and the STATS KV namespace id that npx wrangler kv namespace create STATS hands back. The tracked wrangler.example.toml carries everything else — crons, bindings, migrations — so the deployment shape is in the repository and only the identifiers are not.

CI has no wrangler.toml, so .github/render-wrangler.mjs writes one from the example and three repository secrets. That makes the example the deployed shape rather than a copy of it: change a binding in wrangler.toml alone and CI keeps deploying the old one.

Repository secrets the deploy needs:

CLOUDFLARE_API_TOKEN    dash.cloudflare.com > My Profile > API Tokens >                        Create Token > "Edit Cloudflare Workers"CLOUDFLARE_ACCOUNT_ID   the account_id from wrangler.tomlSTATS_KV_ID             the STATS namespace id from wrangler.tomlREPORT_TO               where the daily digest goes

Secrets set with wrangler secret put live on the Worker, not in the script bundle, so a deploy leaves them alone and CI never needs to know them.

Secrets the Worker reads: TYPESAFE_API_KEY, AI_GATEWAY_API_KEY (Vercel's AI Gateway, which serves Jev on a free monthly credit; when set it is asked first and TypeSafe catches what it refuses), OPENROUTER_API_KEY, CONTEXT_API_KEY (context.dev, the chat's web search and page reads), RESEND_API_KEY, CF_ANALYTICS_TOKEN, REPORT_KEY, PRIVACY_SALT. Add one with npx wrangler secret put NAME; none of them are ever read from the repository. src/index.ts lists the rest in the Env interface.

Secrets are compared with secretEquals (src/secrets.ts), never ===: a plain comparison returns on the first wrong byte and tells a caller how much of a guess was right.

The model

Both tiers answer from TypeSafe's Jev, a decision model rather than a language model: it takes a state and typed questions and returns a calibrated probability per option, in ~150ms. That shape is why the API can do three things the LLM version could not.

A thousand inputs per request. State is an array of {id, text} and each input gets its own question, so the whole batch is one upstream call. The documented limit is 64k tokens per request; jev.ts packs to a conservative budget and runs the resulting requests eight at a time. Measured: 400 news headlines classified in 650ms end to end, and packing 100 items scored the same as sending them one at a time.

Confidence that means something. On 400 six-way emotion items, answers at

= 0.9 confidence were right 82% of the time and answers below 0.5 were right 29%. The previous model's logprob "confidence" put 87% of news items above 0.9 and was right on 68% of those. So tier: "smart" now means: re-ask the single-label answers below 0.7 of a fast reasoning model and replace them, marked escalated: true. Nothing else changes. Which model matters: on exactly the items Jev is unsure about, deepseek-v4-flash, qwen3.7-flash and mercury-2.5 were no better than Jev; gemini-3.8-flash took news topics from 87.5% to 90.0% and emotion from 61.8% to 63.7%, so that is the chain. A frontier model (claude-fable-5.1) gets 72.3% / 90.7% at ~3x the price; the numbers are on /benchmark if that trade ever looks worth it.

Multi-label in one pass. One yes/no question per label, labels at >= 0.7 returned most-likely-first with the full score map. F1 0.887 on the seven-case set against 0.799 for the sweep-and-verify LLM cascade it replaced, in 230ms instead of 1.5s. Re-judging its candidates with the reasoning model made it worse (and took 23s), so multi-label ignores the tier.

The LLM chains in index.ts remain as the fallback when TypeSafe is unavailable, limited to twenty inputs because they are one call per input. The digest reports which model actually answered, with a FALLBACK marker, because the previous primary was delisted upstream and served its backup for weeks at F1 0.546 without anything saying so.

Updates list

The form and POST /subscribe send a confirmation email through Resend. The API returns 202 {"ok":true,"status":"pending_confirmation"}. Nothing is written to the newsletter database until the emailed token is submitted to POST /subscribe/confirm as {"token":"..."} (or with the confirmation form). GET only renders the form, so mail scanners cannot confirm subscriptions.

Tokens are signed with NEWSLETTER_CONFIRMATION_SECRET, expire within 24 hours, and are never returned from signup. Resend's idempotency key deduplicates repeat requests for the same inbox within each clock hour. Existing per-IP limits also apply. Signing-key rotation invalidates outstanding links.

Apply migrations/postgres/ with npm run db:migrate before deploying. The subscriber table shares the application database. Existing unconfirmed subscribers stay unconfirmed; do not backfill confirmed_at or send them updates until they confirm. An existing unsubscribe is never cleared by confirmation or by replaying an old token.

Required Worker secrets: DATABASE_URL, NEWSLETTER_RESEND_API_KEY, and a random NEWSLETTER_CONFIRMATION_SECRET of at least 32 bytes. NEWSLETTER_FROM in wrangler.example.toml must use a verified Resend sending domain. REPORT_TO is the reply address and receives notifications only for newly confirmed rows.

The subscriber table holds email, source, signup/confirmation/unsubscribe dates, which roadmap items were ticked (wants text[], holding ROADMAP keys; the ticks ride in the confirmation token and are written on confirmation), the requested latency when faster inference is selected, and an internal id. It holds no IP, request id, or classification traffic. Pending signups are not stored. Tokens and mail-provider error bodies must not be logged. migrations/postgres/0007_newsletter.sql defines the table; a repeat confirmation replaces the ticks only when it ticked something, and never clears an unsubscribe or moves the first confirmation date.

Read only confirmed, active recipients when sending updates:

SELECT email, wants, desired_latency_ms FROM subscriberWHERE confirmed_at IS NOT NULL AND unsubscribed_at IS NULL;

What people asked for first, to order the work by:

SELECT unnest(wants) AS item, count(*) FROM subscriberWHERE unsubscribed_at IS NULL GROUP BY 1 ORDER BY 2 DESC;

The Worker uses the application DATABASE_URL for newsletter and account data. Subscriber rows have no account foreign key or API traffic identifier; sharing storage does not subscribe account holders or change existing consent. The old newsletter project is retained as a read-only migration archive. See docs/postgres-setup.md for the verified cutover and recovery procedure.

The copy is one constant — ROADMAP in src/newsletter.ts. The plain text at curl classifier.dev, the form on the rendered page and index.md all read it, so a change to the roadmap changes all three or none.

Jev long context

Default Jev and explicit model: "jev" inputs over 32,000 characters use Chonkie RecursiveChunker with 600-token cl100k_base chunks, parallel Jev screening for relevant or uncertain evidence (including opposing evidence and exceptions), then final Jev over whole eligible chunks in source order. The final evidence budget is 20,000 cl100k_base tokens plus a conservative provider context check. Eligible chunks can be omitted when that budget fills; usage.long_context reports selection counts, tokens, calls and timing. This does not guarantee full-document final reading or universal accuracy.

Use Fast with a workspace key backed by paid balance or an active paid subscription; anonymous access and signup credit do not qualify. Synchronous limits: 250,000 original context tokens summed across inputs, 20 documents, 32 decisions, and a 1 MB body. Retail is $0.084/M original cl100k_base context tokens, counted once across inputs regardless of dimensions and actual inference usage. Each continuous whitespace or non-whitespace run is limited to 8,192 UTF-16 code units. Longer runs return 400 long_context_input before document tokenization or chunking, bounding tokenizer work on pathological inputs. No eligible evidence returns 422 long_context_no_evidence without charge. Explicit model: "chunklaya" keeps its separate legacy opt-in behavior.

Send one whole document of up to 10 million input tokens (100 MB) to POST /v1/classify with a funded workspace key. Use the normal JSON input and labels, or send text/plain with a labels query parameter. Large uploads return 202 with a status_url; Prefer: respond-async also works for smaller documents. The server splits, screens, retries and finishes automatically. The repository CLI handles upload and polling with node cli/classify.js a,b --document document.txt --json.

The exact whole-document token count sets the reservation and $0.084/M charge. Source text is stored privately while queued and deleted as it is screened. Final Jev reads at most 20,000 selected evidence tokens. Completion, failure, cancellation and 24-hour expiry delete remaining source and evidence; failed or canceled work is refunded. Results remain available until expiry.

Analytics

Every request writes one Analytics Engine datapoint (tier, label-set fingerprint, country, status, count, latency). No request text is written to analytics.

Long-context aggregates also go to classifier_long_context_events, with context/document totals, chunk screening/selection/omission counts, phase input tokens (including unknown-count flags), answered-call counts and phase timings. Account events append the corresponding fields; their ordered column contract is in src/server/analytics/schema.ts. Documents and original context tokens count once; chunk and phase counters accumulate across dimension passes. No document text or caller identifiers are written to the dedicated dataset.

Every request also records what it cost us: OpenRouter returns the charge for a call when asked, and Jev is billed on the input tokens it reports, at the rate eval/bench.py prices the benchmarks with. Spend accumulates in a per-request meter (src/cost.ts) and lands in double3. That column was added after launch, so it reads 0 for anything older than that deploy.

A cron at 15:00 UTC queries it and emails a digest via Resend.

Alerts

A separate cron runs every fifteen minutes and stays silent unless something fires. It only watches conditions with an action attached: the Jev key being refused, Jev not answering (the fallback chain serving quietly, which has happened), 5xx rates, smart-tier escalations failing (the shape an exhausted OPENROUTER_API_KEY takes), mean latency, a spend spike against the trailing day, and traffic stopping outright. 4xx is ignored — that is scanners probing for /wp-admin, not a fault.

Jev credits. TypeSafe publishes no balance endpoint — its API is /v1/systemone and /v1/models, nothing else — so there is no number to watch. Instead the check calls /v1/models with the key every fifteen minutes and reports back whatever TypeSafe says: a 401, 402 or 403 there means out of credit, revoked or wrong, and raises a critical alert quoting TypeSafe's own message rather than guessing which status means what. Because it probes rather than waiting for traffic, it fires on a quiet host before any caller meets the fallback chain, and it runs even when Analytics Engine is down.

Each condition emails once when it starts, again every six hours while it lasts, and once when it clears, with the state in KV under alert:. Thresholds are the T object at the top of src/alerts.ts.

curl -H "authorization: Bearer $REPORT_KEY" https://classifier.dev/alertscurl -H "authorization: Bearer $REPORT_KEY" "https://classifier.dev/alerts?demo=1&send=1"

The first previews without sending or touching state; the second emails a sample through the real path, to prove delivery works.

Preview the digest any time without sending it:

curl -H "authorization: Bearer $REPORT_KEY" https://classifier.dev/report

Jev provider attempts are stored separately in classifier_jev_attempts through JEV_AE, including recovered failures, retries and gateway cooldown skips. The report includes status, reason, count and mean latency for each provider; /alerts shows current incidents even when their notification is suppressed. The existing 15-minute alert check warns when at least three attempts fail and failures exceed 5% for either provider. Counts account for Analytics Engine sampling. No input text, caller identifiers or upstream messages are stored. Successful simple and multi-label classifier names are retained for 90 days in a separate aggregate KV record with no caller or source-text fields. Its keyed fingerprint also appears in per-request analytics, allowing operators to resolve label names in those pseudonymous records.

AI_GATEWAY_DISABLED = "true" in wrangler.example.toml keeps production on TypeSafe directly after the gateway repeatedly returned 429 on September 19. The gateway key is retained. To restore gateway-first routing, verify gateway capacity, change this variable to "false" in the example and local config, and deploy. Check provider attempts and the live API tests after re-enabling.

The header is the only way in. A query string lands in access logs, in browser history and in the Referer header of whatever gets clicked next, so ?key= is gone. Append ?send=1 to actually email it.

Cloudflare's Analytics Engine SQL is a narrow ClickHouse subset — no uniq(), no SELECT DISTINCT, and a bare SELECT col ... GROUP BY col is rejected. Distinct counts therefore use SELECT col, count() ... GROUP BY col and count the returned rows. Each query is isolated so one failure cannot blank the report.

Privacy

Two columns in that dataset used to be the caller: the IP address, and the label set, joined and lowercased. Both are keyed hashes now (src/privacy.ts), so the figures still count distinct callers and distinct classifiers and nothing can be read back into an address or into somebody's wording. The caller hash takes the UTC day as well, so it stops being the same value tomorrow — which is why a unique-caller count over 7d or 30d is really caller-days.

Set the key once, and treat it as a secret like any other:

npx wrangler secret put PRIVACY_SALT     # 32 random bytes

Rotating it renumbers every fingerprint, so distinct counts double-count across the rotation. It falls back to ADMIN_SIGNING_KEY, then REPORT_KEY, then a per-isolate random value, because an unkeyed hash of an IPv4 address or of a common label set inverts in seconds.

Eval

npm run bench                 # multi-label, 7 cases: jev vs any OpenRouter modelnpm run single -- --dataset emotion --backend jevnpm run single -- --dataset ag_news --backend openrouter:qwen/qwen3.7-flashpython3 eval/escalate.py --dataset emotion     # what the smart tier buys

single.py downloads AG News and dair-ai/emotion test rows on first use and caches raw results under eval/data/results/ so escalate.py can combine backends without re-spending. eval/README.md lists the caveats.

Rate limiting

Per IP in a Durable Object, counted in classifications: 3,000/min and 20,000/day on fast, 200/min and 2,000/day on smart.

Two other approaches were tried and rejected:

  • Cloudflare's native ratelimit binding registers fine but never decremented (70 calls against a limit of 60 all returned success: true).
  • KV is edge-cached and eventually consistent, so a counter written this second is invisible to the next read — every request saw remaining: 59.

A Durable Object is single-threaded and strongly consistent, which is what a counter needs. Verified at the original 60/min: 75 requests -> 60 × 200, 15 × 429.

DNS

The domain is registered at Porkbun; the Worker is on Cloudflare. Cloudflare Workers custom domains require the zone to live in Cloudflare, and neither API token here has zone.create, so that one step is manual:

  1. https://dash.cloudflare.com -> Add a domain -> classifier.dev -> Free plan
  2. ./finish-dns.sh — reads the assigned nameservers, points Porkbun at them via the Porkbun API, and attaches the Worker to the apex and www.

Agent skill

npx skills add https://classifier.dev

Served from this domain over RFC 8615 well-known discovery, so there is no repository in the middle:

src/SKILL.md                          the skill, bundled as a Text moduleGET /skill.md                         the artifactGET /.well-known/agent-skills/index.json   discovery, schema v0.2.0

The index must carry a sha256 of the artifact, and an index that disagrees with the file makes the skill uninstallable. Rather than commit a digest that a later edit would silently invalidate, src/skill.ts hashes the bytes it actually serves, once per isolate. Editing SKILL.md is therefore enough; nothing else needs updating.

The skill teaches the case the API pitch misses: you are already a model and can classify anything you can see, so the reason to call out is context, not capability — filtering forty search results down to six without reading forty.

Two things it documents because testing found them the hard way. Cloudflare 403s Python's stdlib urllib User-Agent before the request reaches the Worker, so the recipe sets one explicitly. And filters should be told "when in doubt, keep it": on a ten-snippet research filter that took signal kept from 4/6 to 6/6 with no extra noise, where adding a third "possibly relevant" label did nothing.

Discovery surfaces

GET /openapi.json               OpenAPI 3.1, also at /.well-known/openapi.jsonGET /llms.txt                   short index for agents, linked from robots.txtGET /benchmark                  measured accuracy, cost, latency

Both are linked from the third paragraph of GET / so an agent reading the landing page finds them immediately.

Known issue: Cloudflare's managed robots.txt

Adding the zone enabled Cloudflare AI Crawl Control, which prepends a managed block to /robots.txt disallowing GPTBot, ClaudeBot, CCBot, Google-Extended, Bytespider, Amazonbot and meta-externalagent. The Worker's own robots.txt is appended after it and cannot override it.

This blocks training crawlers, not runtime API consumers — any agent can still call the API. But it does keep the docs out of future model training data, which works against discovery. The toggles at dash.cloudflare.com -> classifier.dev -> AI Crawl Control -> Security did not persist when flipped, so this likely needs a plan-level change or support.

Operator agent access

For operator-owned bulk agent work, set a dedicated AGENT_API_KEY Worker secret and send it as Authorization: Bearer .... Classification quotas are unmetered, but inference uses a separate $2/day operator allowance (OPERATOR_DAILY_USD), shared across IPs, with the same request and concurrency limits as free usage. It neither consumes nor expands the anonymous allowance and does not replace ENTERPRISE_API_KEY. The CLI accepts it through CLASSIFY_API_KEY or CLASSIFIER_API_KEY. It does not authorize private reports or admin access. Keep it in an ignored secret file; never give it to public clients. Anonymous quotas continue to apply to unauthenticated traffic.

For a funded workspace key with an arranged bulk quota, set the API_KEY_RATE_LIMIT_MULTIPLIERS Worker secret to a JSON object mapping the SHA-256 hex digest of each key to an integer multiplier (1–1,000). Overrides only raise the admission quota for that key; account counters, balance charging, model capacity limits, and other keys remain unchanged. Remove the entry to restore the plan quota.

Finding the classification code

The domain terms are defined in CONTEXT.md.

ModuleResponsibility
src/index.tsHTTP validation, quota, model selection, smart escalation, fallback, and response formatting.
src/jev.tsJev questions and answers, shared batch budgets and recovery, gateway/direct transport, and provider validation.
src/dimensions.tsDimension definitions and the mapping from input–dimension decisions to Jev questions and back.

Both ordinary and dimension classification use the same Jev batching module. A question group is the smallest part of a request that recovery keeps together: one input's questions for ordinary classification, one decision for dimensions. Batch preparation accounts for shared input text once, checks both provider budgets, and runs before quota charging for dimensions. Execution limits concurrent requests and splits an oversized batch between question groups.

Change provider budgets and recovery in src/jev.ts; keep dimension meaning in src/dimensions.ts. Test the posted requests and returned classifications through jevClassify and the Worker HTTP interface, without depending on packing internals.

Multiple dimensions

POST /v1/classify also accepts items and dimensions:

json
{  "items": ["Checkout charges me twice"],  "dimensions": {    "team": ["billing", "identity", "platform"],    "urgency": {      "labels": ["immediate", "normal", "low"],      "instructions": "Active financial harm is immediate."    },    "kind": ["bug", "request", "question"]  }}

Each results[i].dimensions[name] carries its own label, confidence, scores, model and latency. Jev shares state across item–dimension questions, packing both context limits. Smart escalation is per field and clears the original scores; unavailable Jev falls back only up to 20 decisions. The API accepts up to 20 dimensions and 1,000 decisions, with each decision charged to quota. The classify_dimensions MCP tool uses this same path.

Analytics adds blob9 (single/multi/dimensions), double6 (successful input items), double7 (dimension count), double8 (uncertain or unscored fields), and double9 (fields served by fallback). double1 counts successful decisions. Dimension configurations are keyed fingerprints, never stored verbatim. The admin panel queries dimension traffic separately, including failures and caller-days. The existing alert cron flags three or more dimension 5xx in an hour above 10% of dimension requests, plus any dimension fallback usage.

Run npm run test:e2e with Node 22.18+ against the real production API, or set CLASSIFIER_BASE_URL to a staging origin. These are named TypeScript tests using node:test and real HTTP/model calls, with no fetch or inference mocks. Set CLASSIFIER_API_KEY to an authorized key when the shared IP quota is used up; it is optional. The suite uses inference and counts toward normal quotas.

The tests check exact decisions and ordering for a 300-decision batch, every field's allowed labels and probability distribution, real smart escalation, provider score preservation, aliases, validation errors, MCP, OpenAPI, and legacy calls. Responses start as unknown and are validated before becoming typed matrices. Smart tests require an actual escalation; model drift that makes every fixture confident fails the test instead of silently skipping the reasoning provider.

npm run typecheck checks the Worker and the TypeScript live tests. Deployment CI runs the live suite after publishing; npm test stays offline. Fault injection remains in test/dimensions.test.ts and test/dimensions-observability.test.ts.

Run the live API tests workflow manually from GitHub Actions (or gh workflow run e2e.yml) to check production without deploying a Worker.

Source: README.md at commit a17bf2b

Tools

5

classify_texts

Classify texts into one label each

Sort up to 1,000 texts into exactly one of your own labels each, with confidence per answer. Use this when you have many items to triage, route, filter or bucket and do not want to read them all: search results before opening them, tickets, log lines, changed files, feedback. Do not use it for fewer than about five items you can already see — just decide. For default Jev, confidence is calibrated (answers >= 0.9 are right ~82-92% of the time; < 0.5 about 30-60%); these measurements do not apply to experimental Laya. so act on the sure ones and look at the rest yourself, or pass tier "smart" to have the unsure ones re-asked of a reasoning model.

Parametersurltiermodelinputslabelsrequiredincludeprocessingshare_datainstructions

classify_dimensions

Classify several dimensions per text

Classify each text by several named dimensions, such as team, urgency and kind, in one request. Returns a label, confidence, scores and model for each field. At most 1,000 item × dimension decisions; every field counts toward the quota. Use per-dimension instructions to define ambiguous categories.

Parametersurltieritemsmodelincludedimensionsrequiredprocessingshare_datainstructions

classify_multi_label

Tag texts with every label that applies

Like classify_texts, but each text gets every label that applies (possibly none), with an independent 0-1 score per label. Use this for tagging — topics of an article, components touched by a ticket — where one answer is not enough. Set max_labels to cap how many come back per text. Labels scoring >= 0.7 are kept.

Parametersurlmodelinputslabelsrequiredincludemax_labelsprocessingshare_datainstructions

count_labels

Count how many texts fall under each label

Classify up to 1,000 texts and return only a histogram: how many landed on each label, and how many the model was unsure about. Use this when you want the shape of a corpus — what share of feedback is bugs vs praise, how many search results are relevant — without pulling a thousand individual answers into context. Use classify_texts when you need the answer per item.

Parametersinputsrequiredlabelsrequiredshare_datainstructionsunsure_below

review_uncertain

Find the texts the classifier was unsure about

Classify up to 1,000 texts and return only the ones whose confidence fell under a threshold (default 0.7), each with its two most likely labels. Use this after a bulk classification to decide which items deserve your own attention: the confident answers can be trusted, these are the ones to read. Returns the index of each item so you can map back to your list.

Parametersbelowinputsrequiredlabelsrequiredshare_datainstructions

Version history

1
  1. v1.0.0LatestSep 19, 2026

Score

100% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 daysRequired25 pts
  • Auth declaredRequired10 pts
  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts

Other Classifier integrations