Skip to content

Concepts

The swarm

Jeff's open specialists — support, retrieval, reasoning, agents, engineering, safety, language, legal and compliance — are each trained and calibrated for one narrow kind of decision. A caller whose traffic mixes many kinds of request does not have to classify each one itself: send the request to a router, jeff.routing.router-open, with a menu of your specialists, and it picks which one should answer. One entry on the menu, base, means no listed specialist fits and the base model should answer the request itself, with no specialist at all.

Why route instead of calling a specialist directly

A specialist is trained for its own kind of request and is usually more accurate on it than the general base model answering the same request unprepared. Routing means your integration code does not carry its own if/else logic mapping request types to functions: it sends one request to the router and gets back which specialist to call, so adding a specialist or changing which one handles a kind of request is a change to the router's training, not to your code.

How well it routes

On 6,930 routing requests held out from its training, with menus of up to 32 entries, the router picks the right specialist (or base) for 99.8% of them, and for 99.5% of the 1,552 whose instructions were reworded.

Calling it

The router is an open adapter, so you call it through the SDK like any other function: as the input, send the request's instructions, its options (the label keys) and its state (the specialist's own state — its context fields and input — as a JSON string), with your menu of specialists as labels. Read value (the specialist it picked) and confidence, then call that specialist as usual.

Example: a failed test run

Input

instructions: What kind of failure is this CI run? Use the log, the change (if there is one) and the CI facts (attempts on this commit, the history of the same test or job on the main branch, other jobs, dependency versions compared with the last passing run, durations). Apply the first rule that fits. build_or_config: the run failed before any test ran, in compiling, type checking, linting, packaging or the CI configuration itself (a bad pipeline file, a missing secret or variable, a wrong path), even if the change caused it. environment_or_infra: the error comes from the CI machine or an outside service (network or DNS errors, a package registry or mirror down, no disk space, a lost runner, a service container that did not start), not from the code; other jobs often fail the same way. flaky_test: the test fails only some of the time on the same code: a rerun of the same commit passed, or the test fails now and then on the main branch without code changes. timeout_or_resource: a test or job was stopped for exceeding a time limit or a memory limit (out of memory, killed, exit code 137) on every attempt. dependency_change: a third-party package resolves to a different version than in the last passing run (pulled in automatically, or upgraded by the change itself) and the error comes from that package. test_needs_update: the change says this behaviour was changed on purpose, and the failing test still expects the old behaviour. real_regression: the change broke behaviour it did not mean to change: the failure repeats on every attempt and the same test passes on the main branch. Warnings or retries in the log that did not stop the run do not decide the answer. options: ["build_or_config","dependency_change","environment_or_infra","flaky_test","real_regression","test_needs_update","timeout_or_resource"] state: {"project": "tidepool, an open-source Python library for reading and writing tide-gauge data files.", "ci_run": {"job": "tests (python 3.12)", "trigger": "pull request 412", "commit": "9f3c2e1", "date": "2 November 2026"}, "change": {"title": "Speed up the CSV reader", "description": "Reads rows in blocks of 4,096 instead of one at a time. No behaviour change intended."}, "ci_facts": {"attempts_on_this_commit": "2, both failed the same way", "same_test_on_main": "passed in each of the last 30 runs", "other_jobs_in_this_run": "lint passed; docs passed", "dependency_versions": "unchanged since the last passing run", "duration": "3 min 40 s (usual 3 min 30 s)"}, "log": "tests/test_csv.py::test_reads_last_row FAILED\nE AssertionError: expected 1,000 rows, got 904\nE assert 904 == 1000\n========== 1 failed, 213 passed in 41.2s =========="}

Labels (the options)

message-triage
Triages a message using the inbox's own message kinds, routing table and rules: what it is, what action it needs, how urgent it is and who should handle it.
support-intents
Names what a customer or assistant user is asking for, from a fixed list of requests such as cancel_order or track_refund.
triage
Routes a support ticket or email to a team and scores its urgency, sentiment and need for a person.
passage-relevant
For search and retrieval: checks whether a passage answers a question for the right subject and period: yes, partly or no.
evidence-sufficient
For search and retrieval: checks whether a set of passages is enough to answer a question: sufficient, insufficient or conflicting.
entails
Checks whether the evidence given (documents, messages or records) supports a claim: supported, unsupported or unclear.
contradicts
Checks whether two statements or records conflict: yes, no, or unclear (for example a date that can be read two ways).
guard
Checks a user message or outside content for prompt injection, jailbreak and data-leak attempts, and names the kind.
spam
Says whether a text message or email is spam or phishing, and whether it is legitimate, spam or a phishing scam.
tool-route
Given a tool menu written into the request (in any common format) and the conversation, picks the tool to call next, no tool, or asking the user for a missing detail.
ci-failure
Reads a failed automated build-and-test run (its log, the code change and the run history) and says what kind of failure it is: a real regression, a flaky test, infrastructure, a dependency change, a test that needs updating, and so on.
base
No listed specialist fits this request, so the general model with no specialist answers it: everyday reasoning, reading, knowledge and judging tasks, or any task that none of the listed specialists covers.
Answer (JSON)
value ci-failure, confidence 1
Top options
  1. ci-failure 1
  2. contradicts 0
  3. guard 0

Open in playground

import os import jeff with jeff.Jeff.remote("https://api.jeffhub.ai", token=os.environ["JEFF_API_KEY"], timeout_s=30) as client:     decide = client.function("jeff.routing.router-open.specialist", labels={        "message-triage": "Triages a message using the inbox's own message kinds, routing table and rules: what it is, what action it needs, how urgent it is and who should handle it.",        "support-intents": "Names what a customer or assistant user is asking for, from a fixed list of requests such as cancel_order or track_refund.",        "triage": "Routes a support ticket or email to a team and scores its urgency, sentiment and need for a person.",        "passage-relevant": "For search and retrieval: checks whether a passage answers a question for the right subject and period: yes, partly or no.",        "evidence-sufficient": "For search and retrieval: checks whether a set of passages is enough to answer a question: sufficient, insufficient or conflicting.",        "entails": "Checks whether the evidence given (documents, messages or records) supports a claim: supported, unsupported or unclear.",        "contradicts": "Checks whether two statements or records conflict: yes, no, or unclear (for example a date that can be read two ways).",        "guard": "Checks a user message or outside content for prompt injection, jailbreak and data-leak attempts, and names the kind.",        "spam": "Says whether a text message or email is spam or phishing, and whether it is legitimate, spam or a phishing scam.",        "tool-route": "Given a tool menu written into the request (in any common format) and the conversation, picks the tool to call next, no tool, or asking the user for a missing detail.",        "ci-failure": "Reads a failed automated build-and-test run (its log, the code change and the run history) and says what kind of failure it is: a real regression, a flaky test, infrastructure, a dependency change, a test that needs updating, and so on.",        "base": "No listed specialist fits this request, so the general model with no specialist answers it: everyday reasoning, reading, knowledge and judging tasks, or any task that none of the listed specialists covers.",    })     decision = decide({        "instructions": "What kind of failure is this CI run? Use the log, the change (if there is one) and the CI facts (attempts on this commit, the history of the same test or job on the main branch, other jobs, dependency versions compared with the last passing run, durations). Apply the first rule that fits. build_or_config: the run failed before any test ran, in compiling, type checking, linting, packaging or the CI configuration itself (a bad pipeline file, a missing secret or variable, a wrong path), even if the change caused it. environment_or_infra: the error comes from the CI machine or an outside service (network or DNS errors, a package registry or mirror down, no disk space, a lost runner, a service container that did not start), not from the code; other jobs often fail the same way. flaky_test: the test fails only some of the time on the same code: a rerun of the same commit passed, or the test fails now and then on the main branch without code changes. timeout_or_resource: a test or job was stopped for exceeding a time limit or a memory limit (out of memory, killed, exit code 137) on every attempt. dependency_change: a third-party package resolves to a different version than in the last passing run (pulled in automatically, or upgraded by the change itself) and the error comes from that package. test_needs_update: the change says this behaviour was changed on purpose, and the failing test still expects the old behaviour. real_regression: the change broke behaviour it did not mean to change: the failure repeats on every attempt and the same test passes on the main branch. Warnings or retries in the log that did not stop the run do not decide the answer.",        "options": [            "build_or_config",            "dependency_change",            "environment_or_infra",            "flaky_test",            "real_regression",            "test_needs_update",            "timeout_or_resource",        ],        "state": "{\"project\": \"tidepool, an open-source Python library for reading and writing tide-gauge data files.\", \"ci_run\": {\"job\": \"tests (python 3.12)\", \"trigger\": \"pull request 412\", \"commit\": \"9f3c2e1\", \"date\": \"2 November 2026\"}, \"change\": {\"title\": \"Speed up the CSV reader\", \"description\": \"Reads rows in blocks of 4,096 instead of one at a time. No behaviour change intended.\"}, \"ci_facts\": {\"attempts_on_this_commit\": \"2, both failed the same way\", \"same_test_on_main\": \"passed in each of the last 30 runs\", \"other_jobs_in_this_run\": \"lint passed; docs passed\", \"dependency_versions\": \"unchanged since the last passing run\", \"duration\": \"3 min 40 s (usual 3 min 30 s)\"}, \"log\": \"tests/test_csv.py::test_reads_last_row FAILED\\nE   AssertionError: expected 1,000 rows, got 904\\nE   assert 904 == 1000\\n========== 1 failed, 213 passed in 41.2s ==========\"}",    })     print(decision.value, decision.confidence)

The router picks ci-failure for this failed test run. Its confidence is high, so the caller goes straight to the specialist it named:

Context

project
tidepool, an open-source Python library for reading and writing tide-gauge data files.
ci_run
job: tests (python 3.12) trigger: pull request 412 commit: 9f3c2e1 date: 2 November 2026
change
title: Speed up the CSV reader description: Reads rows in blocks of 4,096 instead of one at a time. No behaviour change intended.
ci_facts
attempts_on_this_commit: 2, both failed the same way same_test_on_main: passed in each of the last 30 runs other_jobs_in_this_run: lint passed; docs passed dependency_versions: unchanged since the last passing run duration: 3 min 40 s (usual 3 min 30 s)

Input

tests/test_csv.py::test_reads_last_row FAILED E AssertionError: expected 1,000 rows, got 904 E assert 904 == 1000 ========== 1 failed, 213 passed in 41.2s ==========

Answer (JSON)
value real_regression, confidence 0.996
Top options
  1. real_regression 0.996
  2. dependency_change 0.001
  3. environment_or_infra 0.001

Open in playground

import os import jeff with jeff.Jeff.remote("https://api.jeffhub.ai", token=os.environ["JEFF_API_KEY"], timeout_s=30) as client:     decide = client.function("jeff.engineering.ci-failure.failure", context={        "project": "tidepool, an open-source Python library for reading and writing tide-gauge data files.",        "ci_run": {            "job": "tests (python 3.12)",            "trigger": "pull request 412",            "commit": "9f3c2e1",            "date": "2 November 2026",        },        "change": {            "title": "Speed up the CSV reader",            "description": "Reads rows in blocks of 4,096 instead of one at a time. No behaviour change intended.",        },        "ci_facts": {            "attempts_on_this_commit": "2, both failed the same way",            "same_test_on_main": "passed in each of the last 30 runs",            "other_jobs_in_this_run": "lint passed; docs passed",            "dependency_versions": "unchanged since the last passing run",            "duration": "3 min 40 s (usual 3 min 30 s)",        },    })     decision = decide("tests/test_csv.py::test_reads_last_row FAILED\nE   AssertionError: expected 1,000 rows, got 904\nE   assert 904 == 1000\n========== 1 failed, 213 passed in 41.2s ==========")     print(decision.value, decision.confidence)

ci-failure answers real_regression: the test fails on every attempt and passes on the main branch, so the change broke it.

Example: a retrieval question

Input

instructions: Can the question be answered from these passages alone? Use only what the passages state about the right subject and period: facts about a different subject (even one with a similar name) or a different period do not count, and a passage saying a detail is not known yet does not give it. The answer may need two or more passages together, for example one passage names the related site, supplier or person and another gives its figure. sufficient: together the passages state every asked detail for the right subject and period, and they do not disagree about it. insufficient: at least one asked detail, or a link needed to reach it, is not stated for the right subject and period. conflicting: two passages give different values for the same asked detail for the same subject and period, and nothing in the passages says which one is right. options: ["conflicting","insufficient","sufficient"] state: {"question": "How long is the warranty on an Aster 2 heat pump installed on or after 1 March 2026?", "passages": ["For Aster 2 units installed on or after 1 March 2026 the warranty is seven years, parts and labour.", "Aster 2 heat pumps installed from 1 March 2026 onwards are covered by a ten-year parts and labour warranty."]}

Labels (the options)

message-triage
Triages a message using the inbox's own message kinds, routing table and rules: what it is, what action it needs, how urgent it is and who should handle it.
support-intents
Names what a customer or assistant user is asking for, from a fixed list of requests such as cancel_order or track_refund.
triage
Routes a support ticket or email to a team and scores its urgency, sentiment and need for a person.
passage-relevant
For search and retrieval: checks whether a passage answers a question for the right subject and period: yes, partly or no.
evidence-sufficient
For search and retrieval: checks whether a set of passages is enough to answer a question: sufficient, insufficient or conflicting.
entails
Checks whether the evidence given (documents, messages or records) supports a claim: supported, unsupported or unclear.
contradicts
Checks whether two statements or records conflict: yes, no, or unclear (for example a date that can be read two ways).
guard
Checks a user message or outside content for prompt injection, jailbreak and data-leak attempts, and names the kind.
spam
Says whether a text message or email is spam or phishing, and whether it is legitimate, spam or a phishing scam.
tool-route
Given a tool menu written into the request (in any common format) and the conversation, picks the tool to call next, no tool, or asking the user for a missing detail.
ci-failure
Reads a failed automated build-and-test run (its log, the code change and the run history) and says what kind of failure it is: a real regression, a flaky test, infrastructure, a dependency change, a test that needs updating, and so on.
base
No listed specialist fits this request, so the general model with no specialist answers it: everyday reasoning, reading, knowledge and judging tasks, or any task that none of the listed specialists covers.
Answer (JSON)
value evidence-sufficient, confidence 1
Top options
  1. evidence-sufficient 1
  2. base 0
  3. passage-relevant 0

Open in playground

import os import jeff with jeff.Jeff.remote("https://api.jeffhub.ai", token=os.environ["JEFF_API_KEY"], timeout_s=30) as client:     decide = client.function("jeff.routing.router-open.specialist", labels={        "message-triage": "Triages a message using the inbox's own message kinds, routing table and rules: what it is, what action it needs, how urgent it is and who should handle it.",        "support-intents": "Names what a customer or assistant user is asking for, from a fixed list of requests such as cancel_order or track_refund.",        "triage": "Routes a support ticket or email to a team and scores its urgency, sentiment and need for a person.",        "passage-relevant": "For search and retrieval: checks whether a passage answers a question for the right subject and period: yes, partly or no.",        "evidence-sufficient": "For search and retrieval: checks whether a set of passages is enough to answer a question: sufficient, insufficient or conflicting.",        "entails": "Checks whether the evidence given (documents, messages or records) supports a claim: supported, unsupported or unclear.",        "contradicts": "Checks whether two statements or records conflict: yes, no, or unclear (for example a date that can be read two ways).",        "guard": "Checks a user message or outside content for prompt injection, jailbreak and data-leak attempts, and names the kind.",        "spam": "Says whether a text message or email is spam or phishing, and whether it is legitimate, spam or a phishing scam.",        "tool-route": "Given a tool menu written into the request (in any common format) and the conversation, picks the tool to call next, no tool, or asking the user for a missing detail.",        "ci-failure": "Reads a failed automated build-and-test run (its log, the code change and the run history) and says what kind of failure it is: a real regression, a flaky test, infrastructure, a dependency change, a test that needs updating, and so on.",        "base": "No listed specialist fits this request, so the general model with no specialist answers it: everyday reasoning, reading, knowledge and judging tasks, or any task that none of the listed specialists covers.",    })     decision = decide({        "instructions": "Can the question be answered from these passages alone? Use only what the passages state about the right subject and period: facts about a different subject (even one with a similar name) or a different period do not count, and a passage saying a detail is not known yet does not give it. The answer may need two or more passages together, for example one passage names the related site, supplier or person and another gives its figure. sufficient: together the passages state every asked detail for the right subject and period, and they do not disagree about it. insufficient: at least one asked detail, or a link needed to reach it, is not stated for the right subject and period. conflicting: two passages give different values for the same asked detail for the same subject and period, and nothing in the passages says which one is right.",        "options": [            "conflicting",            "insufficient",            "sufficient",        ],        "state": "{\"question\": \"How long is the warranty on an Aster 2 heat pump installed on or after 1 March 2026?\", \"passages\": [\"For Aster 2 units installed on or after 1 March 2026 the warranty is seven years, parts and labour.\", \"Aster 2 heat pumps installed from 1 March 2026 onwards are covered by a ten-year parts and labour warranty.\"]}",    })     print(decision.value, decision.confidence)

Here the router picks evidence-sufficient, again with high confidence:

Context

question
How long is the warranty on an Aster 2 heat pump installed on or after 1 March 2026?

Input

- For Aster 2 units installed on or after 1 March 2026 the warranty is seven years, parts and labour. - Aster 2 heat pumps installed from 1 March 2026 onwards are covered by a ten-year parts and labour warranty.

Answer (JSON)
value conflicting, confidence 0.951
Top options
  1. conflicting 0.951
  2. sufficient 0.044
  3. insufficient 0.005

Open in playground

import os import jeff with jeff.Jeff.remote("https://api.jeffhub.ai", token=os.environ["JEFF_API_KEY"], timeout_s=30) as client:     decide = client.function("jeff.retrieval.evidence-sufficient.sufficient", context={        "question": "How long is the warranty on an Aster 2 heat pump installed on or after 1 March 2026?",    })     decision = decide([        "For Aster 2 units installed on or after 1 March 2026 the warranty is seven years, parts and labour.",        "Aster 2 heat pumps installed from 1 March 2026 onwards are covered by a ten-year parts and labour warranty.",    ])     print(decision.value, decision.confidence)

evidence-sufficient answers conflicting: the two passages give different warranty terms for the same heat pump, and nothing says which one is right.

Low confidence

The router's own decision can abstain, exactly like any other function's: when its confidence falls below its calibrated threshold, abstained comes back true. Nothing escalates on its own. A confident base pick means you call the base model yourself instead of a specialist; a low-confidence pick is a separate question — decide what it means for your traffic with on_low_confidence (return it as it is, escalate it to another function, an LLM or a person, or raise an error), the same way as for any other function. See Escalation.

The menu

The menu you send the router holds only the specialists you want it to pick between, each as a label key with a description — the open specialists below, by family. The keys are the adapters' own names:

FamilySpecialists on the menu
Agentsaction-check, after-result, result-relevant, task-complete, tool-route, tools
Reasoningcontradicts, entails, equivalent, is-complete, satisfies
Supportmessage-triage, requires-action, support-intents, triage, trading-desk
Engineeringci-failure, issue-route
Retrievaldoc-type, evidence-sufficient, extract-check, ground, passage-relevant, query-route, section-id, legal-clauses
Safety and languageguard, spam, emotion, sanctions, soc

Add base too, for requests that no listed specialist fits. A menu can hold up to 32 entries, base included.

The general-knowledge and assistant-thinking adapters (knowledge-qa, assistant-routing) are not on the router's menu: call those directly.

Open. The router runs through the API or on your own machine with Jeff.local, like the specialists it routes to. Its weights are on Hugging Face, mstrasser/jeff-adapter-router-open-v14 at revision v1.4.

Pinning a version

The name jeff.routing.router-open follows the newest open router only while its interface stays the same: when a new base is released, the name moves to the new base's router only if that router takes the same request and the same kind of menu and gives the same kind of answer, so your code keeps working unchanged. A router that changes any of these gets a new name, and the old name stays with the old router. If you want to keep the router unchanged, call it by its versioned ID instead, which names the router of one base for as long as the API serves it:

NameVersioned ID (base v1.4)
jeff.routing.router-openjeff.routing.router-open-v14

Every answer identifies the function used: a decision's function names the versioned function that ran (jeff.routing.router-open-v14.specialist for a call to jeff.routing.router-open.specialist), so it changes when the name moves to a new base's router; compare decisions by that if you keep them. A version pin on the unversioned name (jeff.routing.router-open.specialist@1.4) works only until the name moves, and is then refused with UnknownFunction: pin the versioned ID instead. GET /v1/functions lists each function's other names under aliases.

Next steps

  • Escalation: what on_low_confidence can do with an abstained decision, from the router or from any function.
  • Open-source adapters by example: every specialist on the menu above, shown working.
  • Adapters: what an adapter is, and what each needs to run.