Skip to content

Add decision model bindings for Cloudflare Clef - #662

Merged
ndisidore merged 7 commits into
mainfrom
feat/classifier-binding
Oct 13, 2026
Merged

ndisidore merged 7 commits into
mainfrom
feat/classifier-binding

Conversation

@ndisidore

@ndisidore ndisidore commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Lets gadgets bind Cloudflare's Clef and Clef Flash. These are decision models: JSON state + typed questions in, a probability per allowed answer out. No chat, tools or text output, so they can't back a chat, a spawned agent or LanguageModelBinding.run(). Gadgets get a new DecisionModelBinding instead:

const { intent } = await env.DECISION_MODEL.decide({
  state: { message: "My order never arrived." },
  questions: { intent: { type: "choice", instructions: "What does the customer want?",
    criteria: { refund: "Money back or a replacement", other: "Anything else" } } },
});
// { type: "choice", choice: "refund", probabilities: { refund: 0.96, other: 0.04 }, confidence: 0.86 }
  • ModelKind ("language" | "decision") is part of the shared API. SUGGESTED_MODELS entries carry kind, and modelKind(provider, model) reads it.
  • Reuses the AI Model connection. newAiModelGatekeeper() mints a DecisionModelGatekeeper when the model's kind is "decision". Creation spec, sharing and analytics unchanged.
  • Never offered to chats or spawners: listModels(kind = "language") lists one kind, so listModels("decision") returns the decision models and a future kind needs no new method.
  • Blueprint aiModel bindings record modelKind: "decision", so installers pick among decision models.
  • state is any (JSON-compatible), as JSON.parse() returns; Workers AI also takes plain text there.
  • Routing follows getModel(): direct with the user's Cloudflare creds, or through the platform AI Gateway. On the WORKERS_AI binding that's binding.run(model, input, {gateway}), since the gateway's passthrough refuses /run.
  • Admin "Disabled" stops bound gadgets. No quota or cost accounting, same as the LLM binding today.

Known gap: doesn't work on deployments that reach AI Gateway over HTTPS (gateway in another account). The gateway supplies Workers AI's credential only for chat, and reusing the gateway token for /run gets a 401 even when it has Workers AI permissions. Checked live on three accounts. Docs say to disable Clef there; I raised this with the AI Gateway team and am waiting to hear back. Deployments on the default binding route are unaffected.

Naming: "decision model", the industry term and the one Cloudflare's Clef docs use. pi calls these "classifiers" (type: "classifier", classify()), so its own Classifier* names remain only where the code calls pi.

Prompt (against pi): "Please look at the last 10 commits in this repo and classify as either a new feature, a bugfix, docs changes, a refactor, or none of the above"
image
image

@github-actions github-actions Bot added workshop/frontend Changes to the Workshop frontend kernel Changes to the Workshop kernel workshop/shared Changes to shared Workshop APIs labels Oct 5, 2026
@ndisidore
ndisidore force-pushed the feat/classifier-binding branch from 5b50949 to 5d26884 Compare October 5, 2026 15:53
@ask-bonk

ask-bonk Bot commented Oct 5, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Preview: pr662-feat-classifi-0f0dbda7

https://pr662-feat-classifi-0f0dbda7-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@ndisidore
ndisidore force-pushed the feat/classifier-binding branch from 5d26884 to 0782057 Compare October 5, 2026 23:52
@ask-bonk

ask-bonk Bot commented Oct 5, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions github-actions Bot deleted a comment from ask-bonk Bot Oct 6, 2026
@ndisidore
ndisidore marked this pull request as ready for review October 6, 2026 14:58
devin-ai-integration[bot]

This comment was marked as resolved.

@ndisidore
ndisidore force-pushed the feat/classifier-binding branch from 0782057 to 4847fd4 Compare October 6, 2026 16:04
@ask-bonk

ask-bonk Bot commented Oct 6, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions github-actions Bot deleted a comment from ask-bonk Bot Oct 6, 2026
@ndisidore
ndisidore force-pushed the feat/classifier-binding branch from 4847fd4 to dbc6df6 Compare October 6, 2026 20:16
@ask-bonk

ask-bonk Bot commented Oct 6, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions github-actions Bot deleted a comment from ask-bonk Bot Oct 6, 2026
Comment thread packages/workshop-shared/src/api.ts Resolved
Comment thread packages/workshop-shared/src/api.ts Resolved
Comment thread plans/classifier-models.md Resolved
pi 1.0.2 adds Cloudflare's Clef and Clef Flash to the Workers AI
classifier catalog, and accepts Clef's response: its answers come back
directly, where Jev's come wrapped in a run record that the transport
used to require. The classifier bindings in the next commit need both.

The only other change on Workshop's request paths: Anthropic models
whose pi compat enables mid-conversation tool changes now send the
inline-tools-2026-09-15 beta in place of
mid-conversation-tool-changes-2026-07-01. Workshop declares its tools
only in the initial system message, so its requests are otherwise
unchanged. pi-agent-core drops its harness, node and search modules,
which Workshop doesn't import.
Clef and Clef Flash are Workers AI decision models: given a JSON state
and typed questions, they return a probability for each allowed
answer. They don't chat, so they can't run a chat, a spawned agent or
LanguageModelBinding.run().

A gadget now binds one through the existing AI Model connection and
calls ClassifierModelBinding.classify({state, questions}).
newAiModelGatekeeper() mints a ClassifierModelGatekeeper when the
model is a classifier, i.e. a SUGGESTED_MODELS entry marked
`classifier: true` (see isClassifierModel()), and a
LanguageModelGatekeeper otherwise. The creation spec, sharing rules and
analytics are unchanged. An aiModel blueprint binding records
`classifier: true` so installers pick among classifiers, and the
agent's views of a blueprint name the kind.

listModels() now returns chat models only, and the new
listClassifierModels() returns the classifiers, so chats, spawners and
the new-conversation default never offer one.

Routing follows getModel(): through the platform AI Gateway when one
is configured, otherwise straight to Workers AI with the config's own
credentials. The gateway supplies Workers AI's credential only for
chat requests, so over the WORKERS_AI binding the request goes through
the binding's own run(), which still routes through the gateway. Over
HTTPS, /run wants a Workers AI credential as `Authorization`, and a
request sending the gateway token there is refused even when the token
grants Workers AI. Until AI Gateway authenticates /run as it does
chat, classifiers fail on deployments without the binding transport,
and the docs have those deployments disable them. As with the LLM
binding, there is no quota, BYOK routing or cost accounting.

A classifier session runs the admin check LanguageModelGatekeeper
already ran, now shared as refuseRevokedModel(), so disabling Clef on
the admin Models tab stops the gadgets bound to it.

See plans/classifier-models.md.
The AI Model connection lists chat and classifier models under
separate headings, starting on the first chat model. A blueprint's
classifier binding picks from, and auto-suggests against, classifier
models, and says how to add one when there are none. Spawner pickers
keep the chat list.

The Providers page lists classifiers beside chat models, without the
quick-model action: the quick model writes chat and gadget titles,
which a classifier can't. Onboarding's add-model dialog leaves
classifiers out, since its model step picks the default chat model.

On the admin Models tab a classifier keeps its three modes and gains a
Classifier badge, but has no Test or Settings: the test sends a chat
request, and the reasoning level and compaction budget only shape
chats.
getClassifier() returns from each route instead of reassigning a
base URL and options in three branches, and builds the gateway
metadata once.

The user DO's listModels() now takes the kind, so
listClassifierModels() stays only on AuthenticatedApi, which asks for
the classifiers. ModelKind names the kind where the lists take it.

The gateway cost test's filter for the offered chat models becomes
one chatIds() helper.
The Providers page keeps the chat and classifier lists as fetched and
combines them during render, rather than storing the combined list
beside a set of the classifier IDs.

The BlueprintLandingPage and GatekeeperModal tests share their mount
and picker-opening steps instead of repeating them per test.
"Decision model" is the industry's term, and the one Cloudflare's
Clef docs use; "classifier" was pi's, which is an implementation
detail. The gadget-facing binding becomes DecisionModelBinding.decide(),
bound as DECISION_MODEL; pi's own Classifier* names stay where they
name pi's exports.

ModelKind ("language" | "decision") moves into the shared API, where
SuggestedModel.kind and BlueprintBinding.modelKind replace the
classifier flags, and modelKind() replaces isClassifierModel(). One
AuthenticatedApi.listModels(kind?) replaces listClassifierModels(),
so a future kind needs no new method.

DecisionRequest.state is any, as JSON.parse() returns, rather than a
JsonObject the d.ts had to define: it only has to be JSON-compatible,
and Workers AI also takes plain text there.
The pickers, the Providers page and the admin model rows follow the
kernel's rename: they ask listModels("decision"), test a blueprint
binding's modelKind, and label the groups Language and Decision
models.
@ndisidore
ndisidore force-pushed the feat/classifier-binding branch from dbc6df6 to cdd6316 Compare October 11, 2026 14:01

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

@ask-bonk

ask-bonk Bot commented Oct 11, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions

Copy link
Copy Markdown

Eval results

Verdict: ⚪ Unchanged. No task moved beyond what 10 runs can tell apart from noise.

Task Score Δ score Fisher test Cache hits Avg min Avg steps
change-calendar 90% → 70% −20 pp p = 0.58 95%
0 pp
2.8 → 3.0 26.7 → 23.6
chess 80% → 100% +20 pp p = 0.47 98% → 97%
−1 pp
6.9 64.5 → 52.3
incident-desk 100% → 90% −10 pp p = 1.00 97% → 96%
−1 pp
3.6 → 3.5 38.6 → 29.4
worker-logs 100% → 90% −10 pp p = 1.00 95% → 96%
+1 pp
3.5 → 3.1 26.4 → 26.5
Failed checks
Task Check Failed
change-calendar t1 schedules-and-lists-windows 0/10 → 1/10
change-calendar t1 rejects-invalid-windows-without-changing-anything 0/10 → 1/10
change-calendar t1 overlap-is-per-service-and-touching-is-allowed 0/10 → 1/10
change-calendar t2 plan-is-a-document-listing-exactly-the-weeks-windows 0/10 → 1/9
change-calendar t4 names-the-booked-windows-the-new-rules-reject 1/10 → 0/8
change-calendar t4 earliest-available-applies-every-rule 0/10 → 1/8
change-calendar t4 earliest-available-matches-the-reference 0/10 → 1/8
change-calendar t4 earliest-available-slots-are-bookable 0/10 → 1/8
change-calendar t4 earliest-available-sees-new-bookings 0/10 → 1/8
chess t1 agrees-with-the-oracle-on-the-hard-positions 2/10 → 0/10
chess t1 agrees-with-the-oracle-on-perft-positions 2/10 → 0/10
chess t1 agrees-with-the-oracle-through-random-games 2/10 → 0/10
incident-desk t1 opens-acknowledges-and-resolves-in-order 0/10 → 1/10
incident-desk t1 simultaneous-acknowledges-yield-exactly-one-owner 0/10 → 1/10
incident-desk t1 simultaneous-opens-of-one-id-admit-exactly-one 0/10 → 1/10
incident-desk t1 board-lists-by-severity-then-age 0/10 → 1/10
worker-logs t1 ingests-resets-and-summarises-per-worker 0/10 → 1/10
worker-logs t1 hourly-buckets-cover-every-hour-including-empty-ones 0/10 → 1/10
worker-logs t1 ranges-are-half-open-and-filter-by-worker 0/10 → 1/10

Run · trajectories and raw results

@github-actions github-actions Bot deleted a comment from ask-bonk Bot Oct 11, 2026
@ask-bonk

ask-bonk Bot commented Oct 11, 2026

Copy link
Copy Markdown

🔬 Eval runs review

Performance

Across 10 runs per task, main → PR pass rates were change-calendar 90% → 70%, chess 80% → 100%, and incident-desk and worker-logs 100% → 90%; comparison.json calls all four unchanged, with pass-rate p-values ≥ 0.47. Mean run costs fell respectively from $0.0206 → $0.0184, $0.0552 → $0.0475, $0.0274 → $0.0215, and $0.0224 → $0.0192, but variation was substantial and several candidate failures stopped early; cache hits stayed around 95–98%, with no significant hit-rate or break-rate changes. Calendar lost the most runs, while chess remained the largest cost and step consumer: 64.5 → 52.3 model steps at roughly 6.9 minutes on both sides, with PGN escaping repairs and exact-text edit retries adding detours.

⚪ VERDICT: NO REGRESSION FROM THIS PR

The candidate failures trace to generated gadget mistakes or an unchanged verifier, not the decision-binding additions or blueprint labels, and the pi upgrade has no demonstrated beyond-noise improvement or regression in these runs.

Triage

Failure modes

  • Rejecting valid timestamp spellings · change-calendar 1/10 · model error · this PR: no — Trial 4, turn 1, writeFile(server.js) required timestamps to equal toISOString(), rejecting valid inputs without .000; its executeCode smoke test used only .000Z timestamps and therefore missed the defect.
  • Undefined slot-search helper · change-calendar 1/10 · model error · this PR: no — Trial 6, turn 4, editFile(server.js) introduced calls to ceilQuarterHour without defining it. Subsequent executeCode verified the booking-audit reply, not earliestAvailable, leaving every search check throwing; baseline searches passed.
  • Human-readable headings rejected · change-calendar 1/10 · verifier · this PR: no — Trial 7, turn 2, executeCode(setDocument) produced the correct windows under “API Gateway”, “Billing”, and “Edge Cache”. The prompt asks for service headings without requiring literal slugs, but the unchanged verifier requires headings to contain api-gateway and edge-cache; baseline documents used those slugs.
  • Empty SQL lookup treated as nullable · incident-desk 1/10 · model error · this PR: no — Trial 8, turn 1, writeFile(server.js) used .one() for the duplicate-ID lookup before inserting a new incident, so the empty lookup threw and nothing opened. There was no RPC smoke test; baseline implementations handled absent rows with arrays or conditional inserts.
  • Oversized SQL insert batches · worker-logs 1/10 · model error · this PR: no — Trial 9, turn 1, writeFile(server.js) batched 100 events with six bindings each, exceeding the runtime’s SQL-variable limit. Its executeCode test ingested only three events successfully; the verifier’s larger ingest failed and left downstream reports empty, whereas baseline row-at-a-time inserts worked.

Tool errors

  • editFile: No matching text was found in the file. · chess 5 → 11, incident-desk 1 → 2, worker-logs 7 → 1 · model error — Agents quoted text that differed from the file, notably double-escaped regexes on both sides; candidate chess trial 3 recovered by grepping halfText and using the literal source.
  • editFile: Multiple matches were found. The text to match must be unique. · chess 6 → 0, incident-desk 9 → 4, worker-logs 2 → 1 · model error — Baseline chess retried repeated method-call fragments; candidate incident-desk trial 7 submitted the same repeated SQL fragment twice, then recovered by including surrounding statements.
  • editFile: Validation failed for tool "editFile": · change-calendar 1 → 1, chess 2 → 1, incident-desk 7 → 3, worker-logs 1 → 5 · model error — Calls omitted required fields or used the wrong argument names. Candidate worker-logs trial 2 omitted filename, then supplied it successfully; baseline calls made similar omissions.
  • readFile: File does not exist. · chess 2 → 2, incident-desk 4 → 2 · model error — Agents attempted to read server.js and client.js immediately after creating empty gadgets, despite the prompt explaining that scratch gadgets have no files; they recovered by writing them.
  • editFile: CHESS has no file named "CHESS". · chess 2 → 1 · model error — The binding name was also passed as filename. Candidate trial 2’s turn-3 documentation edit succeeded after changing filename to README.md; the baseline likewise corrected the filename.

What to do

  • Optional follow-up in packages/workshop-evals/evals/change-calendar.eval.ts: normalize service headings for spaces versus hyphens before comparison, so defensible display names do not count as failures.
  • Optional follow-up in packages/workshop-backend/src/agent.ts: add guidance to exercise newly added RPCs with representative inputs before declaring completion, including realistic batch sizes, and clean up probe data afterward. These runs do not require a regression fix in this PR.

github run

@ndisidore ndisidore changed the title Add classifier model bindings for Cloudflare Clef Add decision model bindings for Cloudflare Clef Oct 11, 2026
@ndisidore

Copy link
Copy Markdown
Member Author

@kentonv actioned against all your feedback and ready for re-review.

@ndisidore
ndisidore merged commit 7cd3d24 into main Oct 13, 2026
30 checks passed
@ndisidore
ndisidore deleted the feat/classifier-binding branch October 13, 2026 14:46
@kziemski

Copy link
Copy Markdown

🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kernel Changes to the Workshop kernel workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants