Instructions to use infosave/cmf-decision with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- cortiq
How to use infosave/cmf-decision with cortiq:
# one Rust binary, no additional dependencies cargo install cortiq-cli # or a prebuilt binary from github.com/infosave2007/cmf/releases hf download infosave/cmf-decision --include "*.cmf" --local-dir . ls *.cmf # some repos ship more than one quantization
cortiq run FILE.cmf --prompt "What is the capital of France?"
cortiq serve FILE.cmf --port 8080 # OpenAI-compatible server
- Laya
How to use infosave/cmf-decision with Laya:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Download ORACLE.md from infosave/cmf-decision: direct link, hf CLI and curl.
- Browser
- Download file 51.8 kB
-
https://huggingface.co/infosave/cmf-decision/resolve/main/ORACLE.md
- Command line
-
hf download hf://infosave/cmf-decision/ORACLE.md
-
curl -L -o ORACLE.md https://huggingface.co/infosave/cmf-decision/resolve/main/ORACLE.md
Cortiq Decision β oracle cascade and self-learning
The local model answers every question its certified gate accepts. A question
it cannot determine β the gate rejected the answer, or the question is not
trained at all β can be passed to a large language model, the oracle,
through OpenRouter. The oracle's answers are cached and can become training
examples, so the local model learns the traffic it abstains on; a refit
changes no other label's parameters (checked by sha256), although a refitted
label can win rows another label used to win. The oracle is off by
default; nothing leaves the machine until you enable it β in two steps:
the key in OPENROUTER_API_KEY, then cortiq serve FILE --oracle MODEL
(or cortiq decide FILE β¦ --oracle MODEL; cortiq decision oracle check
says first whether it is ready β Check your setup).
- How a question flows
- Self-learning
- Budget and stop rules
- What leaves the machine
- Connect OpenRouter β check your setup, one text or a batch
- Measured effect
How a question flows
- Local decision. Encoder, hashing and resonance run once per request. If
the gate accepts, the answer is
action: local. Such a question is never sent to the oracle, not even withcmf.oracle: true. - Permission. An undetermined question may go to the oracle only if all of
these hold:
oracle.enabled(orserve --oracle MODEL); the key variable is set in the server's environment; the caller's key hasoracle_allowed(in open mode, without keys:auth.require: falsein the configuration, orserve --oracleon a loopback address β see Who may use it); the request consents (cmf.oracle, the router'soptions.allow_oracle, elseoracle.default_per_request); budget is left; no stop rule fired. Otherwise a trained question staysabstainwith a flag (oracle_disabled,no_keybesideoracle_disabledwhen the key variable is not set,bad_keybeside it when the variable holds something that is not a key,consent_off,budget,stopped) and, on the decisions API, a one-linecmf.hintthat says what to do (the router API keeps its answer and its flags as they were β a missing key isoracle_disabledthere β and the server logs the hint); an untrained question fails with 422 or 503. - Semantic cache. Within the same scope β the question's contract
(type, instructions and criteria), and for a question matched to a skill
also the skill and the set of options β a stored oracle answer whose text
embedding has cosine β₯ 0.97 with the new one is reused:
action: cache, cost 0. Up to 50000 entries, ring buffer. The cache is shared by all accounts: an answer given under one caller's instructions is never served to a question with other instructions or criteria, and acacheanswer tells its caller that some account asked a near-identical text under the same contract. - Single flight. A question already in flight in another request (same scope, cosine β₯ 0.97) waits for that call instead of making its own.
- One call carries all remaining questions of the request:
POST {base_url}/chat/completionswith a strict JSON-schema answer (an enum of the option ids for choice, an integer level for score, a boolean for noul),temperature 0, reasoning off,max_tokens64 per question. The system prompt tells the model that the state is untrusted data, not instructions. - Result. A valid answer is
action: oraclewith its cost inusageand is stored in the cache. For a choice question matched to a skill it becomes a learning example of that skill β which every account is served β when the caller's key haslearning_allowed, or when the question is exactly the skill's own (its rubric's instructions and criteria over all its active labels, as/v1/routeasks it), so that the answer is the skill's rubric applied to the text and not a caller's instructions. The one exception is the open mode ofserve --oracleon a loopback address without keys: nobody is identified there, so its answers are cached but never learned. A failed call never becomes an error for a trained question: it is answered locally withaction: abstainand the flagoracle_unavailable(an untrained question gets 502).
Superset questions (a skill's labels plus new ones) are decided by the oracle
only; for a caller with learning_allowed the answer teaches that skill, and
a new label starts a cold start. A choice question no skill fits is learned
into an auto-skill of its own (Auto-skills), for such a
caller only.
Self-learning
- Examples are vectors, not texts. The buffer keeps the encoder vector and
the sparse hashed features of the text, its label and source (client
feedback weight 3.0, oracle 1.0; the weights are recorded, the fit is
unweighted). An example with cosine β₯ 0.995 to a row of the same label is a
duplicate and is dropped. The buffer and the cache are rebuilt from
learn.logat start. - Trigger. When a label collects 25 new examples (
learning.refit_min_new) a learning attempt runs (in one background worker, or inline withlearning.synchronous). - Champion / challenger. Only that label's subspace is refitted, on its training rows plus the learned ones (calibration rows never enter the fit). The challenger replaces the champion only if it is at least as good on the holdout β 20 % of the calibration rows β for the label's accuracy and the macro accuracy over labels (tolerance 1e-4).
- Re-certification. T, ΞΈ and Ο are recomputed on the skill's calibration
rows with the same procedure as at build time. If a certified gate would lose
its threshold, the challenger is rejected. The holdout that picks the
challenger is part of those calibration rows, so from the first promotion
the rows that certify Ο also chose the model: the bound is nominal after
any promotion, while answers keep
certified: true. - Isolation. Before a promotion the sha256 of the mean and basis of every other label is compared with the values before the attempt; any difference cancels the promotion. Other labels' parameters stay byte for byte the same. This is not a promise about accuracy: the refitted label still competes in the argmin and can take rows from the others (after the pre-training below, BANKING77 had 10 fewer correct rows).
- Cold start. A label the skill does not have (from an oracle answer to a
superset question, or from feedback, of a caller with
learning_allowed) becomes a quarantined task that is not scored. At 25 examples it is fitted and activated and the gate re-certified; answers it wins carrycertified: false. - Feedback.
POST /v1/feedback({"id", "question", "label"}with one of the question's options, or the router's{request_id, correct_task_label}with any label) corrects a decision of the caller's own account. From a key withlearning_allowedit becomes an example of weight 3; from any other key it is consumed and answered (accepted: false) but not learned. No key has the permission unless it was created or imported with it. - Limits. At most 5000 examples per label of a skill and 32 labels per skill waiting for a cold start; an example past either is refused.
- Generations and rollback. Every promotion writes
generations/gNNNNNN.cmf(only the changed task tensors and the learned rows, relative to the base file), fsyncs it and swaps the served model atomically;CURRENTnames it.POST /v1/admin/rollback {"generation": N}orcortiq decision rollbackserves any generation again (0 = the base file; the buffer is kept).cortiq decision materializewrites the served generation as one file. - Offline pre-training.
cortiq decision learnasks the oracle about the texts of a file of unlabelled traffic that the gate rejects, then refits every label that got new examples, however few (there is no threshold of 25 here), checks each challenger on the holdout and re-certifies the gate once at the end; if a certified gate would be lost, every promotion of the skill is undone (see API.md, section 7). An auto-skill (below) learns online only.
Auto-skills
A choice question whose option ids match no skill (match: untrained, not
an ambiguous one β several skills fit β and not one forced with cmf.skill)
goes to the oracle on every new text and, until 0.8.5, was never learned. Since
0.8.6 its contract becomes a skill of its own:
- Key. The contract is the question itself:
auto-+ the first 12 hex characters of the sha256 of the canonical JSON of{type, instructions, criteria}β the instructions as sent (nullwhen absent), the criteria object with its keys sorted and every description kept. The criteria in any order are one contract; other instructions, a changed description, a dropped or an added option are each another contract, learned separately. The id set alone is not the key: two clients using{yes, no}under different questions get two skills, and a multiple-choice benchmark whose positional ids{A, B, C, D}carry a new description per request is never learned as one skill (every request is its own contract, bounded byauto_max_skills). To be learned, a client keeps its question stable β the same instructions text, the same ids with the same descriptions. The first request's instructions and criteria are stored verbatim as the skill's rubric (the id is recomputed from it), visible through/v1/skills/{id}to every key. The contract is written tolearn.logbefore its first example, so a restart rebuilds it. - Who teaches. Only a caller whose key has
learning_allowed(the explicit open modeauth.require: falsehas it; the implicit open mode of a loopback address never teaches). The rule "a question that is exactly the skill's own teaches it" does not apply to auto-skills: their rubric is the contract every caller sends.learning.auto_skills: falseturns the learning off; the oracle answers as before. - Examples. Each answer is an example of the auto-skill under the
contract's label (the dedup of 0.995 applies). The labels are closed: a
feedback with a label outside the contract is refused (a request with one
more id is another contract, learned on its own).
At most
learning.auto_max_examples_per_label(1000) examples per label. - Activation. When a label collects 25 new examples (the usual trigger)
the whole skill is attempted: one row in five β by a hash of the row itself,
so a row never moves between the fit and the calibration subset as more
arrive β is held out; a label with β₯
auto_min_rows(10) fit rows is eligible and fitted (rankauto_k, 8), the others stay quarantined; the attempt needs two eligible labels including the triggering one and, the first time, the eligible labels must hold β₯auto_min_coverage(0.8) of the contract's rows. T and ΞΈ are certified on the held-out rows as at build time (Ο certifies only with 100 accepted odd rows, so the skill reportscertified: falsefor a long time, and its winners arecold_startones β never certified). The first activation (auto_start) requires a macro agreement of the fitted skill with the oracle's labels on the held-out rows β₯auto_min_agreement(0.8) and β₯ 0.5 per label; a later attempt (auto_refit, the whole skill refitted) must not regress the champion on the same rows (the holdout rule). A promotion writes a generation that carries the auto-skill entirely; every other skill is byte for byte the same. - Serving. The contract is then decided locally,
match: exact,skill: auto-β¦,certified: false. An auto-skill is matched by its contract alone: a part of its ids, a superset, other instructions or another description is another contract (untrained, learned on its own), never asubsetorsupersetof the auto-skill. A quarantined label still counts as known within the contract: it gets probability 0, and a text of it is expected to abstain on novelty and keep teaching it. Because ΞΈ and T of a young auto-skill come from a handful of rows (ΞΈ is close to the largest novelty of the even half, T may sit at a bound), a local answer also needsp_top β₯ learning.auto_tau(0.9) underbalancedandquality-first(cost-saverkeeps its ΞΈ-only rule), and the T an attempt records is floored atlearning.auto_temperature_min(0.02): a clean held-out subset fits T at its lower bound, where everyp_topis 1 and theauto_taufloor never bites. An abstention escalates and teaches as any other. The agreement gate is the real guard while the subset is small; the gate improves with every refit. Rejected examples are never forgotten: a contract polluted by a noisy oracle needs more consistent examples, not a reset (or a rollback to a generation before it). - Exploration. The hard rule β a question the gate accepted never
reaches the oracle β has exactly one exception, for auto-skills only and
only while one of their labels is quarantined: the gate names such a
label's texts as a neighbour with full confidence (measured on a stand: all
21 texts of a quarantined label answered locally as another label at
p_top1), so without it the label could never learn. One text inlearning.auto_explore_every(8;u64le(sha256(Ο_P))modulo it is 0, a property of the text, never of the order; 0 turns it off) is escalated although accepted: the oracle's answer is served (action: oracle/cache, the flagexplore, thegateblock still reportsaccepted: true) and learned as usual, so the rare label collects examples at a quarter of its traffic until the next attempt activates it, after which the contract explores no more. A refused or failed call leaves the local answer. Exploration draws only when the answer could teach: the oracle consented, learning andauto_skillson, a key withlearning_allowed; data skills,cortiq decide, shadow mode and/v1/routenever explore. - Limits. A contract is learned only with 2..
auto_max_labels(64) option ids and while fewer thanauto_max_skills(256) contracts are registered; otherwise the oracle answers and nothing is recorded (auto_skippedinGET /v1/admin/learning, one warning an hour, never the ids). Every generation re-carries every auto-skill's rows: with many contracts runcortiq decision materializefrom time to time and serve the materialised file (it holds the auto-skills as ordinary skills;decide --labelsandverifyread it; such a file needs cortiq β₯ 0.8.6). A served auto-skill keeps learning wherever its contract comes from: the contract record oflearn.log, or β a materialised file on a fresh state directory, alearn.loglost while the generations were kept β the skill's own labels and rubric, registered (and written tolearn.log) at start and after a rollback. - Routing.
/v1/routewithouttaxonomy_idstill means the file's only data skill; an auto-skill is routed by its id (its question is then the whole contract from the rubric).cortiq decide --labelsnever names an auto-skill by labels alone:--skill auto-β¦names it and its rubric supplies the contract. When a data skill and auto-skills fit a question equally, the data skill answers; two auto-skills of one contract cannot exist (the contract is the id). - Admin.
GET /v1/admin/learninglistsauto_skills(id, labels,examplesper label β the rows the next attempt fits: the served learned rows of the label plus the buffer examples not among them, each row once, so their sum is the attempt'srows.totalβ what is served),auto_contractsandauto_skipped; attempts carrykind: auto_start | auto_refitand anautoblock with the eligible and quarantined labels, the rows and the agreement./healthzaddsauto_skills. Rollback to an earlier generation drops the skill from the served model (the contract is untrained again; its examples stay and start it over at the next trigger). - Compatibility. A 0.8.5 binary opening a 0.8.6 state directory truncates
learn.logat the first contract record and refuses a generation that carries an auto-skill (the same rule as theLOCKof 0.7.9: never run an older binary on a newer state directory). The cache scope of a contract changes from the contract to the skill at its activation, so its first requests after that miss the cache (they are answered locally anyway).
Budget and stop rules
- Reservation first. Before each call the server reserves
((body bytes + 4096) Β· max_price.prompt + max_tokens Β· max_price.completion) / 1e6USD. The call is made only if spent + reservations in flight + this reservation β€budget_usd, calls <max_calls, for the caller's key spent + reservation β€ itsoracle_budget_usd, and, for a key withcredit_usdwhen the oracle is billed (oracle_passthrough), reservation Γoracle_markupβ€ the credit left. - Ledger.
oracle.jsonlgets areservedline, fsynced, before the request leaves; after the answer asettled,failed_billedorfailed_unknown_costline with the cost, tokens and latency. At start any reservation without a closing line counts as spent. - Checks of every answer. HTTP 200 within
deadline_s(no retries), a finiteusage.costβ₯ 0,finish_reason: stop, exactly the asked question ids with the schema's types. A 200 that carries only anerroris a failure. - Stop rules switch the oracle off (reason in
oracle.state) untilPOST /v1/admin/oracle {"enabled": true}(orcortiq decide β¦ --oracle --oracle-resumeon a state directory no server holds): HTTP 401, 402 or 403 from OpenRouter; a returned model that is not the configured one; a cost above the reservation;max_errorsfailures in a row (counted across restarts andcortiq decideruns). GET /v1/admin/oracleshows spent, reserved, remaining, calls, failures and the stop reason;POST /v1/admin/oraclecan switch the oracle and lowerbudget_usd/max_callswithin the configured values.
What leaves the machine
- Sent, only for undetermined questions with the oracle permitted: the
stateand theinstructionsandcriteriaof those questions. Receivers: OpenRouter and the provider it routes to (provider.sort: price, fallbacks allowed; setoracle.data_collection: "deny"to exclude providers that store data). - PII redaction is on by default (
oracle.redact_pii): e-mail addresses, secret-like tokens (20 or more characters of[A-Za-z0-9_-]with a digit and a letter) and numbers of 9 or more digits β also when their digit groups are separated by spaces, dashes, dots, slashes or parentheses, as in4111 1111 1111 1111,+1 (555) 123-4567or a spaced IBAN β in every string of the state are replaced by[REDACTED]and the question gets the flagpii_redacted. It is a heuristic: names, postal addresses, numbers written in words and identifiers with letters between short digit groups are not detected. A request can opt out withcmf.allow_pii_egress(router:options.allow_pii_egress). - Never sent: accepted questions, other questions of the request, client keys, accounts, vectors.
- Kept on disk in the state directory: vectors and hashed features of learned examples and cached answers, the oracle ledger (no texts), usage records (no texts). Hashed n-gram features can show whether a known text was seen, so treat the state directory as sensitive (it is created with mode 0700).
- The OpenRouter key is read from the environment variable named by
oracle.api_key_env; it is never written to the configuration, the model, the state or the logs.
Connect OpenRouter
Create an account at openrouter.ai, add credit, and under Settings β Keys create a key with a credit limit of its own. Then:
export OPENROUTER_API_KEY="<your OpenRouter key>" # step 1
cortiq serve cortiq-decision.cmf --oracle deepseek/deepseek-v4.1-flash # step 2
That is the whole setup. The model is any
openrouter.ai/models id with structured
outputs (JSON schema); deepseek/deepseek-v4.1-flash is the one every
number on this page was measured with. At start the server:
- makes one public request without the key,
GET https://openrouter.ai/api/v1/models/<MODEL>/endpoints, and sets the max price (which caps the provider price and sizes every reservation) to twice the prompt and completion prices of the model's cheapest endpoint with structured outputs; - refuses to start with a message that names the problem β and 2β3
cheap models that fit β when OpenRouter does not list the model, when
none of its endpoints supports structured outputs, or when it has only
variable prices (as
openrouter/auto: give--oracle-max-priceor pick a concrete model); it also refuses a given max price below every structured-output endpoint, since OpenRouter would refuse every call; - falls back to a max price of $0.10/$0.50 per 1M in/out, with a warning, when the listing cannot be fetched;
- logs one line about the oracle, never the key:
oracle: ready β deepseek/deepseek-v4.1-flash via openrouter.ai, budget $1.00, max price in/out $0.06/$0.58 per 1M (2Γ the cheapest structured-output endpoint, β¦ at $0.03/$0.29)
oracle: NOT ready β OPENROUTER_API_KEY is not set (set it to your OpenRouter key and restart; β¦)
Everything else keeps its safe default: a budget of $1.00, provider
routing {sort: price, require_parameters: true, allow_fallbacks: true},
PII redaction on, the stop rules, and the oracle only for questions the
local model cannot decide. Optional companions of --oracle (each
overrides --decision-config):
| Flag | Default | Meaning |
|---|---|---|
--oracle-budget USD |
1.0 | the most this server spends on the oracle |
--oracle-max-calls N |
10000 | the most oracle calls |
--oracle-max-price IN,OUT |
2Γ the cheapest structured-output endpoint | max price, USD per 1M prompt / completion tokens |
--oracle-key-env VAR |
OPENROUTER_API_KEY |
the NAME of the variable that holds the key: 1β128 bytes of [A-Za-z0-9_] starting with a letter or _. A value that looks like a key (see below) is refused and never shown, only its length |
--oracle-base-url URL |
https://openrouter.ai/api/v1 |
https; plain http only to a loopback address (a local proxy or a test mock); no credentials, query or key in it (a URL holding sk-or- is refused unshown) |
--no-oracle-learning |
off | answers are cached but the served model never changes |
A value the numeric flags (--oracle-budget, --oracle-max-calls,
--oracle-max-price, and --max-price of oracle check) refuse is never
shown either β only the flag, the value's length and why β and neither is a
string a --decision-config gives where a number belongs.
Key-like values. A value looks like a
key when, checked in this order, it holds sk-or- anywhere or starts with
sk- (ASCII letters in any case), starts with Bearer (any case, a space
after the word), has leading or trailing ASCII whitespace, is 41 bytes or
longer without a /, or holds 32 or more hexadecimal digits in a row. The
test applies as it is to --oracle MODEL and oracle check --model
(which must also be 1β256 bytes without whitespace or control characters,
and author/slug: a /, no empty, . or .. segment); to the value of a
numeric flag, its surrounding whitespace trimmed; and to every command-line
argument, or the value of a --flag=VALUE, that a usage error would quote
(shown only by its length). For a variable's name (--oracle-key-env,
oracle check --key-env) a value of only [A-Z0-9_] starting with an
upper-case letter or _ (such as
MY_COMPANY_PRODUCTION_OPENROUTER_API_KEY_V2) is a name at any length,
unless it holds 32 hexadecimal digits in a row; any other value is judged
by the test above and, besides, taken for a random token when it is 16
bytes or longer with letters and digits and no _. A base URL holding
sk-or- in any case is refused unshown.
Check it β /healthz needs no token, the admin view needs
CORTIQ_DECISION_ADMIN_TOKEN in the server's environment:
curl -s http://127.0.0.1:8080/healthz | jq -r .oracle_status # β ready
curl -s http://127.0.0.1:8080/v1/admin/oracle -H "x-admin-token: $CORTIQ_DECISION_ADMIN_TOKEN" \
| jq '{status, model, max_price, budget_usd, spent_usd, calls}'
status is one of ready, no_key (the variable is unset or empty),
bad_key (the variable holds something that is not a key β see below;
key_problem says what, by position and length), disabled (not
configured, or switched off by POST /v1/admin/oracle),
budget_exhausted (something was spent and the budget or max_calls is
used up: what is left cannot hold the smallest possible call, or a call it
refused; restart with a larger --oracle-budget / --oracle-max-calls),
budget_too_small (nothing spent, and the budget
cannot hold even one call, or max_calls is 0: min_call_usd is the least
budget a call needs β the smallest possible call's reservation, one short
question, until the budget refuses a real, longer call; from then on the
larger of the two, that call's reservation, whether the budget was below
even the smallest call (0 included) or only below that one, so the status
is budget_too_small then, not ready; the hint of the refused request
names it rounded up to the micro-dollar, so passing that figure admits the
call; the startup line, before any request, can only give the smallest
call's, "more for longer questions"; with max_calls 0 the hint names the
call limit, and the budget too only when it is short as well) or stopped: <reason> (a stop rule; last_error names
the code of the last failed call, which max_errors counts; POST /v1/admin/oracle {"enabled": true} resumes after the fix). An admin
limit (budget_usd or max_calls set by POST /v1/admin/oracle) is
kept in oracle.state and outlives a restart, so a larger
--oracle-budget or --oracle-max-calls alone cannot lift it: when it is
among what refuses the next call, the startup line (with the file's path)
and the hint (the server's oracle.state) name it and the admin request
that lifts it, e.g. the admin limit budget_usd $0.00 in STATE/oracle.state binds (β¦; the next call needs a budget of $0.00178924): raise it with POST /v1/admin/oracle {"budget_usd": 0.00179} (at most the configured $5.00) or lift it with {"budget_usd": null} β a figure rounded
up to the micro-dollar that the admin API takes (never above the
configured budget; when rounding would put it above, only null is
offered); when the configured limit refuses the call as well, the hint
says to lift the admin limit and restart with the flag of at least the
figure. On the router
API, /v1/healthz carries the status as cmf.oracle_status only with
x-cmf-extensions: 1, so the router's own shape stays exact.
The key is read as it is in the variable, less surrounding spaces, tabs, CR
and LF (a .env file's line ending): the server, decide and oracle check warn the key had surrounding whitespace, trimmed and go on. A value
that still holds whitespace, a control byte or a byte outside ASCII, that
starts with Bearer (the request adds it), that is a whole .env line
(NAME=β¦) or starts or ends with a quote is bad_key: it is never sent, and the message names only the position
and the length of the problem, both counted among the variable's raw bytes
(the whitespace around the key included, as an editor shows the .env
line). No message, log line (at any RUST_LOG level), status, ledger or
state file ever holds a byte of the key: a failed request is a fixed code
such as transport_connect, transport_timeout or
transport_bad_header; an answer that did not finish is finish_length,
finish_content_filter, finish_tool_calls, finish_error or
finish_other, never the upstream's finish_reason text; every code
shown comes from that closed set (with http_ and a three-digit status),
and a stop_reason or last_error read back from oracle.state outside
it (an older version's file, or one edited by hand) is shown as
unknown_code β still a stop. A name an upstream answers (a response's
provider and model, a listing's provider names and model ids) is kept
only as it is β at most 64 bytes of [A-Za-z0-9 ._:/()-] β and written or
printed as [redacted] otherwise, never filtered or cut; also when it
holds Bearer (any case), looks like a key, or holds 8 bytes in a row of
the configured key, and when its letters and digits alone (an echo
interleaved with dots, dashes or spaces) hold bearer, skorv or 8 in a
row of the key's letters and digits. The public listings are fetched
without the key, but their names are checked against it too (the value of
its variable); a suggested model id must also be one --oracle takes. A
key pasted where the command line takes no value (cortiq decision oracle check sk-or-β¦, --test-call=sk-or-β¦) is shown in the usage error only by
its length.
Out of scope: an upstream at the configured base URL (OpenRouter, or your
own proxy) that has already received the key and deliberately echoes it,
or pieces of it, in its own answers holds the key already β Cortiq still
copies none of its free text into another client's answer, and every code
it shows comes from a closed set. So is a key typed into an argument that
is not about the key or the oracle (a file path, --host, a skill id).
Check your setup
cortiq decision oracle check tells whether the oracle is ready before
anything is started or spent. It reads the same flags as --oracle
(--model, default deepseek/deepseek-v4.1-flash; --key-env;
--base-url; --max-price IN,OUT, which a model listed only with variable
pricing such as openrouter/auto needs, as --oracle-max-price does):
cortiq decision oracle check # free: the key, the account, the model
cortiq decision oracle check --test-call # and one tiny structured call (a 2-option choice, max_tokens 16)
Oracle check: deepseek/deepseek-v4.1-flash via openrouter.ai
β key OPENROUTER_API_KEY is set
β account the key is valid (credit limit $10.00, $1.25 used, $8.75 left)
β model 3 endpoints, 2 with structured outputs; the cheapest is MockProvider at $0.03/$0.29 per 1M in/out, so --oracle sets the max price to $0.06/$0.58
β test call answered 'yes' for $0.00001 (reserved $0.000317, provider MockProvider, 0 ms)
ready: cortiq serve FILE --oracle deepseek/deepseek-v4.1-flash (or: cortiq decide FILE -p TEXT --oracle deepseek/deepseek-v4.1-flash)
Each line is one check: key β the variable holds a usable key (only its
presence is shown, never the key; surrounding whitespace trimmed is said,
anything else wrong is bad_key); account β GET /auth/key accepts the key
(free) and shows its credit limit and usage; model β OpenRouter's
public endpoint listing (free, no key) has the model with structured
outputs, and its cheapest price; test call β with --test-call only,
one call through the same reservation ledger and stop rules as a server,
with its answer and cost. β passed, β failed (the line says why and
what to do, and for a model it names 2β3 cheap ones that fit), β not
checked. The exit code is 0 only when every check passed (and the test
call answered, when asked); --json gives the same as one object with
ready and problems[] (no_key, bad_key, key_refused, no_credit,
account_unreachable, unknown_model, no_structured_outputs,
variable_price, max_price_too_low, listing_unreachable,
test_call_failed).
The commands of this section and the next were run as written against a
local mock of the OpenRouter API with a test key: --base-url (for serve
and decide, --oracle-base-url) pointed at the mock, so the outputs here
show openrouter.ai, and no --oracle-base-url, where the run showed the
mock's address. Endpoints, provider, prices, credit, costs and the oracle's
answers are the mock's; nothing was sent to OpenRouter.
Statuses. A server reports its oracle in the startup line (oracle: ready β β¦, oracle: NOT ready β <what to do>, oracle: off β β¦), in GET /healthz (oracle_status) and in GET /v1/admin/oracle (status);
cortiq decide β¦ --oracle MODEL in the start line of a batch, in its
hint and in cmf.oracle.status of --json. Each message says what to do;
in short:
curl -s http://127.0.0.1:8080/healthz | jq -r .oracle_status # the server of step 2 β ready
| Status | Means | What to do |
|---|---|---|
ready |
questions the gate rejects go to the oracle | nothing |
no_key |
the key variable (OPENROUTER_API_KEY, or --oracle-key-env) is unset or empty |
export OPENROUTER_API_KEY=β¦ where the server (then restart it) or decide runs |
bad_key |
the variable holds something that is not a key; nothing was sent | fix it as the message says β it names the problem (a quote, Bearer , a whole NAME=β¦ line, whitespace, a control or non-ASCII byte) by position and length, never the key β then restart the server or run decide again |
disabled |
the server has no oracle (no --oracle MODEL, or oracle.enabled false), or the admin switched it off |
start the server with --oracle MODEL; after the admin's switch, POST /v1/admin/oracle {"enabled": true} |
budget_too_small |
nothing spent, and the budget cannot hold one call (min_call_usd), or max_calls is 0 |
--oracle-budget of at least the figure the message names (and --oracle-max-calls of 1 or more); for an admin limit in oracle.state, the POST /v1/admin/oracle the message names |
budget_exhausted |
the budget or max_calls is used up |
a server: restart with a larger --oracle-budget / --oracle-max-calls (the budget counts the state directory's whole ledger, across restarts); decide: each run has its own budget, pass a larger --oracle-budget; for an admin limit, the POST the message names |
stopped: <reason> |
a stop rule: http_401 or http_403 (the key was refused), http_402 (no credit), unexpected_model, cost_above_reservation, max_errors (failures in a row; last_error names the last one) |
fix the cause (cortiq decision oracle check tests the key, the credit and the model), then POST /v1/admin/oracle {"enabled": true} on a server, or one decide run with --oracle-resume |
One text or a batch
cortiq decide takes the same --oracle MODEL (and the --oracle-*
flags of the table above) for one text or a batch. The text is decided
locally first; only when the gate rejects it β or no skill has the asked
labels β is one call made, with the same reservation, stop rules, PII
redaction and key handling as a server. A text the gate accepts sends
nothing.
cortiq decide cortiq-decision.cmf --skill banking77 -p "the exchange rate you gave me looks wrong" \
--oracle deepseek/deepseek-v4.1-flash
choice: card_payment_wrong_exchange_rate (from oracle deepseek/deepseek-v4.1-flash, $0.00014)
action: oracle (the gate rejected the local choice card_payment_wrong_exchange_rate), certified false
skill: banking77 (exact match, 77 candidates)
gate: p_top 0.5678609 (tau 0.7), novelty 0.84277564 (theta 0.804234), margin 0.0045858026, confidence 0.56217486
errors: card_payment_wrong_exchange_rate 0.19301137, wrong_exchange_rate_for_cash_withdrawal 0.19957425, exchange_rate 0.35151693, wrong_amount_of_cash_received 0.43852428, extra_charge_on_statement 0.48889443
oracle: deepseek/deepseek-v4.1-flash via openrouter.ai: $0.00014 spent in this run (1 call), budget $1.00; ledger cortiq-decision.cmf.state/oracle.jsonl: $0.00014 over 1 call in all
model: cortiq/decision@386b6e43fd35 (generation 0), 3489 input tokens, 15828 Β΅s
A batch reads one {"text", "label"?} object per line and writes one JSON
row per input, never the text:
printf '%s\n' '{"text": "I still have not received my new card", "label": "card_arrival"}' \
'{"text": "can I pay my rent with a virtual card"}' > rows.jsonl
cortiq decide cortiq-decision.cmf --skill banking77 --input rows.jsonl --out results.jsonl \
--oracle deepseek/deepseek-v4.1-flash --oracle-budget 0.05
jq -c '{answer, action, source, oracle_cost_usd, flags}' results.jsonl
{"answer":"card_arrival","action":"local","source":"local","oracle_cost_usd":0.0,"flags":[]}
{"answer":"get_disposable_virtual_card","action":"oracle","source":"oracle","oracle_cost_usd":0.0001397,"flags":[]}
- Columns. A batch row keeps the columns of a run without
--oracle(choice,p_top,accepted, β¦) and addsanswerβ the final answer: the oracle's or its cache's foractionoracle/cache, else the local choice (for anabstainrow that is the local model's choice the gate rejected, so readactionbefore trustinganswer) βaction(local,oracle,cacheorabstain),source,oracle_cost_usd(this row's call; 0 for a local or cached answer),flags(pii_redacted, or why a row abstained:budget,no_key,bad_key,stopped,oracle_unavailable, β¦) and, for a labelled row,answer_correct(answerequals the label). The oracle's totals and ahintare in the summary on stderr. For one text,--jsoncarriesactionandsourceincmf.questions.task, the call's cost incmf.usage.oracleand the run's status, spend and budget incmf.oracle. - Without a usable key a rejected text abstains with
no_key(orbad_key) and the hintOPENROUTER_API_KEY is not set (decide --oracle reads the key from the environment): export β¦; nothing is sent and no state directory is made. Labels no skill has can only be answered by the oracle, so there the same words end the run with an error. --oracle-budget USD(default $1.00) and--oracle-max-calls Ncap each run; the rows past the cap abstain with the flagbudget. A budget that cannot hold the run's first call β 0 included β isbudget_too_small(the hint names that call's reservation and the least--oracle-budgetthat holds it, rounded up to the micro-dollar: rerun with that figure and the call is made; a batch's start line, before any row, gives the smallest call's, "more for longer questions"); one the run spent isbudget_exhausted. Without--oracle,decidenever calls the oracle.- State. The reservation ledger, the answer cache and
oracle.statelive in the state directory,<FILE>.statenext to the file (or--state DIR), under itsLOCK: one process per directory β a runningcortiq serveon it is asked through its API instead. A repeated text comes from the cache for free. Admin limits a server set there (budget_usd,max_callsinoracle.state) count the whole ledger and hold fordecidetoo, whatever--oracle-budgetor--oracle-max-callssay; its messages name the one that binds, with the file's path and the figure the ledger needs, and how a server of that directory lifts it (POST /v1/admin/oracle {"budget_usd": null}, or that figure rounded up when it is within the server's configured budget).
After a stop rule (a refused key, no credit, another model, a cost
above the reservation, max_errors failures in a row) the oracle of that
directory stays off for every later run: a rejected text abstains with the
flag stopped and a hint that names the rule. After the fix, add
--oracle-resume once, as POST /v1/admin/oracle {"enabled": true} does
(here after an http_402, credit added):
cortiq decide cortiq-decision.cmf --skill banking77 -p "hello there" --oracle deepseek/deepseek-v4.1-flash --oracle-resume
oracle: resumed β the oracle of state directory cortiq-decision.cmf.state was stopped by the stop rule http_402: OpenRouter answered HTTP 402: the key in OPENROUTER_API_KEY has no credits left (add credits at https://openrouter.ai/settings/credits, or raise the key's own limit); it may be called again
β¦
After an interrupted run (Ctrl-C, SIGTERM, a closed terminal) the
LOCK is released; a signal the run inherited as ignored (nohup, a
background job of a script) stays ignored. A LOCK left by a crash is
named with its pid (state directory cortiq-decision.cmf.state has a LOCK left by pid 42341, which is no longer running (an interrupted run): pass --break-lock to remove it, or give this run a directory of its own with --state DIR); --break-lock removes it only when that process is gone:
cortiq decide cortiq-decision.cmf --skill banking77 -p "thanks for your help" --oracle deepseek/deepseek-v4.1-flash --break-lock
warning: removed the LOCK of state directory cortiq-decision.cmf.state left by pid 42341, which is not running (--break-lock)
β¦
Who may use it, and who teaches
- No keys, loopback address (the default
127.0.0.1:8080): with--oracle, callers without a key may use the oracle within its budget β the operator enabled it on the command line β but never teach the model: their feedback is not learned and the oracle's answers to them are cached, not learned. A reverse proxy on the same host forwards anyone to a loopback address, so behind one setauth.require: trueand use keys. - No keys,
auth.require: falsein--decision-config: the explicit open mode, which may use the oracle and teach. - No keys, another address: keys are required (
auth.require: nullmeans "required unless loopback"). - Keys:
cortiq decision keys createmakes keys that may use the oracle (oracle_allowed: true, like imported router keys);--oracle-allowed=falsemakes one that never escalates, and--oracle-budget-usdcaps one key's oracle spending. Teaching the shared skills is a separate permission,--learning-allowed(off by default); it alone lets a caller's untrained contracts become auto-skills. Keys made throughPOST /v1/admin/keyskeep the router's default (oracle_allowedonly when the body says so).
With a configuration file
For every oracle setting β deadline, error limit, provider options,
data_collection: "deny", per-request consent β use --decision-config
(the --oracle* flags, when given, override its values):
- Configure the oracle:
enabled: trueand amodelfrom openrouter.ai/models that supports structured outputs (JSON schema).provider.max_price(USD per 1M tokens) caps the provider price and sizes the reservation.base_urlmust be https (plain http only to a loopback address). - Set limits:
budget_usd,max_calls,deadline_s,max_errors; per keyoracle_allowed,oracle_budget_usdand, for keys whose own questions and feedback may teach the model,learning_allowed. - Put the key in the environment of the server process, start the server and verify.
export OPENROUTER_API_KEY="<your OpenRouter key>"
cat > oracle-server.json <<'EOF'
{
"oracle": {
"enabled": true,
"base_url": "https://openrouter.ai/api/v1",
"api_key_env": "OPENROUTER_API_KEY",
"model": "deepseek/deepseek-v4.1-flash",
"provider": {"sort": "price", "require_parameters": true, "allow_fallbacks": true,
"max_price": {"prompt": 0.1, "completion": 0.5}},
"budget_usd": 1.0,
"max_calls": 10000,
"deadline_s": 30,
"max_errors": 30,
"redact_pii": true
},
"cache": {"enabled": true, "threshold": 0.97},
"learning": {"enabled": true, "refit_min_new": 25}
}
EOF
export OKEY=$(cortiq decision keys create --state ./oracle.state --plan developer \
--account acme --oracle-allowed --oracle-budget-usd 0.50 --json | jq -r .key)
export CORTIQ_DECISION_ADMIN_TOKEN=$(openssl rand -hex 24)
cortiq serve cortiq-decision.cmf --decision-config oracle-server.json \
--state ./oracle.state --port 8081
Verify: the admin view must show status: "ready" (configured: true: the
configuration enables the oracle; key_present: true: the key variable is
set in the server's environment). enabled: true only means that no stop
rule or admin call has switched the oracle off; it is true with the oracle
off in the configuration as well. Then a question the gate rejects comes back from the
oracle (on the published model, "can you recommend a good tattoo artist" is
out of scope for clinc150 and is rejected by its gate).
export ORC=http://127.0.0.1:8081
curl -s "$ORC/v1/admin/oracle" -H "x-admin-token: $CORTIQ_DECISION_ADMIN_TOKEN" \
| jq '{status, configured, key_present, enabled, model, budget_usd, spent_usd, stop_reason}' # β 200
curl -s "$ORC/v1/route" -H "Authorization: Bearer $OKEY" \
-H 'Content-Type: application/json' \
-d '{"input": {"text": "can you recommend a good tattoo artist"}, "taxonomy_id": "clinc150"}' \
| jq '{source: .decision.source, label: .decision.task_label, oracle}' # β 200
curl -s "$ORC/v1/admin/oracle" -H "x-admin-token: $CORTIQ_DECISION_ADMIN_TOKEN" \
| jq '{calls, spent_usd}' # β 200
The statuses above were recorded with base_url pointed at a local mock of
the OpenRouter API; the documentation run sent nothing to OpenRouter.
source is oracle (a repeat of the same text is cache); calls and
spent_usd grow. If source stays router with the flag oracle_disabled,
consent_off or budget, check status, the key's oracle_allowed
(without keys: auth.require: false, or --oracle on a loopback address)
and the budget; stop_reason names a stop rule, and the decisions API's
cmf.hint says what to do. The same questions through
the decisions API need "cmf": {"oracle": true} or
oracle.default_per_request: true (the default).
For reference, the oracle calls behind every number on this page β test and dev sets of the three benchmarks, 3616 calls of which 3 failed, in 15 ledger files β cost $0.0886 in total.
Measured effect
All rows are the test sets of BANKING77 (n = 3080), CLINC150 (4500) and MASSIVE
en-US (2974); the oracle is deepseek/deepseek-v4.1-flash through OpenRouter
with Jev's rubric, temperature 0, reasoning off. Jev 1.13 is
typesafe/jev-1.13, its stored answers on the same rows.
1. Static cascade against the model alone
The published model answers what its certified gate accepts; every abstention goes to the oracle (all of them were answered).
| BANKING77 | CLINC150 | MASSIVE | |
|---|---|---|---|
| Model alone, all rows (top-1) | 2875 (93.34 %) | 4328 (96.18 %) | 2562 (86.15 %) |
| Answered locally by the gate / correct | 2791 / 2714 | 4145 / 4091 | 1615 / 1581 |
| Abstentions sent to the oracle | 289 (9.38 %) | 355 (7.89 %) | 1359 (45.70 %) |
| Oracle correct on them | 180 | 295 | 1036 |
| Jev correct on the same rows | 180 | 299 | 1021 |
| Cascade, correct | 2894 (93.96 %) | 4386 (97.47 %) | 2617 (88.00 %) |
| Oracle spend on the test set | $0.009508 | $0.016002 | $0.025337 |
| $ per 1M decisions, cascade | $3.09 | $3.56 | $8.52 |
| $ per 1M decisions, Jev 1.13 | $183.69 | $271.61 | $110.86 |
The cascade adds 19, 58 and 55 correct answers over the model's own top-1 (2894 β 2875, 4386 β 4328, 2617 β 2562). The abstained rows are hard for any system: there the oracle and Jev are about equally right (180 and 180, 295 and 299, 1036 and 1021).
2. Self-learning in one pass (mode A) against the static cascade (mode B)
cortiq serve with a fresh state per run, the test rows in a fixed hashed
order, one request at a time, no ground truth and no feedback sent. Mode A
has the cache and self-learning on, with learning inline
(learning.synchronous) and PII redaction off so that the request bodies
match the stored ledgers, in open mode (auth.require: false), with
deadline_s 200, budget_usd 10 and max_calls 1000000; mode B has the
cache and learning off. Oracle answers were replayed from the stored
DeepSeek ledgers through a local proxy; the 24 requests of MASSIVE mode A
that no ledger held were sent live.
| BANKING77 A / B | CLINC150 A / B | MASSIVE A / B | |
|---|---|---|---|
| Oracle calls | 282 / 289 | 351 / 355 | 1335 / 1359 |
| Oracle calls by third of the stream | 104, 80, 98 / 104, 81, 104 | 113, 100, 138 / 114, 101, 140 | 449, 415, 471 / 447, 449, 463 |
| Cache hits (correct) | 7 (4) / 0 | 4 (4) / 0 | 10 (7) / 0 |
| Learning attempts | 0 / 0 | 0 / 0 | 28 (16 promoted, 12 rejected) / 0 |
| Answered locally (correct) | 2791 (2714) / 2791 (2714) | 4145 (4091) / 4145 (4091) | 1629 (1595) / 1615 (1581) |
| Cascade correct | 2893 / 2894 | 4386 / 4386 | 2617 / 2617 |
| $ per 1M decisions | $3.01 / $3.09 | $3.53 / $3.56 | $8.48 / $8.52 |
| Isolation violations | 0 | 0 | 0 |
In one pass over the test sets mode A (cache and self-learning) saved only 35 of 2003 oracle calls (1.75 %): 21 were cache hits (1.05 %) and 14 were questions answered locally after the 16 promotions on MASSIVE (0.70 %). Per set that is 7 of 289 (2.42 %), 4 of 355 (1.13 %) and 24 of 1359 (1.77 %), with cascade correct β1, 0 and 0. The reason is the refit threshold. A label is refitted only after 25 new examples, and a benchmark test set has few abstentions per label: 282 examples spread over the 77 BANKING77 labels and 351 over the 150 CLINC150 labels, so no label reached 25 and the only savings were cache hits. MASSIVE gave 1333 examples over 60 labels: 28 attempts, 16 promoted (14 more questions answered locally) and 12 rejected by the holdout, and the calls per third did not fall steadily. Self-learning pays off on traffic that keeps coming back to the same labels, not on one pass over a benchmark; these numbers do not show a larger effect.
3. Pre-training on unlabelled dev traffic
A base model trained on the train split only (not the published model) was
pre-trained with cortiq decision learn on the dev texts without labels
(1498, 2998 and 2025 texts). Its gate rejected 220, 280 and 727 of them; all
were answered from stored DeepSeek ledgers. 59, 94 and 50 labels were promoted
and 2, 3 and 6 rejected; every gate stayed certified with the same Ο. On the
test sets, pre-trained minus base:
| Ξ (pre-trained β base) | BANKING77 | CLINC150 | MASSIVE |
|---|---|---|---|
| All rows correct | β10 | +23 | +4 |
| Answered by the gate | +17 | β10 | +15 |
| Correct among them | +11 | β7 | +10 |
| Static cascade correct | β5 | +1 | β6 |
| Oracle calls | β17 | +10 | β15 |
The effect is about neutral. The published model uses the labelled dev rows as training data instead, so it is not pre-trained through the oracle; it keeps learning from the oracle and from feedback on your own traffic.