OneHot Python SDK
Synchronous and asynchronous clients for the OneHot API. A decision is one question
with a fixed set of allowed outcomes, and OneHot answers it with a probability for
each outcome. The SDK needs Python 3.11 or newer, and its only dependency is httpx.
Install
pip install sqwishai
The package installs and imports as sqwishai: from sqwishai import OneHot.
Your first decision
Make a key in the console at console.sqwish.ai. Keys look like
onehot_sk_....
import os
from sqwishai import OneHot
with OneHot("https://api.sqwish.ai", api_key=os.environ["SQWISH_API_KEY"]) as client:
result = client.decide(
context="Please refund the duplicate charge on my invoice.",
decisions=[{
"id": "team", "question": "Which team should handle this?",
"outcomes": ["billing", "technical", "sales"],
}],
)
team = result["decisions"]["team"]
print(team["top"], team["p_top"], team["probabilities"])
The address is the server's origin, without /v1. An argument you pass wins. If you leave
out the address or the key, the client reads SQWISH_BASE_URL or SQWISH_API_KEY, and with no
address there either it uses https://api.sqwish.ai. With no key anywhere, a call
raises before anything is sent; only lint and systemone_translate work without one.
Leaving model out uses the server's default base size. To pick a size, pass its id as
model, such as "kettei-core": client.models.list() lists every size with its status.
A family you have trained is called by its name, such as "router", and one of its
models by its number, such as "router@3".
Each decision has an id, a question and its outcomes. kind is "single" (the
default), "binary" (yes or no) or "ordinal" (levels "0", "1" and so on, with one
rubric line per level). temperature, from 0.2 to 5, reshapes every answer's
probabilities: below 1 sharper, above 1 softer, and the top answer never changes. Left out,
the model's own default applies, and temperature=1 gives the probabilities as the model
scored them.
Replies are plain dicts, typed in sqwishai.types, and every type imports from the package as
well: from sqwishai import Job. AsyncOneHot has the same methods, used with await and
async with.
What else it covers
| Call | What it does |
|---|---|
client.lint(context, decisions) |
Warns about how questions are asked. No model runs. |
client.datasets |
create, upload (JSONL, up to 16 MiB), get, list, iter, rows (of any version), rename, delete and restore; add_rows and upload_rows (JSONL or CSV) add a version, versions lists them and download reads one; labels_to_check lists the labels prompt tuning doubted, each with its row |
client.evaluations |
Score models on a dataset's evaluation rows: create, get, list, iter, cancel, retry |
client.fine_tuning |
create (starts a family), continue_family, estimate and estimate_continue (their exact fee first), get, wait, events, records (the held-out answers), cancel, list, iter |
client.prompt_tuning |
Better wording for one decision: create, get, wait, cancel, list, iter; dismiss_label_issues takes a job's doubted labels off its dataset's labels to check |
client.families |
list, get, switch, keep_warm, rollback and delete; wait until a family serves its new model |
client.models |
list, get, ready, switch, keep_warm and delete, each model by its number, such as "router@3" |
client.labeling |
Teacher labels for your own queries: teachers, then sources.upload and jobs.create, wait, results, finalize; sources.delete removes a source and its teacher answers for good |
client.account |
get, and keys.list, a page at a time, active or revoked, each with its scope and, for an inference key, its monthly cap. Keys are made and revoked in the console. |
client.capabilities() |
Models, recipes, limits, training availability and capacity warnings |
Most list calls return one page with has_more and next_cursor, and iter walks every
page. client.models.list(), client.families.list() and client.datasets.versions()
return the whole list. The lists of jobs filter by status, one or a list, and those of
fine-tunes and prompt-tuning jobs by dataset_id too:
client.fine_tuning.list(status=["queued", "running"], dataset_id=dataset_id).
wait polls a job until it ends. The fine-tuning
and labelling guides describe both flows step by step.
client.capabilities().get("warnings", []) contains advisory messages. engine_busy
names the affected catalogue model in model; queue_full and tenant_queue_full
concern fine-tuning. Display message beside the relevant action. Read again to clear
warnings as capacity returns. These snapshots do not reserve capacity, and missing or
stale engine measurements produce no warning, not a promise of spare capacity.
Families and models
A family, such as router, is a line of models, each trained from the one before:
router@1, router@2 and on. A model joins only if it passes its gate. Calls to "router"
get the model the family serves while it is switched on, and calls to "router@1" get that
model while its own switch is on.
quote = client.fine_tuning.estimate(dataset_id, family="router") # the exact fee, before anything starts
job = client.fine_tuning.create(dataset_id, family="router", max_price_usd=quote["price_usd"]) # from core; start_from="router@2" starts from a model
job = client.fine_tuning.wait(job["id"]) # router@1; outcome "joined" if it passed its gate
client.datasets.upload_rows(dataset_id, "more.csv", family="router") # the dataset's next version
quote = client.fine_tuning.estimate_continue("router")
job = client.fine_tuning.continue_family("router", dataset_version=quote["dataset_version"],
max_price_usd=quote["price_usd"]) # its next model, on the rows quoted
job = client.fine_tuning.wait(job["id"]) # a family trains one model at a time
family = client.families.wait("router", job["output_model"]) # calls to "router" now get it
family = client.families.get("router") # served, latest, move, jobs and continue
client.models.switch("router@1", True) # answers calls to its number as well
client.families.rollback("router", "router@1", ["router@2"]) # discards router@2 for good
A model that joins doesn't answer calls to the family's name straight away: a family that is
switched on moves to it by itself once it has loaded and answered a probe. client.families.wait
returns the family once it serves the model you name, the job's output_model or its number.
It raises at once for a wait that can't end: a family switched off never moves, a model that
failed its gate never joined, and a family moves only to its latest model. Running out of time
raises TimeoutError, saying what the move waits for. Continue a family only once its last
fine-tune has finished: while one trains, continue_family raises family_training. It
trains on the dataset's newest version, or the dataset_version you name. The example names
the version it was quoted for, so rows added meanwhile wait for the next model. The version
must be newer than the one the family's last finished job trained on, or the call raises
no_new_data.
A switch or keep warm past your tier's limit raises live_limit or warm_limit. Its
details list what answers or is kept warm, least recently used first, and switch_off=
frees a place in the same call. served= on a family and adapter= on a model are the id you
read: the call raises served_changed or model_changed if it changed since.
sqwishai.types.RefusalCode lists the codes these calls refuse with.
Estimates and paid requests
client.estimate(context, decisions, model=..., fallback=...) returns billed input tokens,
the pinned billing tokenizer, each eligible model's quote_usd, and
maximum_charge_usd, the requested model's price, which a fallback never exceeds. client.systemone_estimate(state, questions, model=...) quotes a
Jev request. Estimates run no model and reserve no money; a later request uses the policy
in effect when it is accepted.
client.fine_tuning.estimate(...) takes the arguments of create, and
client.fine_tuning.estimate_continue(family, ...) those of continue_family, without
max_price_usd or idempotency_key. Each returns
the exact price_usd the job would hold if it started now, with its trained_tokens, steps,
seed, passes, the rows it takes (new, earlier and evaluation) and whether
minimum_applied. Pass price_usd as max_price_usd: a job that costs more by then, because
a price, the dataset or the family changed, raises price_changed and holds nothing.
decide and systemone send one generated operation key per call and reuse it for
network and retryable API failures. To survive a restart or retry a timed-out call, persist
your own idempotency_key="your-operation-id" and pass the same payload and key again. A
different payload with the same key returns idempotency_conflict; a new key creates a new
billable operation.
When a call fails, its APIError names the key it sent as idempotency_key: yours, or the
one the SDK made. After an error that is retryable, or whose idempotency_status is
"pending", call again with that key and the same payload. A pending error is a
409 request_in_progress with an operation_id: the server accepted the decision, and it is
still running or its end isn't known yet. Only its key asks about it. A call without the key
is a new operation, and you are charged for both if the first was answered after all.
import time
from sqwishai import APIError
ticket = "Please refund the duplicate charge on my invoice."
refund = [{"id": "refund", "kind": "binary", "question": "Does the customer ask for money back?"}]
try:
result = client.decide(ticket, refund)
except APIError as error:
if not (error.retryable or error.idempotency_status == "pending"):
raise
time.sleep(error.retry_after or 2)
# The same key asks about the same decision. Without it, this would be a second one.
result = client.decide(ticket, refund, idempotency_key=error.idempotency_key)
The same holds for every call that takes idempotency_key, such as fine_tuning.create: a
second call without the key starts a second job. A call that got no answer leaves its outcome
unknown too: it ends with TimeoutError, or with the httpx.TransportError of a lost
connection, and that error carries the same idempotency_key attribute. Call again with it.
getattr(error, "idempotency_key", None) reads the key from any of these errors, and is
None for a call that sent none, such as a read.
The SDK sends a key with every decide and systemone, so it never meets
409 outcome_unknown. A request sent over plain HTTP without an Idempotency-Key gets that
when the server can't say how an accepted decision ended. It is not retryable: nothing names
that decision, so a request sent again would be a second one and could be a second charge.
The answer's charge contains amount_usd (the served model's final cost), reserved_usd
(the maximum reserved), currency, and operation_id. USD values are exact decimal
strings; use decimal.Decimal for arithmetic. Unused reserved credit is released.
Replaying a completed operation returns its stored answer without running or charging
again. Once the answer expires, the key still prevents rerunning it and returns
idempotency_response_expired.
A final failure releases its hold. APIError.idempotency_status == "terminal" and
operation_id identify this outcome, and the SDK stops retrying it. The one exception is
model_warming with wait_for_warm: that failure charged nothing, so the SDK sends the
decide again under a new key after each Retry-After, until wait_for_warm runs out. It
does this only for a key it made; with your own idempotency_key it raises, and you start
a new call deliberately.
Errors and retries
Every API error raises APIError. It carries status, a stable code such as
model_answers, the message as str(error), request_id, details (on a 422, each bad
field with its path) and retryable, which says whether the same request can succeed later.
idempotency_key is the key the call sent, or None for a call that sent none, such as a
read: send a paid request again with it.
from sqwishai import APIError
try:
client.models.delete("router@2")
except APIError as error:
print(error.code, error) # model_answers: its family serves it, or its own switch is on
The SDK retries by itself up to max_retries times (default 2) when an error is
retryable, or when a reply without that flag is a 429, 502, 503, 504 or 529. It waits for
Retry-After when the server sends one, and a random tenth of it at most more, so clients refused
together don't all come back at once, unless that would leave the call no time to try again:
then it raises the error at once, with retry_after, as for warm_capacity. A create keeps one Idempotency-Key across those
retries, and the error that ends them names it; pass idempotency_key= to keep it across
restarts as well. timeout (default 60 seconds) bounds a whole call, retries included, and
ends it with a TimeoutError that names the key too.
Some calls are never repeated automatically, because a lost reply leaves their outcome
unknown: a prompt-tuning job's cancel, a family's delete, and a family's switch that
names served, since a second one would find the family gone or moved. Check retryable, or
read the job with prompt_tuning.get or the family with families.get, before you send one again.
A client keeps its connection open between calls for up to keepalive_expiry seconds (default
180), so a call made within that time skips the setup of a new connection, about 100 ms. Pass
keepalive_expiry=None to keep it until the server closes it; with an http_client of your own,
set it on that client instead. When the server has closed a kept connection just as a call goes
out, a call that is safe to send twice (a read, or one with an Idempotency-Key) goes again at
once on a new connection, without using a retry; any other call raises as above.
wait() raises JobFailed for a failed or cancelled job, with the whole job in
error.job. Running out of time stops the waiting, not the job.
Moving from Jev
client.systemone(state, questions, model=...) sends TypeSafe's Jev request shape to
POST /v1/systemone and returns Jev's answer shape. It needs a model: a size's id from the
console or client.models.list(). Jev's own model ids are accepted too and run on one of our
base sizes, and the answer's model says which. A field that OneHot doesn't model is
rejected with a 422 naming it, rather than ignored.
# A base model that is serving, not one of your fine-tunes (which carry an adapter, `lora`). Or
# use the id shown in the console.
size = next(m["id"] for m in client.models.list() if m["status"] == "ready" and not m.get("lora"))
answer = client.systemone(
"My parcel arrived broken.",
{"refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"}},
model=size,
)
print(answer["answers"]["refund"]["noul"])
client.systemone_translate(state, questions, model=...) takes the same arguments and
returns the native request as request, ready for client.decide(**translated["request"]),
with notes on what changed. The notes include how a Jev confidence threshold becomes a
p_top threshold. It runs no model and needs no key.
More
- api.sqwish.ai/openapi.json is the source of truth for every field.
Dataset creation and JSONL upload accept evaluation_fraction from 0.1 to 1, defaulting to
0.2. Teacher-label finalisation accepts the same option. A value of 1 creates an
evaluation-only dataset. The saved split cannot be edited.
Dataset list and iter accept archived to read the datasets we archived, when credit
couldn't cover their storage or the account was suspended; restore brings one back for new
work. rows accepts partition (all, training or evaluation). rename changes only the
name. delete removes a dataset for good, with every version's upload and the copies its
fine-tunes kept; the fine-tunes and their models stay, and work still reading it refuses.
Metadata
Release files for sqwishai 0.3.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sqwishai-0.3.6.tar.gz | 85.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqwishai-0.3.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 140.7 kB
Release files / sqwishai-0.3.6.tar.gz
| Download URL | sqwishai-0.3.6.tar.gz |
|---|---|
| Size | 85.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
437eb52db67fae3d7e7694f939faec80480bc40c93d5b4233f940ae30c513992
|
|
BLAKE2b-256 checksum How to use checksums |
7ce0e995d546fa5f294e85b3c464766e2824f6ebe16bc267846ebe5fd5951fe3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.10 {"installer":{"name":"uv","version":"0.9.10"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / sqwishai-0.3.6-py3-none-any.whl
| Download URL | sqwishai-0.3.6-py3-none-any.whl |
|---|---|
| Size | 55.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
db2d7f7bc4573a8742d2c5a2163ac93952845c1c77144eec133961e78f3cd880
|
|
BLAKE2b-256 checksum How to use checksums |
25b7ea16b5fe3012bd89f996b7b76a472942062f9c4abea307a9ff91b44481f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.10 {"installer":{"name":"uv","version":"0.9.10"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|