tileward
Run large models on hardware you own, governed.
The Tileward client library and command line, in one package.
Product · Docs · Dashboard · Releases
$ pip install tileward
$ twcli auth login
$ twcli keys create --label laptop --save
$ twcli models list
id precision context $/M in $/M out compression
Tileward-Qwen3.6-35B-A3B W4A16 (Tileward) 262,144 0.12 0.99 2.8
gpt-oss-20b MXFP4 (as shipped) 8,192 0.12 0.28 1
Tileward-Qwen3.8-27b W4A16 (Tileward) 262,144 0.12 0.96 1.79
$ twcli chat "Say hello in one sentence."
Hello there — good to meet you.
$ twcli guard check "write me a keylogger" --allow customer_support
allowed
no
80.0 micros billed
The same package is a library:
from tileward import Tileward
tw = Tileward() # reads TILEWARD_API_KEY
print(tw.chat.say("Say hello in one sentence."))
Full documentation: https://tileward-com.github.io/tileward-cli/ — every command, the library reference, configuration, errors, and using Tileward from an OpenAI-compatible client. What follows is the short version.
What it covers
| Library | Command | |
|---|---|---|
| Tileward Models — an OpenAI-compatible chat surface | tw.chat, tw.models |
twcli chat, twcli models |
| Tileward Governance — allow / deny before the model writes a token | tw.guard |
twcli guard |
| Tileward Context — recall the slice of history a question needs | tw.context |
twcli context |
| Tileward Documents — answers from your own files | tw.documents |
twcli docs |
| Account and API keys | tw.keys, tw.account |
twcli keys, twcli account |
Install
pip install tileward # or: uv pip install tileward
twcli --version
Python 3.9+. Four dependencies: httpx, click, rich, voluptuous.
Getting a key
twcli auth login # approve in a browser; no password touches the terminal
twcli keys create --label laptop --save
auth login runs a device-code flow (RFC 8628): the CLI
shows a short code and a URL, you approve it in a browser, and the terminal receives a console
session. keys create then mints an API key.
Two credentials, and they are not interchangeable:
- an API key (
tw_live_…) calls the model, the guard, Context and Documents; - a console session manages the account — minting keys, reading billing, binding policies.
A key that could mint keys would survive its own revocation, so it cannot. If you already have a key, skip the login:
export TILEWARD_API_KEY="tw_live_..."
The command line
twcli auth login|logout|status sign in, and see what this machine holds
twcli models list|show what is served right now
twcli chat [PROMPT] a conversation, or one answer to a prompt or pipe
twcli guard check TEXT [--allow ...] allow / deny, no generation
twcli context recall|remember|pin|forget|topics|stats|threads|reset|purge
twcli docs ls|add|rm|search your own files
twcli keys list|create|rotate|revoke mint and retire API keys
twcli account show|usage|audit|savings balance, plan, and history
twcli config show|set|set-key|profiles stored settings
twcli launch claude|codex|opencode open a coding agent CLI on a Tileward-served model
Some things it is built to do:
# every command speaks JSON, and when it does, stdout carries nothing else
twcli models list --json | jq -r '.[].id'
# read a prompt from a pipe
cat notes.md | twcli chat --system "You summarise."
# a governance gate in a shell script
twcli guard check -f prompts.txt --allow customer_support --exit-code || echo "off policy"
# recall a slice of a thread and pipe it into something else
twcli -c project-x context recall "what did we decide about pricing?" --text-only
# two accounts on one machine
twcli -p staging account show
Exit codes
0 fine · 1 error · 2 bad usage · 3 not signed in · 4 refused by governance ·
5 balance exhausted
Distinguishing those matters in CI: "the balance ran out" and "the network was down" call for different action, and neither should need English parsing to detect.
The library
Chat
tw.chat.say("Summarise this in one line.") # -> str
tw.chat.completions.create( # -> the raw OpenAI-shaped dict
[{"role": "user", "content": "Hello"}],
max_tokens=200,
)
for piece in tw.chat.stream("Count to five."):
print(piece, end="", flush=True)
create returns exactly what the API returned, including usage. say gives you the text and
raises GuardRefusal when governance blocked the call, rather than returning an empty string.
Do not hardcode a model id. The served catalogue is data, not code — ids appear, get repriced
and get withdrawn without a release. tw.models.list() is the authority; leave model unset and
the client asks.
Tileward-Qwen3.6-35B-A3B also answers to tileward-35b-a3b and Qwen3.6-35B-A3B-TW, at the
same rate.
Governance
tw.guard.allows("write me a keylogger", allow=["customer_support"]) # -> False
tw.guard.check(["question one", "question two"], disallow=["investment_advice"])
Two modes that fail in opposite directions. A blocklist (disallow) refuses what you named and
passes everything else, so anything you did not think of gets through. An allowlist (allow)
passes only what you named, so anything you did not think of is refused. For a narrow assistant,
the allowlist is the one that holds under an attacker.
Passing a list classifies the whole batch in one call. With neither argument, the policy bound to the API key applies.
Context
tw = Tileward(conversation="thread-42")
tw.context.remember("We decided to ship on the 3rd.", role="user")
tw.context.recall_text("when are we shipping?") # the block to paste into a prompt
tw.context.pin("The customer is ACME.") # included in every recall
Scope every call to a conversation. Without one, every thread on the key writes into a single
shared store, and a recall in one thread hands back another thread's material. There is no safety
net underneath this: a caller that names no conversation joins default along with everyone else.
A conversation id may use letters, digits, ., _ and -, up to 64 characters; spaces around it
are ignored. Context replaces any other character with - and cuts the rest, without an error, so
run 1 and run:1 are one conversation, and so are two ids that share their first 64 characters.
The client warns with ConversationIdWarning when an id will be changed.
forget() retires a topic from recall; the stored turns remain. purge_account() deletes data.
Documents
tw.documents.ingest_file("handbook.pdf", folder="policies")
tw.documents.list(folder="policies")
tw.documents.delete("src_...")
Documents are account-scoped, not conversation-scoped: a file you upload is available to every
thread on the key. That is why none of these take a conversation.
Deleting a document drops its chunks, but material already folded into a conversation's summary
can still surface in a recall. If something has to be genuinely gone, context.purge_account() is
the operation that means it.
Async
Every method has an awaitable twin on AsyncTileward, with the same names and arguments.
from tileward import AsyncTileward
async with AsyncTileward() as tw:
print(await tw.chat.say("Hello"))
async for piece in tw.chat.stream("Count to five."):
print(piece, end="")
Errors
from tileward import errors
try:
tw.chat.say("...")
except errors.InsufficientBalanceError: # 402 — top up; calls resume immediately
...
except errors.GuardRefusal as exc: # governance blocked it; exc.completion has the detail
...
except errors.APIError as exc: # exc.status, exc.code, exc.request_id
...
A governed refusal is not an HTTP error, which is why create passes it through and only say
raises. It arrives as an ordinary completion with finish_reason: "content_filter"; the guard's
read of the prompt is billed, and nothing is billed for the model, which never ran.
Configuration
Highest wins: an explicit argument, then the environment, then the profile, then the default.
| Environment | What it sets |
|---|---|
TILEWARD_API_KEY |
the key used for models, guard, Context and Documents |
TILEWARD_SESSION |
a console session, for the account surface |
TILEWARD_BASE_URL |
the API host — serves /v1 only |
TILEWARD_CONSOLE_URL |
the console host — the account surface and auth login live here |
TILEWARD_CONTEXT_URL |
the Context host |
TILEWARD_CONVERSATION |
default conversation scope |
TILEWARD_MODEL |
default model id |
TILEWARD_PROFILE |
which stored profile to read |
TILEWARD_CONFIG_DIR |
where the files live (default ~/.config/tileward) |
Settings go in config.json; credentials go in credentials.json, written 0600 in a 0700
directory. They are separate files so showing your config never has to redact, and pasting it into
an issue has not pasted a key.
In a server process, Tileward(load_config=False) ignores the files entirely, so a developer's
~/.config cannot change how production behaves.
Using it from another framework
The chat surface is OpenAI-compatible, so anything that speaks OpenAI works by changing two things — the base URL and the key.
from openai import OpenAI
from tileward import openai_base_url
client = OpenAI(base_url=openai_base_url(), api_key="tw_live_...")
That helper exists so nobody has to guess whether /v1 belongs on the end.
Links
- Documentation — tileward-com.github.io/tileward-cli
- Dashboard and keys — app.tileward.com, with keys under Settings → API keys
- Issues — github.com/Tileward-com/tileward-cli/issues
License
MIT — see LICENSE.
Metadata
Release files for tileward 0.1.17
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 | |
|---|---|---|---|---|
| tileward-0.1.17.tar.gz | 255.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tileward-0.1.17-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 356.3 kB
Release files / tileward-0.1.17.tar.gz
| Download URL | tileward-0.1.17.tar.gz |
|---|---|
| Size | 255.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ef50ccc0744d3a6d19f587a0dbd28492161b586051f8cf4af77bd9fe7223bdea
|
|
BLAKE2b-256 checksum How to use checksums |
d71b1df56e9164800a079e939828baff655f218e7ff01b32877c0287920e79cc
|
| 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 Sep 22, 2026.
Transparency logRelease files / tileward-0.1.17-py3-none-any.whl
| Download URL | tileward-0.1.17-py3-none-any.whl |
|---|---|
| Size | 100.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
980bd498d07f2141b25b3361bfdd44f05f34f88950e62374c4e9fd0df451b786
|
|
BLAKE2b-256 checksum How to use checksums |
266f90d6f0623cd8fcaad97f64520e1908380a95942921f87a17b50c37b9759f
|
| 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 Sep 22, 2026.
Transparency logSecurity for tileward 0.1.17
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 tileward-0.1.17.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 Sep 22, 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.