Guides
Coming from Jev
Wity speaks the same System One request and answer format as Jev, so an existing integration switches over by changing two settings. This guide covers what stays the same, what the response adds, the behaviour differences worth knowing, and a safe way to roll the switch out.
Switch in two settings#
Point your client at Wity's base URL and use a Wity key. If your client sends a model name, set it to wity-1; Wity accepts any value, so this is only for your own logs.
# beforeTYPESAFE_ENDPOINT=https://api.typesafe.aiTYPESAFE_API_KEY=...# afterTYPESAFE_ENDPOINT=https://wity-proxy-production-2c33.up.railway.appTYPESAFE_MODEL=wity-1TYPESAFE_API_KEY=wity_...
Code that already calls /v1/systemone needs no changes. The one thing worth checking is the client timeout: Wity answers most questions in about a tenth of a second, but a close call can take a few seconds while it thinks. Allow at least 60 seconds.
import os, requestsBASE = os.environ["TYPESAFE_ENDPOINT"]KEY = os.environ["TYPESAFE_API_KEY"]def decide(state, questions):r = requests.post(f"{BASE}/v1/systemone",headers={"Authorization": f"Bearer {KEY}"},json={"state": state, "questions": questions},timeout=60, # Wity may think on close calls; allow time for it)r.raise_for_status()return r.json()["answers"]
What stays the same#
- The endpoint path
/v1/systemone(also served as/v1/system-one) and bearer-token authentication. - The request:
stateas a string, object or array, andquestionswithtype,instructionsandcriteria. Amodelfield is accepted and ignored. - The three question types and their answers:
choicewithprobabilitiesover every option,noulas the probability of yes, andscorewith an expected level and level probabilities. - Answers come back under the names you gave the questions, so code that reads them by name keeps working.
Here is an unchanged Jev request, model field and all, sent to Wity:
{"model": "jev-1.13.0","state": "Customer says the card reader at the Harbour St store has shown 'connection lost' since this morning. Payments are going through on the backup terminal.","questions": {"team": {"type": "choice","instructions": "Which team should handle this?","criteria": {"payments": "Card terminals, payment failures, refunds","network": "Store connectivity, Wi-Fi, routers","hardware": "Physical damage or replacement of devices","other": "Anything else"}},"outage": {"type": "noul","instructions": "Is the store unable to take payments right now?"}}}
What the response adds#
The answer fields a Jev client reads keep their names and meaning. Wity adds a few fields next to them. A client that ignores fields it does not know, as most JSON clients do, keeps working without changes.
{"model": "wity-1","answers": {"team": {"type": "choice","choice": "network","probabilities": { "payments": 0.31, "network": 0.66, "hardware": 0.02, "other": 0.01 },"confidence": 0.44,"direct_probabilities": { "payments": 0.49, "network": 0.47, "hardware": 0.03, "other": 0.01 },"reasoning": { "mode": "auto", "thought": true, "reason": "close_call", "forecast": false, "thought_tokens": 162 }},"outage": {"type": "noul","noul": 0.04,"reasoning": { "mode": "auto", "thought": false, "reason": null, "forecast": false, "thought_tokens": null }}},"usage": { "input_tokens": 312, "output_tokens": 0 },"metadata": { "reasoning": "auto", "elapsed_ms": 1620.4 }}
close_call, order_sensitive, forecast or requested.direct_noul for noul questions).input_tokens is what you are billed for; output_tokens is always 0 for decisions.elapsed_ms on our side.In the example, the ticket mentions a card reader, which pulls towards payments, but the actual symptom is "connection lost". The direct read was a coin flip (0.49 / 0.47), so Wity thought about it and settled on network. The outage question was clear straight away (payments are still going through on the backup terminal), so it was answered without thinking. Both came back in the same response.
Behaviour differences#
Thinking is on by default
Jev answers every question in a single pass. Wity's reasoning field defaults to "auto": clear questions still get an instant direct answer, and close calls get a short thought first. Your code sees the same answer shape either way; only the latency differs.
- Keep
autofor most workloads. You get Jev-like speed on the easy majority and better answers on the hard minority. - Set
"off"where every call has a hard latency budget, such as a step in an interactive UI. That matches a single-pass setup exactly. - Set
"always"for batch jobs where each decision is expensive to get wrong and nobody is waiting.
See Reasoning & auto mode for how auto mode decides.
You can stop decomposing hard questions
Jev's guidance for questions that need reasoning is to break them into simpler steps and chain the calls. That works around a model that cannot think, and it costs a round trip and a request's worth of tokens per step.
# With a single-pass model, a hard question is often broken into stepsdays = decide(state, {"days": {"type": "choice", "instructions": "How many days since delivery?", "criteria": DAY_BUCKETS}})faulty = decide(state, {"faulty": {"type": "noul", "instructions": "Is the item faulty?"}})eligible = decide({**state, "days": days, "faulty": faulty},{"eligible": {"type": "noul", "instructions": "Given the above, is a refund due?"}})
With Wity, ask the question you actually have. When the direct answer is uncertain, auto mode reasons through the steps itself, in one call:
# With Wity, ask the question you actually haveanswers = decide(state, {"eligible": {"type": "noul","instructions": "Under the policy in the state, is the customer owed a refund?",}})# reasoning defaults to "auto": Wity thinks through the dates and the policy# only when the direct answer is not clear.
Keep separate questions when you need the separate answers, for example because your code branches on whether the item was faulty. Just do not split one judgement into steps for the model's sake. See Writing good questions.
Option order no longer matters
Wity checks that each answer holds whichever order the options are presented in, and thinks when it does not (the order_sensitive reason). If you added a workaround for order effects, such as shuffling options and averaging several calls, you can remove it and make one call instead.
Re-tune your thresholds
Wity's probabilities are on the same 0 to 1 scale, but they are not Jev's probabilities. A threshold you tuned on Jev (act on its own above 0.8, send to a person below) is a sensible starting point, not a finished setting. Run your test set through Wity, look at where right and wrong answers fall, and set the thresholds again. See Probabilities & confidence.
What you can use now#
Once you are on Wity, these work from the same key with no other setup:
- Images. Add an
imageto a request and every question sees it with the state: a photo of the damage, a screenshot for an agent, a scanned form. See Images. - generate. Jev returns decisions only. Wity's
/v1/generatewrites short text or JSON in a shape you define, from the same state: the value to type after deciding to type. See Generate. - Forecasts. How-likely questions come back with worked-out odds instead of a lean. See Forecast questions.
- Billing on your input only. Thinking and answers are never billed, so auto mode does not change what a request costs. See Pricing.
Sessions, which keep a running state across a whole task instead of one state per request, are coming soon. The /v1/systemone format will stay stable as they arrive.
Rolling it out#
A switch you can trust takes a few steps, not one:
- Replay your test set. Run the cases you already use to check Jev through Wity with the new settings, and compare answer by answer.
- Set thresholds and modes. Re-tune thresholds on those results, and pick a
reasoningmode for each workflow based on its latency budget. - Shadow live traffic. Keep acting on Jev's answers, send a sample of the same requests to Wity, and log where the two disagree. Read the disagreements: they show you which cases change.
- Switch. Once the disagreements look like improvements, change the two settings in production. Rolling back is the same change in reverse.
Here is a simple shadow wrapper. Production still gets Jev's answer, and a failure on the Wity side is logged and ignored:
import json, random, requestsdef call(base, key, body):r = requests.post(f"{base}/v1/systemone", headers={"Authorization": f"Bearer {key}"}, json=body, timeout=60)r.raise_for_status()return r.json()["answers"]def decide(state, questions):body = {"state": state, "questions": questions}live = call(JEV_BASE, JEV_KEY, body) # still what production acts onif random.random() < 0.10: # shadow 10% of traffictry:shadow = call(WITY_BASE, WITY_KEY, body)for name, a in live.items():b = shadow[name]if a.get("choice", a.get("noul")) != b.get("choice", b.get("noul")):log.info("disagree %s", json.dumps({"q": name, "jev": a, "wity": b}))except Exception:log.exception("shadow call failed") # never let the shadow break productionreturn live
Benchmarks