intura-ai
Run the Intura AI models you own on your own CPU.
Intura makes lightweight fine-tuned models for specific tasks, like routing a customer-support ticket to the right team or tagging what a social post is asking for. You buy a model once, which gets you every version of it. This package downloads the model and runs it in your process. After the download there is no network call, no per-request price, and your text never leaves your machine.
from intura_ai import Intura
model = Intura().models.load("chat-customer-support-ticket-en")
model.predict("I was charged twice, please refund me")
# {'labels': ['billing_payment', 'refund_return'],
# 'scores': {'billing_payment': 0.6956, 'refund_return': 0.8482, ...}}
Install
pip install intura-ai
# or
uv add intura-ai
Python 3.10–3.13. This installs everything a prediction needs: onnxruntime
(CPU), tokenizers, numpy and httpx for the download. The old local
extra still resolves, so pip install "intura-ai[local]" keeps working, but it
adds nothing.
Get a download key
Get a model from https://ai.intura.co/models, then create a download key
on your account page. Give it to the client as
an argument, or in INTURA_API_KEY:
export INTURA_API_KEY=sk_...
from intura_ai import Intura
client = Intura() # reads INTURA_API_KEY
client = Intura(api_key="sk_...") # or pass it explicitly
The key is only used to download. Intura() works without one, and so does
loading a model that is already downloaded. The first call that needs the API
raises MissingAPIKey and tells you where to get a key.
Load a model, then predict
model = client.models.load("chat-customer-support-ticket-id")
model.predict("kak, saya sudah bayar tapi pesanan belum dikirim")
# {'labels': ['order_delivery'], 'scores': {'billing_payment': 0.0056, 'order_delivery': 0.9932, ...}}
[r["labels"] for r in model.predict_batch(["tidak bisa login, kode OTP tidak masuk", "terima kasih kak"])]
# [['account_access'], []]
On first use, load downloads the model into a local cache. After that it
loads from disk in well under a second. Load once per process and keep the
model. A loaded model holds no connection and no key, and you can share it
across threads.
predict_batch(texts) gives the same result as [model.predict(t) for t in texts],
in the same order. It runs one text per forward pass on purpose. The
published models are INT8 graphs with dynamic quantization, which scales each
activation tensor by its own range. A padded batch is one tensor, so batching
would shift every item's logits, sometimes by more than a whole logit, and that
can flip a decision near a cut. For throughput, call predict_batch from
several threads (onnxruntime releases the GIL while it computes).
You can find each model's slug in client.library.list(), on its page on the
website, or by running intura-ai library.
What predict returns
Each model carries its own decision rule and cut-offs in its serving.json.
predict applies them, so a new version that moves a threshold gives the right
answer as soon as it is downloaded. These are the same five rules as the
plain-Python script on each model page:
Rule (decision) |
Models | predict(text) returns |
|---|---|---|
Multi-label (sigmoid-per-label) |
customer-support ticket (id, en), Threads post intent | {"labels", "scores"}. Every label whose own sigmoid reaches threshold (or that label's entry in thresholds, when the model has one), plus a score for each label. The scores do not sum to one. No label past the cut gives "labels": []: for a support ticket, a message that asks for nothing. |
Argmax with fallback (argmax-with-fallback, argmax) |
— | {"label", "score"}. The top class and its probability, or the model's fallback label (e.g. "other") when the top class scores below threshold. |
One positive class (positive-class) |
— | {"label", "score"}. score is always the positive class's probability. label is the positive class at or above threshold, otherwise the other label. |
Risk sum with bands (risk-sum-with-bands) |
— | {"label", "decision", "score"}. score is the summed probability of the risk classes. decision is "block" at or above threshold, "review" at or above review_threshold, otherwise "allow". |
| Token classification | — | A list of {"type", "start", "end", "value"} spans, in text order. text[start:end] == value. |
The rule comes from the model's own files, never from its name, so a model
released after your version of this package still works. A decision name the
package has never seen is read from the keys the model carries instead
(positive_label, risk_labels, otherwise the top class).
Scores are rounded to four decimals. To apply a rule of your own, such as a
stricter cut for one label, model.scores(text) gives every label's unrounded
probability with no cut applied. model.labels, model.serving (the whole
serving.json), model.task and model.version are there too.
posts = client.models.load("chat-threads-post-intent-id")
posts.predict("kecewa banget, paket udah seminggu belum sampai")["labels"]
# ['complaint', 'review_experience']
scores = posts.scores("rekomendasi sunscreen buat kulit berminyak dong")
[label for label, p in scores.items() if p >= 0.9]
# ['ask_recommendation']
Versions and the cache
Buying a model gets you every version of it. load decides which one to use
like this:
| Call | What it loads | Network |
|---|---|---|
load(slug) |
The newest version already in the cache. If none is cached, the recommended version, downloaded first. | Only when nothing is cached |
load(slug, version="v1.0") |
Exactly that version, downloaded if it is missing | Only when that version is missing |
load(slug, refresh=True) |
Asks which version is recommended now and downloads it if you don't have it | Always (one small call, plus the download if the version is new) |
So once a model is cached, load(slug) works offline and without a key.
Nothing updates behind your back. Run with refresh=True (or
intura-ai predict --refresh) when you want the latest. Older versions stay in
the cache, so you can pin back to one with version=.
The cache is laid out <cache>/<slug>/<version>/. Its location is the first of
these that is set:
load(..., cache_dir="./models")$INTURA_CACHE_DIR$XDG_CACHE_HOME/intura~/.cache/intura
A partly downloaded model is never mistaken for a complete one. Every download
goes into a hidden staging folder next to its destination. That folder is
renamed to <version>/ only after every file has arrived at the size the bucket
reports. If a download is interrupted (dropped connection, Ctrl-C, full disk),
no version folder is left behind, and the next load starts again. To free
space, delete a version folder, or the whole cache.
To put a model in a folder of your own choosing, for example in a container
image or on a machine with no internet access, use download, then load that
folder:
from intura_ai import LocalModel
path = client.models.download("chat-customer-support-ticket-id", "./models", version="v1.0")
# ./models/chat-customer-support-ticket-id/v1.0/
model = LocalModel.load(path)
download is atomic in the same way. It keeps a complete bundle that is already
there, unless you pass overwrite=True. client.models.links(slug) returns the
signed URLs without downloading anything, if you want to use your own
downloader.
The download key only signs the links. It authenticates one call to
POST /v1/platform/downloads/models/{slug}/download, which answers with GET URLs
on Cloud Storage that are valid for 15 minutes. The files come straight from
Cloud Storage, and the key is never sent there. If a link expires partway
through a download, it is re-signed once.
Your library
for item in client.library.list():
print(item.code, item.kind, item.current_version, item.versions)
# chat-customer-support-ticket-id model v1.0 ('v1.0',)
# template-support-reply template None ()
This is the same list as the library page on the website: models, templates,
courses and apps. For a model, code is the slug you pass to load, and
versions lists every build you can pin. Everything the service sent is also
on item.data. client.models.list() lists the downloadable models, with the
recommended version of each.
Async
from intura_ai import AsyncIntura
async with AsyncIntura() as client:
model = await client.models.load("chat-customer-support-ticket-id")
model.predict("ongkirnya berapa?")
Only the API calls and the download are awaited. Reading the model from disk
happens once and is synchronous, and predict is the same CPU-bound
LocalModel method as in the sync client. Inside an event loop, run a large
batch with await asyncio.to_thread(model.predict_batch, texts).
CLI
$ export INTURA_API_KEY=sk_...
$ intura-ai library # what your account owns
$ intura-ai predict chat-customer-support-ticket-en "I was charged twice, please refund me"
$ intura-ai predict chat-customer-support-ticket-id "paket belum sampai" "mau refund" --field labels
$ cat posts.txt | intura-ai predict chat-threads-post-intent-id --field labels
$ intura-ai predict ./models/chat-customer-support-ticket-id/v1.0 "..." # a downloaded folder
$ intura-ai predict chat-threads-post-intent-id "..." --refresh # fetch the newest
$ intura-ai download chat-customer-support-ticket-id --dir ./models --version v1.0
$ intura-ai models # what you may download
predict loads the model the same way client.models.load does, so the first
run downloads it and later runs work offline. With one text, it prints the
result as JSON. With several texts, given as arguments or one per line on stdin,
it prints one compact JSON value per line. --field labels (or label,
decision, 0.type, ...) prints a single value instead. Errors exit non-zero with one line
on stderr.
Errors
Everything this package raises descends from InturaError. Once a model is
loaded, predict raises nothing from this list, because it runs in your
process.
| Exception | When |
|---|---|
MissingAPIKey |
A download was needed and no key is set |
AuthenticationError |
The key is wrong or revoked |
NotFound |
A model you don't own, or a version that doesn't exist |
RateLimited |
Too many calls in a short time. Retried for you first, and retry_after says when to try again |
TransferError |
Cloud Storage refused or dropped a file. Nothing was cached, so it's safe to retry |
APIConnectionError / APITimeoutError |
The API could not be reached. A model already in the cache still loads |
LocalModel.load raises FileNotFoundError for a folder that isn't a
downloaded model.
Configuration
| Setting | Argument | Environment |
|---|---|---|
| Download key | Intura(api_key=...) |
INTURA_API_KEY |
| Cache folder | load(..., cache_dir=...) |
INTURA_CACHE_DIR, then XDG_CACHE_HOME |
| API root | Intura(base_url=...) |
INTURA_BASE_URL (default https://ai.intura.co/api) |
For a proxy or a custom transport, bring your own httpx client. The package sets the URL and headers on each request, and never closes a client it did not open:
client = Intura(http_client=httpx.Client(proxy="http://proxy:3128"))
Upgrading from 2.x
3.0 is only "download a model you own and run it". The hosted endpoints are removed, along with their result types:
| 2.x | 3.0 |
|---|---|
client.guardrails.pii_detection.mask(text) |
client.models.load("guardrails-pii-detection-id").predict(text) returns the spans; mask them yourself |
client.guardrails.prompt_injection.detect(text=...) |
client.models.load("guardrails-prompt-injection-id").predict(text) |
client.fraud.chat_scam.detect(message) / email_phishing.detect(body) |
load("fraud-chat-scam-id") / load("fraud-email-phishing-en"), then .predict(text) |
client.chat.commerce_intent.classify(message) |
client.models.load("chat-commerce-intent-id").predict(message) |
client.documents.*, client.storage.upload |
removed, no local successor yet |
Inference, PromptInjectionVerdict, PIIResult, PhishingVerdict, ScamVerdict, IntentResult, DocumentVerdict, Extraction, EntitySpan |
removed. predict returns plain dicts (see What predict returns) |
verdict.thresholds, thresholds= per call |
The cut-offs are in model.serving; apply your own rule to model.labels if you need different ones |
client.verify_key(), client.info(), client.health(), Intura.endpoints() |
removed. client.library.list() checks a key |
QuotaExceeded, PayloadTooLarge, UnsupportedMediaType |
removed |
Intura() without a key raised |
constructs. The first call that needs the key raises |
pip install "intura-ai[local]" |
pip install intura-ai (the extra is now empty) |
CLI run, info, health, verify, endpoints |
intura-ai predict, library, models, download |
client.models.list(), links(), download(), client.library.list() and
LocalModel.load(path).predict(text) work as they did in 2.2. Pin
intura-ai<3 if you still need the 2.x client while you move.
Links
- Models: https://ai.intura.co/models
- Account and download keys: https://ai.intura.co/account
- Issues: https://github.com/intura-co/intura-ai/issues
Help
- Help center: https://ai.intura.co/help
- Discord community: https://discord.gg/mztkZSmJuF
- Email: hello@intura.co
MIT licensed.
Metadata
Release files for intura-ai 3.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| intura_ai-3.0.0.tar.gz | 495.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| intura_ai-3.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 535.7 kB
Release files / intura_ai-3.0.0.tar.gz
| Download URL | intura_ai-3.0.0.tar.gz |
|---|---|
| Size | 495.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3fcbda03a3126b17bda445ce80c1ec0252c190d908430ac7a7b8f4b64e119ff6
|
|
BLAKE2b-256 checksum How to use checksums |
3b35f1e9a5adb1594df9a78b62d62062f094e64d73d927e482abef60a474ceab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / intura_ai-3.0.0-py3-none-any.whl
| Download URL | intura_ai-3.0.0-py3-none-any.whl |
|---|---|
| Size | 40.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7fcd580e884ac2f537066c916f34cf399a5a88fce7d20f2ee42be7dc6bd5374f
|
|
BLAKE2b-256 checksum How to use checksums |
a8bfe581bf6661ef1a0b01fd180693253744b11f7f1595b1e338c53d8f528792
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logSecurity for intura-ai 3.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for intura_ai-3.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.