model sdk built by the 9th ditrict at tooig
Project description
nineth
nineth is the public Python, JavaScript, and TypeScript SDK for the district's model API.
This guide is caller-facing and SDK-specific.
If you maintain server internals, use README.md.
Table of Contents
- Install
- Quick Start
- JavaScript and TypeScript
- Public Surface
- Client Construction
- Provider Notes
- Model Catalog
- Request Arguments (Complete)
- Payload Mapping Reference
- VCache Lifecycle
- Callback Lifecycle Matrix
- Full End-to-End Request (All Parameters)
- Cookbook
- Recipe 1: Health check
- Recipe 2: Per-request model override
- Recipe 3: Reasoning and sampling
- Recipe 4: Request JSON output
- Recipe 5: Compute totals
- Recipe 6: Session continuity
- Recipe 6b–6d: VCache persistent memory, override, rename/delete
- Recipe 6f: Seed a vcache buffer directly with
vcache.upsert - Recipe 6e: Async session and vcache lifecycle
- Recipe 7: Built-in services with
default_service(alias table) - Recipe 7b: Shop strategy loop
- Recipe 8: Streaming with service progress
- Recipe 9: Caller-managed include services (manual resume)
- Recipe 10: SDK-managed callback runtime
- Recipe 10b: Callback endpoint contract
- Recipe 10c:
callback: falsemode - Recipe 11: Local schema.py references
- Recipe 12: Messaging (email + telegram)
- Recipe 13–13c: Email templates (inbound, outbound, shape lists, images)
- Recipe 15: Policy and guardrail
- Recipe 16: Async streaming
- Recipe 17: Parallel deputies with
use_deputy
- Response Shapes
- Error Handling
- Practical Patterns
- Troubleshooting
- Versioning and Compatibility
Install
pip install nineth
export NINETH_API_KEY="your-api-key"
For Node.js 18 or newer:
npm install nineth
The Python and npm distributions use the same package name and version. Python uses snake_case request arguments; JavaScript uses camelCase and maps them to the same HTTP payload.
Quick Start
1) Basic synchronous request
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request("Give me a concise BTC market brief.")
print(response["final_response"])
Typical response shape:
{
"final_response": "BTC is range-bound with ...",
"iterations": 2,
"usage": {
"prompt_tokens": 1200,
"completion_tokens": 310,
"total_tokens": 1510
},
"service_calls": [],
"service_responses": [],
"events": []
}
2) Basic asynchronous request
import asyncio
from nineth import AsyncNinethClient
async def main() -> None:
async with AsyncNinethClient(default_model="1984-m3-0424") as client:
response = await client.model.request("Summarize crude oil in 5 bullets.")
print(response["final_response"])
asyncio.run(main())
3) Streaming request
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
for event in client.model.request("Analyze ETH setup.", stream=True):
if event["type"] == "model_delta":
print(event["data"]["text"], end="", flush=True)
elif event["type"] == "result":
print("\n---")
print(event["data"]["final_response"])
Stream event types you should handle:
acceptedmodel_deltaservice_callservice_responseawaiting_client_services(manual callback mode)resulterror
JavaScript and TypeScript
The npm package is a zero-dependency ESM client with bundled TypeScript declarations. It supports buffered requests, SSE streaming, automatic session and vcache continuity, vcache lifecycle methods, callback payloads, messaging configuration, and built-in service selection.
import { NinethClient } from "nineth";
const client = new NinethClient({
apiKey: process.env.NINETH_API_KEY,
defaultModel: "1984-m3-0424",
});
const result = await client.model.request("Give me a concise BTC market brief.", {
session: true,
vcache: { name: "market-brief" },
defaultService: ["search_news", "data_get_current_price"],
});
console.log(result.final_response);
Streaming returns an async iterable:
for await (const event of client.model.request("Research ETH liquidity.", {
stream: true,
defaultService: ["search_web", "read"],
})) {
if (event.type === "model_delta") {
process.stdout.write(event.data?.text ?? "");
}
}
The first session response supplies the process and vcache identifiers. Reuse the same
client and vcache name and the npm client sends those identifiers automatically. Use
client.vcache.delete(...), rename(...), and upsert(...) for explicit lifecycle
operations. The npm-specific README is in npm/nineth/README.md.
Public Surface
Most applications only need:
NinethClientAsyncNinethClientclient.health()client.model.request(...)client.vcache.delete(...)client.vcache.rename(...)client.vcache.upsert(...)
AVAILABLE_MODELS is exported for convenience.
Client Construction
import httpx
from nineth import NinethClient
client = NinethClient(
base_url="https://weirdpablo--rooster-api.modal.run",
api_key="...",
default_model="1984-m3-0424",
timeout=httpx.Timeout(300.0, connect=10.0),
stream_timeout=httpx.Timeout(connect=10.0, read=None, write=60.0, pool=60.0),
headers={"X-Caller": "research-worker-1"},
)
Environment fallbacks:
NINETH_API_KEYNINETH_BASE_URLNINETH_DEFAULT_MODEL(orNINETH_MODEL)
Provider Notes
Provider routing happens server-side. All model names — including 1984-*, amari-*, and openrouter/... slugs — are passed directly as the model argument. No special handling is required in the SDK; the server resolves the provider from the model string. See the server README.md for the full routing matrix and code examples.
Model Catalog
Current SDK AVAILABLE_MODELS:
1984-m0-brute1984-m0-sm1984-m1-unified1984-m2-light1984-m2-preview1984-m3-03171984-m3-04041984-m3-04211984-m3-04241984-m3-05031984-m3-05051984-m3-05071984-m3-06141984-c0-04271984-c1-05031984-c1-05051984-c1-05071984-c1-06141984-c1-miniamari-0524
Note:
AVAILABLE_MODELSis the SDK's baked-in convenience list.- Server deployments can expose additional raw provider names or future aliases that remain valid when passed as
model=ordefault_model=. - Gemma 4 aliases use Google's native function-call convention, including
<|tool_call>call:name{...}<tool_call|>. Rooster normalizes this automatically; SDK callers do not parse these tokens.
Request Arguments (Complete)
client.model.request(...) accepts:
| Parameter | Type / Values | Description |
|---|---|---|
| Task identity | ||
task_input |
str |
Prompt / task text. Required. |
model |
str |
Model name. Optional if default_model was set on the client. |
| Generation controls | ||
reasoning |
disabled | low | medium | high |
Reasoning effort level. |
show_reasoning |
bool |
Include model reasoning in the response. |
temperature |
float |
Sampling temperature. |
top_p |
float |
Nucleus sampling probability. |
min_p |
float |
Minimum probability filter. |
top_k |
int |
Top-K filter. |
repetition_penalty |
float |
Repetition penalty. |
presence_penalty |
float |
Presence penalty. |
frequency_penalty |
float |
Frequency penalty. |
seed |
int |
Seed for deterministic sampling. |
| Loop controls | ||
max_iterations |
int |
Cap on agentic loop iterations. |
continuous |
bool |
Run the loop continuously until a stop signal. |
| Inputs | ||
images |
list[str] |
Base64-encoded image strings. |
audio |
list[str | dict] |
Base64 strings or {data, mime_type?, filename?} objects. |
| Runtime controls | ||
policy |
str |
Caller-supplied runtime policy text appended to the model system prompt. |
guardrail |
str |
ADAM extension text for the default SDK/API path. |
base_system |
bool |
Legacy provider-path compatibility control. |
| Memory continuity | ||
session |
bool |
Keep using the active hot conversational session for the same memory scope. |
vcache |
dict |
Caller-owned persistent memory scope rooted at /knowledge/sdk/{name}/{cache_id}. |
vcache.name |
str |
Scope name; required when vcache is provided. |
vcache.cache_id |
str |
Optional; omitted IDs are generated server-side and returned in the response. Later explicit IDs override a previously remembered generated ID for the same vcache.name on that client instance. |
| Service controls | ||
default_service |
False | True | list[str] |
Disable all services, enable all, or provide an explicit allowlist. |
include_service |
list | dict |
Caller-managed services, or callback object via callback: true / callback: false / callback: "https://...". |
client_service_results |
list[dict] |
Manual callback resume payloads. |
callback_url |
str |
Shared callback URL inherited by managed include-service flows and email templates that omit their own URL. |
use_deputy |
bool |
Enable the deputy service for this request, allowing the model to spin up parallel analysis deputies. Default False. Set to True only when the task is complex enough to benefit from parallel analysis; each deputy consumes additional tokens. |
| Output controls | ||
stream |
bool |
Enable SSE streaming. |
response_format |
text | json |
Output format. |
compute |
bool |
Include simplified total token consumption in the response. |
verbose |
bool |
Verbose output (legacy alias: debug). |
system_prompt |
str |
Legacy alias for policy; policy wins when both are supplied. |
debug |
bool |
Legacy alias that enables verbose. |
| Messaging transport | ||
messaging.email |
dict |
Email delivery configuration. |
messaging.email.use_cache |
bool |
When true, the request's vcache is attached to this mailbox so the inbound email processor uses it for session memory. |
messaging.telegram |
dict |
Telegram delivery configuration. |
messaging.telegram.use_cache |
bool |
When true, the request's vcache is attached to this bot so the inbound Telegram processor uses it for session memory. |
messaging.*.use_cache=True requires vcache. The resolved binding is owned by the authenticating API key; another key cannot take over the same mailbox, bot binding, process id, context id, or vcache identity.
Payload Mapping Reference
_build_payload(...) in the SDK maps request arguments into the API payload with the following rules.
Core fields always emitted:
task_inputmodelmax_iterationsshow_reasoningcontinuous(continuousarg or derived asmax_iterations > 10)sessionbase_system(legacy compatibility)default_service(boolean or expanded/merged service list)use_deputy(always emitted;Falseby default)verbose
Conditionally emitted fields:
reasoning->reasoning_efforttemperature->temperaturetop_p->top_pmin_p->min_ptop_k->top_krepetition_penalty->repetition_penaltypresence_penalty->presence_penaltyfrequency_penalty->frequency_penaltyseed->seedinclude_service->include_service(deduplicated list/object form;callback: falseis preserved)client_service_results->client_service_resultsimages->imagesaudio->audiopolicy->policyguardrail->guardrailmessaging-> normalizedmessagingcallback_url-> normalizedcallback_urlresponse_format="json"->response_format: "json"compute=True->compute: truevcache->vcache(name-only requests are allowed; the server fillscache_idwhen omitted)- remembered SDK session id ->
process_id(internal resume wire field whensession=True)
Vcache continuity rules:
- if the first request uses
vcache={"name": "workspace"}, the server returns a generatedcache_id - the SDK remembers that generated id per
vcache.namefor later name-only requests on the same client instance - a later explicit
vcache={"name": "workspace", "cache_id": "chosen-id"}takes precedence over the remembered generated id - once that explicit request succeeds, the SDK remembers the explicit
cache_idfor subsequent name-only requests on the same client instance
Validation rule:
client_service_resultsrequiressession=Trueand an already established session for that client + vcache scope.
Service auto-enable rule:
- Email messaging can auto-enable
send_email/send_replywhen outbound behavior is needed. - Telegram messaging auto-enables plain and Bot API 10.1 rich send/edit services.
- When
vcacheis set, memory management services are automatically added todefault_serviceso the model has persistent memory access without the caller needing to enumerate service names. This covers journal (read/write/search), knowledge archive (search_knowledge,memory_read), scratch (ephemeral working notes), documents, and media. The auto-enable follows the same merge rules as messaging: ifdefault_servicewasFalse, it becomes an explicit allowlist; if it was already a list, the memory service names are merged in without duplicates; if it wasTrue(all services), it staysTrue. - When
use_deputy=True,deputyis merged into the effectivedefault_servicelist using the same rules. Whenuse_deputy=False(default), anydeputyentry is stripped from the list even if provided explicitly.
Callback propagation rule:
- Top-level
callback_urlis normalized and propagated into email templates (inbound or outbound) missingurl. - If
include_serviceis list-form andcallback_urlexists, payload is promoted to object-form with callback + schema. - If
include_service.callbackisfalse, the SDK preserves that flag in the emitted object even when a globalcallback_urlis present. - If
include_service.callbackistrue, the SDK treats it as a mode flag and uses the top-levelcallback_url.
VCache Lifecycle
Use the client.vcache namespace when you want to manage a provisioned durable memory scope directly instead of only referencing it from client.model.request(...).
Delete a vcache instance:
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
client.model.request(
"Remember that this workspace only covers power markets.",
session=True,
vcache={"name": "research-team"},
)
deleted = client.vcache.delete(name="research-team")
print(deleted["found"])
Rename a vcache instance while keeping the same cache_id:
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
client.model.request(
"Remember that this workspace only covers power markets.",
session=True,
vcache={"name": "research-team"},
)
renamed = client.vcache.rename(name="research-team", new_name="power-team")
print(renamed["vcache"])
Lifecycle rules:
- all three lifecycle methods require an API key, like normal model requests
- all three methods accept
cache_id=explicitly; when it is omitted, the SDK tries to reuse the rememberedcache_idfor thatname client.vcache.delete(...)removes the whole/knowledge/sdk/{name}/{cache_id}/treeclient.vcache.rename(...)moves the same durable memory tree to/knowledge/sdk/{new_name}/{cache_id}/client.vcache.upsert(...)appends state-entry dicts to the vcache buffer without affecting the durable memory tree structure- every lifecycle call is authorized against the API key that first claimed the vcache identity; cross-key access returns
403 - delete, rename, and upsert outcomes are recorded in the server's
vcache_actionsaudit table under that API key name - successful
deleteclears the client's rememberedcache_idand session id for that vcache scope - successful
renametransfers the client's rememberedcache_idand session id from the old name to the new name upsertdoes not mutate the client's remembered state
Callback Lifecycle Matrix
| Surface | Callback source | Runtime events | Purpose |
|---|---|---|---|
include_service (managed mode) |
include_service.callback.url or global callback_url |
service_call, interlude, final_response |
Execute caller-owned tools and request missing service parameters. |
| inbound email templates | template url or global callback_url |
inbound_email_received (pre-model), inbound_email_interlude, inbound_email_model_response (post-model) |
Acknowledge inbound payloads, request template variables, and publish final model output trace. |
| outbound email templates | template url or global callback_url fallback (messaging.email.callback_url) |
outbound_email_interlude when model requests template variables |
Request caller-owned variables before template rendering/sending. |
Interlude payloads include:
template_nametemplate_type(inboundoroutbound)required_fieldsknown_parametersreceivedandresend_emailcontext when available
Callback responses for model-resume surfaces may also include:
status: caller-defined state such asok,warn,failed, or an application-specific valuemessage: caller-authored guidance that is surfaced back to the model on the resumed turn
The same response envelope is recognized across all three callback URL surfaces:
include_service.callback.url- inbound email template
url - top-level
callback_urlwhen the SDK or API inherits it intoinclude_serviceor email templates
Model-visible behavior:
include_serviceservice_callandinterludecallbacks preservestatusandmessageinto the resumedclient_service_results- inbound email
inbound_email_receivedpre-model callbacks preservestatusandmessageinto the email runtime context before the model replies - email template interludes (
inbound_email_interlude,outbound_email_interlude) preservestatusandmessagealongside returned parameters - post-model callbacks such as
final_responseandinbound_email_model_responseaccept the same envelope, but any returnedstatusandmessageare recorded only for observability because the model turn is already complete
Full End-to-End Request (All Parameters)
This section shows the same request at three levels:
- the SDK call your application writes
- the JSON payload the SDK sends to
/model - the buffered response shape you should expect back
Complete SDK call
from nineth import NinethClient
weather_schema = {
"name": "get_weather",
"description": "Resolve weather by city",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"units": {"type": "string"},
},
"required": ["location"],
},
}
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
task_input="Draft an outbound response after checking weather and inbox context.",
model="1984-m3-0424",
reasoning="medium",
show_reasoning=False,
temperature=0.5,
top_p=0.9,
min_p=0.05,
top_k=40,
repetition_penalty=1.05,
presence_penalty=0.1,
frequency_penalty=0.1,
seed=7,
max_iterations=8,
continuous=False,
images=["<base64-image>"],
audio=[{"data": "<base64-audio>", "mime_type": "audio/mpeg", "filename": "brief.mp3"}],
policy="Use concise ops language.",
guardrail="Never reveal credentials.",
session=True,
vcache={"name": "ops-desk", "cache_id": "alice"},
base_system=True,
default_service=["browser", "read"],
include_service={"callback": True, "schema": [weather_schema]},
client_service_results=[
{
"call_id": "client_1_1",
"service_name": "get_weather",
"success": True,
"result": {"location": "Nairobi", "forecast": "Partly cloudy"},
}
],
callback_url="https://app.example.com/unified-callback",
stream=False,
response_format="json",
compute=True,
verbose=True,
messaging={
"email": {
"address": "helpdesk@example.com",
"name": "Helpdesk Bot",
"instruction": "Classify ticket urgency.",
"templates": [
{
"type": "inbound",
"shape": [
{
"name": "ticket_reply_primary",
"subject": "Ticket {{ticket_id}} Update",
"html": "<p>{{summary}}</p>",
},
{
"name": "ticket_reply_escalation",
"subject": "Escalation {{ticket_id}}",
"html": "<p>{{action_required}}</p>",
},
],
},
{
"type": "outbound",
"name": "ticket_outbound_notice",
"recipients": ["owner@example.com"],
"shape": [
{
"name": "ticket_notice_primary",
"subject": "Notice {{ticket_id}}",
"html": "<p>{{body}}</p>",
}
],
},
],
},
"telegram": {
"botId": "ops-bot",
"chatId": "12345",
},
},
)
Representative emitted HTTP payload
{
"task_input": "Draft an outbound response after checking weather and inbox context.",
"model": "1984-m3-0424",
"reasoning_effort": "medium",
"show_reasoning": false,
"temperature": 0.5,
"top_p": 0.9,
"min_p": 0.05,
"top_k": 40,
"repetition_penalty": 1.05,
"presence_penalty": 0.1,
"frequency_penalty": 0.1,
"seed": 7,
"max_iterations": 8,
"continuous": false,
"images": ["<base64-image>"],
"audio": [
{
"data": "<base64-audio>",
"mime_type": "audio/mpeg",
"filename": "brief.mp3"
}
],
"policy": "Use concise ops language.",
"guardrail": "Never reveal credentials.",
"session": true,
"vcache": {"name": "ops-desk", "cache_id": "alice"},
"base_system": true,
"default_service": ["search_web", "read_web"],
"include_service": {
"callback": {"url": "https://app.example.com/unified-callback"},
"schema": [
{
"name": "get_weather",
"description": "Resolve weather by city",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"units": {"type": "string"}
},
"required": ["location"]
}
}
]
},
"client_service_results": [
{
"call_id": "client_1_1",
"service_name": "get_weather",
"success": true,
"result": {"location": "Nairobi", "forecast": "Partly cloudy"}
}
],
"callback_url": "https://app.example.com/unified-callback",
"response_format": "json",
"compute": true,
"verbose": true,
"messaging": {
"email": {
"address": "helpdesk@example.com",
"name": "Helpdesk Bot",
"instruction": "Classify ticket urgency.",
"templates": [
{
"type": "inbound",
"shape": [
{
"name": "ticket_reply_primary",
"subject": "Ticket {{ticket_id}} Update",
"html": "<p>{{summary}}</p>"
},
{
"name": "ticket_reply_escalation",
"subject": "Escalation {{ticket_id}}",
"html": "<p>{{action_required}}</p>"
}
]
},
{
"type": "outbound",
"name": "ticket_outbound_notice",
"recipients": ["owner@example.com"],
"shape": [
{
"name": "ticket_notice_primary",
"subject": "Notice {{ticket_id}}",
"html": "<p>{{body}}</p>"
}
]
}
]
},
"telegram": {
"botId": "ops-bot",
"chatId": "12345"
}
}
}
Important request notes:
- on a first request you may omit
vcache.cache_id; the server will generate one UUID and return it inresponse["vcache"] - on later
session=Truerequests the SDK may injectprocess_idautomatically behind the scenes to resume the active hot session for that same vcache scope - if you later send an explicit
vcache.cache_id, that explicit id wins over any previously remembered generated id and becomes the remembered id for future name-only calls on that client
Exhaustive buffered response shape
{
"final_response": {
"summary": "Ticket classified and customer updated",
"risk": "low"
},
"raw_response": "{\"summary\":\"Ticket classified and customer updated\",\"risk\":\"low\"}",
"usage": {
"prompt_tokens": 2140,
"completion_tokens": 490,
"total_tokens": 2630
},
"compute": 2630,
"thinking": [
"Checked inbound context.",
"Resolved tool output.",
"Drafted outbound reply."
],
"service_calls": [
{
"service_name": "get_weather",
"call_id": "client_1_1",
"params": {"location": "Nairobi"}
}
],
"service_responses": [
{
"service_name": "get_weather",
"success": true,
"result": {"location": "Nairobi", "forecast": "Partly cloudy"}
}
],
"artifacts": [
{
"type": "email_template",
"name": "ticket_outbound_notice",
"status": "configured"
}
],
"iterations": 3,
"events": [
{
"type": "mailbox_configured",
"data": {
"address": "helpdesk@example.com",
"inbound_uuid": "inb_abc123"
}
},
{
"type": "service_response",
"data": {
"service_name": "send_reply",
"success": true
}
}
],
"vcache": {"name": "ops-desk", "cache_id": "alice"},
"session_id": "proc_ops_desk_alice_01"
}
Response field notes:
final_responseis the caller-facing result; inresponse_format="json"mode it is parsed into a dict/list when possibleraw_responseis present only when JSON mode preserved the original raw string alongside parsed outputusagealways reports token accounting when the server provides itcomputeis present only whencompute=Truethinkingis present only when reasoning output is surfacedservice_callsandservice_responsesare ordered traces of executed toolsartifactscontains durable side effects the runtime chose to exposeeventsis the broader execution trace, including messaging and callback milestonesvcacheis present when a durable vcache scope is activesession_idis present whensession=Trueis active; it is the caller-facing alias of the underlying resumeprocess_id
Expected callback payload families for the same request:
| Phase | Event type | Key fields |
|---|---|---|
| include-service execution | service_call |
service_name, params, available_services, process_id |
| include-service missing params | interlude |
service_name, required_fields, known_parameters, reason |
| include-service final | final_response |
final_response, process_id |
| inbound pre-model | inbound_email_received |
phase=pre_model, received, resend_email, available_templates |
| template variable fetch | inbound_email_interlude / outbound_email_interlude |
template_name, template_type, required_fields, known_parameters |
| inbound post-model | inbound_email_model_response |
phase=post_model, model_response, service_calls, service_responses |
Cookbook
Recipe 1: Health check
from nineth import NinethClient
with NinethClient() as client:
print(client.health())
Notes:
client.health()is API-key protected like other runtime endpoints.- If authentication is missing/invalid, the server returns auth errors rather than a public health payload.
- Server schema/docs endpoints are typically hidden in production unless the server is started with
ROOSTER_EXPOSE_OPENAPI=true.
Typical response:
{"status": "ok", "timestamp": "2026-05-24T00:00:00+00:00"}
Recipe 2: Per-request model override
with NinethClient(default_model="1984-m2-preview") as client:
a = client.model.request("fast summary")
b = client.model.request("deeper review", model="1984-m3-0424")
Recipe 3: Reasoning and sampling
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Build a scenario tree for BTC next week.",
reasoning="high",
temperature=0.4,
top_p=0.9,
seed=7,
)
Recipe 4: Request JSON output
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Return a JSON object with keys trend, risk, levels.",
response_format="json",
)
print(type(response["final_response"])) # dict if JSON parse succeeded
print(response.get("raw_response")) # original string is preserved
Typical JSON-mode response:
{
"final_response": {
"trend": "neutral",
"risk": "medium",
"levels": ["68000", "70000"]
},
"raw_response": "{\"trend\":\"neutral\",...}",
"iterations": 1
}
Recipe 5: Compute totals
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request("Explain carry trade risk.", compute=True)
print(response.get("compute"))
Recipe 6: Session continuity (session)
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
first = client.model.request("Remember: my risk budget is medium.", session=True)
second = client.model.request(
"What risk budget did I set?",
session=True,
)
print(second["final_response"])
Important rule:
- keep using the same
NinethClientinstance when you wantsession=Trueto auto-reuse the latest session id.
Recipe 6b: Persistent memory partitions with vcache
Use when you need durable memory that survives session resets, namespaced under a caller-controlled scope name. The server creates a persistent directory tree at /knowledge/sdk/{name}/{cache_id}/ that accumulates across sessions.
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
r1 = client.model.request(
"Remember that this workspace tracks only energy equities.",
session=True,
vcache={"name": "research-team"},
)
resolved_vcache = r1["vcache"]
r2 = client.model.request(
"What domain did I say this workspace tracks?",
session=True,
vcache={"name": "research-team"},
)
print(resolved_vcache["cache_id"])
print(r1["session_id"])
print(r2["final_response"])
vcache behavior:
- creates/uses
/knowledge/sdk/{name}/{cache_id}/...on the server - omitting
cache_idon the first request creates one UUID and returns it asresponse["vcache"]["cache_id"] - subsequent requests from the same
NinethClientmay keep passing only{"name": ...}; the client reuses the resolvedcache_id - a later explicit
{"name": ..., "cache_id": ...}overrides the previously remembered generated id for that samename - the filesystem scope persists independently of individual sessions
session=Truereuses only the hot conversational session for that vcache scope- starting a new session in the same vcache scope clears hot conversational state while keeping durable memory artifacts in that vcache path
Recipe 6c: Override a generated cache_id
Use when you want to replace the server-generated cache_id with a deterministic caller-supplied ID mid-conversation — for example, to pin a workspace to a known identifier after initial discovery.
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
first = client.model.request(
"Remember that this workspace is for North Sea gas only.",
session=True,
vcache={"name": "energy-desk"},
)
second = client.model.request(
"Keep using the same durable workspace, but pin my own id now.",
session=True,
vcache={"name": "energy-desk", "cache_id": "desk-alpha"},
)
third = client.model.request(
"Which market did I say this desk covers?",
session=True,
vcache={"name": "energy-desk"},
)
print(first["vcache"])
print(second["vcache"])
print(third["vcache"])
Expected behavior:
- the first response returns a server-generated
cache_id - the second request's explicit
cache_id="desk-alpha"overrides that generated id for this client instance - the third name-only request reuses
desk-alpha
Recipe 6d: Rename or delete a durable vcache
Use to rename a vcache scope (e.g. after project handoff) or to fully tear down the durable memory tree when it is no longer needed.
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
client.model.request(
"Remember that this workspace covers only LNG shipping.",
session=True,
vcache={"name": "shipping-desk"},
)
renamed = client.vcache.rename(name="shipping-desk", new_name="lng-desk")
print(renamed["vcache"])
deleted = client.vcache.delete(name="lng-desk")
print(deleted["found"])
Operational semantics:
renamekeeps the samecache_idand moves the durable files to the newnamedeleteremoves the full provisioned memory tree for thatname+cache_id- after a successful lifecycle operation, the SDK updates or clears its remembered local state for that vcache scope
Recipe 6f: Seed a vcache buffer directly with vcache.upsert
Use when you want to pre-populate a vcache with state entries before running a model request, or to append entries at any time without going through a model turn.
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
# First request to provision the vcache and remember its cache_id
client.model.request(
"Start tracking this research session.",
session=True,
vcache={"name": "research-desk"},
)
# Directly write state entries into the vcache buffer
result = client.vcache.upsert(
name="research-desk",
data=[
{
"id": "00000000-0000-0000-0000-000000000001",
"statetag": "task_input",
"content": {"text": "Background: LNG shipping market context."},
"importance": 0.8,
"timestamp": "2025-01-01T00:00:00+00:00",
},
{
"id": "00000000-0000-0000-0000-000000000002",
"statetag": "memory_summary",
"content": {"text": "User is researching LNG tanker rates."},
"importance": 0.9,
"timestamp": "2025-01-01T00:00:01+00:00",
},
],
)
print(result["written"]) # 2
upsert rules:
- appends entries to the vcache buffer file; does not overwrite existing content
- requires a known
cache_id(either remembered from a prior request or provided explicitly viacache_id=) - each entry in
datamust be a dict; non-dict items are silently skipped - valid
statetagvalues:task_input,model_output,service_call,service_response,interrupt,memory_summary - does not update the SDK's local session or vcache id state
Recipe 6e: Async session and vcache lifecycle
Drop-in async version of the full vcache lifecycle (Recipes 6b–6d). Use in async-native applications or when all model operations must be non-blocking.
All vcache and model operations work identically in async mode:
import asyncio
from nineth import AsyncNinethClient
async def main() -> None:
async with AsyncNinethClient(default_model="1984-m3-0424") as client:
# Initial request — server generates cache_id
r1 = await client.model.request(
"Remember that this workspace covers only energy equities.",
session=True,
vcache={"name": "async-research"},
)
print(r1["vcache"]["cache_id"]) # server-generated UUID
print(r1["session_id"])
# Continue same session
r2 = await client.model.request(
"What domain did I say this workspace covers?",
session=True,
vcache={"name": "async-research"},
)
print(r2["final_response"])
# Rename the durable scope
renamed = await client.vcache.rename(
name="async-research",
new_name="async-energy",
)
print(renamed["vcache"])
# Delete the durable scope
deleted = await client.vcache.delete(name="async-energy")
print(deleted["found"])
asyncio.run(main())
Recipe 7: Built-in services with default_service
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Search web and summarize today's semiconductor headlines.",
default_service=["browser", "read"],
)
Notes:
default_service=Trueenables all built-ins.default_service=Falsedisables built-ins.- List mode accepts group aliases such as
browser,knowledge,computer,workspace,voice,trading,shop. "search"is a recognized alias for"browser"— both expand to the same set.- Alias expansion is automatic, and duplicate service names are deduplicated.
- Individual service names (e.g.
"search_web","voice_generate") can be listed directly alongside aliases.
Complete default_service alias expansion reference
| Alias | Expanded service names |
|---|---|
browser (or search) |
search_web, search_news, search_discussions, search_unified, search_context, search_places, search_local_pois, search_poi_descriptions, search_rich, search_videos, search_images, search_answers, read, deepsearch |
knowledge |
list_documents, read_document_metadata, search_documents, read_document, read_document_markdown, search_knowledge, journal_read, journal_list, journal_search, memory_read, media_list, media_recall, media_search, media_read_manifest |
computer |
create_sandbox, sandbox_status, run, destroy_sandbox, sandbox, volume_write_file, volume_read_file, volume_search_replace, volume_list, create_scratch, write_scratch, read_scratch, list_scratches, search_scratches, delete_scratch, index_document, delete_document, journal_write, journal_search_replace, journal_delete, media_write_manifest, media_write_transcript, media_update, media_decompress, media_delete, set_alarm, schedule_at, get_current_time, cancel_alarm, set_plan, get_plan, update_plan, clear_plan |
workspace |
gh_clone, gh_new, gh_run, gh_commit, gh_push, gh_pull, gh_branch, gh_status, gh_pr, gh_list |
voice |
voice_list, voice_generate, voice_transcribe |
trading |
fund_balances, data_get_current_price, data_get_historical_ohlc, data_get_market_buffer, data_get_live_ticks, data_get_available_symbols, portfolio_list, portfolio_add, portfolio_update, portfolio_remove, performance, initialize_client, get_terminal_status, get_account_snapshot, get_open_positions, get_pending_orders, get_closed_orders, get_available_symbols, get_current_price, get_historical_ohlc, get_live_ticks, get_market_buffer, get_available_symbols, trade_market_buy, trade_market_sell, trade_buy_limit, trade_sell_limit, trade_buy_stop, trade_sell_stop, trade_buy_stop_limit, trade_sell_stop_limit, trade_modify_position, trade_close_position_partial, trade_close_position_full, trade_close_position_by_opposite, trade_cancel_order |
shop |
shop_apply, shop_read, shop_patch, shop_delete, shop_list, shop_status, shop_observe, shop_watch, shop_stop, shop_start, shop_restart, shop_scaffold, shop_glossary |
Mix aliases and individual names freely — the SDK deduplicates:
# aliases expand then single names are appended without repeating
response = client.model.request(
"...",
default_service=["browser", "search_web", "voice", "journal_write"],
# search_web already in browser group — silently skipped
# journal_write added explicitly after computer group would also include it
)
Recipe 7b: Shop strategy loop
shop lets the model author and iterate Python or Rust trading strategies against the server-side nursery wells. Python .py strategies hot-load under /shop/strategies/python; Rust .rs strategies compile under /shop/strategies/rust. SDK callers enable the service surface; the model performs the plan -> write/patch -> observe -> watch loop through built-in service calls.
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0614") as client:
response = client.model.request(
"""
Build a conservative paper-trading momentum strategy for EURUSD.
Plan first, inspect the shop glossary, write a Python on_tick strategy
or scaffold Rust if needed, run a sandbox check if useful, call shop_observe, and attach
shop_watch if the strategy is ready for long-running monitoring.
Optimize for risk-adjusted return, not raw PnL.
""",
session=True,
continuous=True,
vcache={"name": "trading", "cache_id": "eurusd-momentum"},
default_service=["shop", "computer", "knowledge"],
max_iterations=20,
)
print(response["final_response"])
Operational notes:
- The API deployment owns Shop execution. SDK callers do not deploy Modal functions directly.
- Server operators deploy
entry-stub.py; it contains both the API and the state-awareshop(mode="continuous" | "once" | "compile")Modal function. Continuous mode hot-discovers Python strategies and compiles/runs Rust strategies when present. shop_observeis the compact feedback surface. Healthy state ismode="ack"; failures aremode="alert"with issue details.shop_watchbinds the current continuous worker to runner alerts so the model wakes only when the runner has a real issue.- Use
stream=Truefor long-running SDK calls when you want to display service progress events while the model plans and patches strategy code.
Recipe 8: Streaming with service progress
with NinethClient(default_model="1984-m3-0424") as client:
for event in client.model.request(
"Research AI gateway patterns and summarize.",
stream=True,
default_service=["browser", "read", "deepsearch"],
):
if event["type"] == "model_delta":
print(event["data"]["text"], end="")
elif event["type"] == "service_call":
print("\n[service]", event["data"]["service_name"])
elif event["type"] == "service_response":
print("\n[service done]", event["data"].get("service_name"))
Recipe 9: Caller-managed include services (manual resume)
Use when your caller code must execute the tool calls itself. The model issues call parameters; you run the tool, supply the results, and resume the conversation — no callback server required.
weather_schema = {
"name": "get_weather",
"description": "Return weather for a city.",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
"additionalProperties": False,
},
}
with NinethClient(default_model="1984-m3-0424") as client:
first = client.model.request(
"Get weather for Lagos and summarize.",
include_service=[weather_schema],
session=True,
)
if first.get("status") == "awaiting_client_services":
pending = first["pending_client_calls"]
# Execute pending client tools yourself.
manual_results = [
{
"call_id": pending[0]["call_id"],
"service_name": "get_weather",
"success": True,
"result": {"location": "Lagos", "forecast": "sunny"},
}
]
final = client.model.request(
"continue",
include_service=[weather_schema],
client_service_results=manual_results,
session=True,
)
Recipe 10: SDK-managed callback runtime (include_service object)
Use when you want the server to forward tool-call requests to your HTTP callback endpoint. The model executes tool schemas, and results are returned asynchronously to your callback URL rather than inline.
weather_schema = {
"name": "get_weather",
"description": "Return weather for a city.",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
}
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Get weather for Nairobi and summarize risk impact.",
include_service={
"callback": {"url": "https://app.example.com/api/callback"},
"schema": [weather_schema],
},
)
Recipe 10c: Caller-managed include-service payload (callback: false)
Use when you want to register tool schemas but route all callback traffic yourself via a global callback_url, bypassing the SDK-managed interlude.
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Use the tool schema, but let the caller handle the calls.",
include_service={
"callback": False,
"schema": [weather_schema],
},
callback_url="https://app.example.com/global-callback",
)
In this mode the SDK preserves callback: false in the emitted object and does
not add the managed callback interlude schema.
Callback runtime events sent to your callback URL include:
service_callinterlude(missing caller-side parameters)final_response
Recipe 10b: Callback endpoint contract (request/response)
Reference when implementing the HTTP handler that receives tool-call requests from the server. Documents the request envelope, expected response shape, and all lifecycle event types.
When include_service callback mode is active, your callback endpoint receives
JSON requests with idempotency metadata:
{
"event_type": "service_call",
"listener": "nineth_include_service_callback",
"idempotency_key": "<sha1>",
"process_id": "proc_abc123",
"call_id": "call_1",
"service_name": "get_weather",
"params": {"location": "Nairobi"},
"service": {
"name": "get_weather",
"description": "Return weather for a city.",
"parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}
},
"available_services": [{"name": "get_weather", "description": "..."}]
}
Your callback should return HTTP 200 with a JSON object.
Recommended callback response envelope for any callback that can influence the next model turn:
{
"status": "warn",
"message": "Applicant is already blocked; reply with appeal instructions.",
"success": true
}
Successful service response example:
{
"status": "ok",
"message": "Weather lookup completed from the caller-side cache.",
"success": true,
"result": {
"location": "Nairobi",
"forecast": "Partly cloudy",
"temperature_c": 22
}
}
Interlude response example (ask caller for missing fields):
{
"status": "warn",
"message": "Applicant is already blocked; use the appeal template.",
"success": true,
"parameters": {
"location": "Nairobi",
"units": "metric"
}
}
Failure response example:
{
"status": "failed",
"message": "Rate limit from upstream weather provider.",
"success": false,
"error": "Rate limit from upstream weather provider"
}
Notes:
- The SDK includes
X-Idempotency-Keyon callback HTTP requests. - Callback responses must be JSON objects; non-object JSON is treated as a callback error.
statusandmessageare recognized the same way whether the callback URL was set directly oninclude_service, directly on an inbound template, or inherited from top-levelcallback_url.- The reserved interlude service name is
request_include_service_interlude.
Recipe 11: Local schema.py include-service references
include_service supports legacy local references:
- absolute or relative
schema.pypath - directory containing
schema.py - shorthand token discoverable from local
services/**/schema.py - manager class/object references from loaded schema modules
Example:
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Run local weather service.",
include_service=["./services/weather/schema.py"],
)
Recipe 12: Messaging (email + telegram)
Use when the model should trigger real email or Telegram deliveries as a side effect of the request. Messaging config auto-enables the relevant transport service names.
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Draft and send the update.",
messaging={
"email": {
"address": "ops@example.com",
"name": "Ops Bot",
"instruction": "Reply with concise operational summaries.",
},
"telegram": {
"botId": "bot-1",
"chatId": "12345",
},
},
)
Auto-enable behavior:
- Telegram messaging config auto-enables Telegram delivery service names, including
send_rich_messageandedit_rich_message. - Email messaging auto-enables email send services except inbound-only setup flows.
generate_receiptcreates a compressed knowledge-media PNG. A Telegram worker such as Lati then callssend_photowith the returnedsend_with.arguments; receipt generation does not choose credentials or recipients itself.
For structured Telegram text, the model can call send_rich_message with an InputRichMessage:
{
"rich_message": {
"markdown": "## Deployment status\n\n- API: healthy\n- Worker: healthy\n- Queue: 3 pending"
}
}
The server sends this through Telegram's sendRichMessage method. Rich Markdown supports headings, lists, tables, quotations, details blocks, footnotes, formulas, and HTTP(S) media blocks. Private inbound Telegram sessions also stream temporary rich drafts while the model works; the final response is still sent as a persistent message.
Inbound Telegram work is durably handed from the webhook to a separate Modal worker invocation after the update is written under /knowledge. set_alarm and schedule_at persist deadlines in the same context; a deployment-time wallclock tick claims due alarms and resumes the worker. Callers should treat alarms as durable, non-blocking checkpoints, not as dynamically created cron jobs or guarantees that one HTTP request/container remains alive for the delay.
Recipe 12b: Messaging payload and result events
Example request emphasizing template payloads:
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Handle inbound request and reply.",
callback_url="https://app.example.com/mailbox-hook",
messaging={
"email": {
"address": "support@example.com",
"name": "Support Bot",
"instruction": "Classify and route inbound email.",
"templates": [
{
"type": "inbound",
"shape": {
"subject": "string",
"html": "<p>{{body}}</p>",
"text": "string",
"from": "string"
}
},
{
"type": "outbound",
"recipients": ["customer@example.com"],
"message": {
"subject": "Ticket update",
"body": "Issue resolved.",
"html": "<p>Issue resolved.</p>"
}
}
]
},
"telegram": {
"botId": "ops-bot",
"chatId": "12345"
}
},
)
Typical transport-side effect event fragments inside events:
[
{
"type": "mailbox_configured",
"data": {
"address": "support@example.com",
"inbound_uuid": "inb_abc123"
}
},
{
"type": "service_response",
"data": {
"service_name": "send_reply",
"success": true
}
}
]
Inbound UUID behavior:
- The SDK caches
mailbox_configuredinbound IDs by sender address per client instance. - Subsequent requests can automatically reuse the remembered
inbound_uuidfor the same address.
Recipe 13: Inbound/outbound email templates with global callback URL
Use when you need to define reusable inbound and outbound email template pairs and have all deliveries inherit a shared top-level callback URL.
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Configure mailbox templates.",
callback_url="https://app.example.com/mailbox-hook",
messaging={
"email": {
"address": "helpdesk@example.com",
"name": "Helpdesk Bot",
"templates": [
{
"type": "inbound",
"shape": {
"subject": "string",
"html": "<p>{{body}}</p>",
"text": "string",
"from": "string"
},
# url omitted -> inherits top-level callback_url
},
{
"type": "outbound",
"recipients": ["customer@example.com"],
"messages": [
{
"subject": "We received your request",
"body": "Thanks, we are on it.",
"html": "<p>Thanks, we are on it.</p>"
},
{
"subject": "Follow-up",
"body": "We will update you again soon."
}
}
},
],
}
},
)
Recipe 13b: Multiple named Resend templates per type via shape list
Use when you need distinct named template variants per direction — for example, separate triage and escalation shapes for inbound, or multiple outbound notice templates — all provisioned in a single request.
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Register reusable inbound/outbound template packs.",
callback_url="https://app.example.com/unified-callback",
messaging={
"email": {
"address": "helpdesk@example.com",
"templates": [
{
"type": "inbound",
"shape": [
{
"name": "inbound_triage_primary",
"subject": "Ticket {{ticket_id}}",
"html": "<p>{{summary}}</p>"
},
{
"name": "inbound_triage_escalation",
"subject": "Escalation {{ticket_id}}",
"html": "<p>{{action_required}}</p>"
}
]
},
{
"type": "outbound",
"name": "outbound_pack",
"recipients": ["owner@example.com"],
"shape": [
{
"name": "outbound_notice_primary",
"subject": "Notice {{ticket_id}}",
"html": "<p>{{body}}</p>"
}
]
}
]
}
},
)
Runtime behavior for shape lists:
- each shape entry must define its own
name - each entry is provisioned/cached as its own Resend template ID
- later requests can reuse provisioned IDs by template name without re-creating templates
Recipe 13c: Complete email template shape — all design parameters
Full parameter reference for the email template shape contract. Use this when building or debugging a complex Resend template — covers subject, HTML, fallback variable defaults, and image injection.
The full shape contract exposes five caller-controlled parameters:
| Parameter | Required | Description |
|---|---|---|
name |
no | Internal Resend template name. Used for caching and idempotency. |
subject |
yes | Email subject line. May contain {{{VAR_NAME}}} handlebars placeholders. |
html |
yes | HTML body of the email. May contain {{{VAR_NAME}}} handlebars placeholders. |
fallback |
no | Dict (or path to a .json file) mapping variable names to default values. Resend uses these when the model does not supply a value for a given placeholder at send time, preventing validation errors. |
images |
no | List of image objects injected as <img> tags into the template HTML at creation time. Each entry: {"url": "…", "alt"?: "…", "width"?: N, "height"?: N, "position"?: "append"|"prepend"}. Local file paths are resolved to base64 data URIs by the SDK before the payload is sent; remote URLs (http://, https://) and data URIs are passed through unchanged. |
Variables are extracted automatically from {{{TRIPLE_BRACE}}} patterns in subject, html, and text. If a fallback dict is provided, each matched variable name is looked up in it; any match is registered as Resend's fallbackValue for that variable.
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Send an order confirmation email.",
callback_url="https://app.example.com/hook",
messaging={
"email": {
"address": "orders@example.com",
"name": "Order Bot",
"templates": [
{
"type": "outbound",
"recipients": ["customer@example.com"],
"shape": {
# 1. Template name
"name": "order-confirmation",
# 2. Subject with handlebars placeholder
"subject": "Your order for {{{PRODUCT}}} is confirmed!",
# 3. HTML body — the main visual design
"html": (
"<h1>Thanks for your order!</h1>"
"<p>Item: <strong>{{{PRODUCT}}}</strong></p>"
"<p>Total: <strong>{{{PRICE}}}</strong></p>"
"<p>Expected delivery: {{{DELIVERY_DATE}}}</p>"
),
# 4. Fallback values — prevent send-time errors when the
# model omits a variable. Can be an inline dict or a
# path to a local .json file (resolved by the SDK before
# the payload is sent to the server):
"fallback": {
"PRODUCT": "your item",
"PRICE": "N/A",
"DELIVERY_DATE": "TBD",
},
# — or equivalently —
# "fallback": "./templates/order_fallbacks.json",
},
}
],
}
},
)
Fallback as a JSON file — when fallback is a string the SDK treats it as a local filesystem path, reads it, and inlines the parsed object before the payload is transmitted:
// ./templates/order_fallbacks.json
{
"PRODUCT": "your item",
"PRICE": "N/A",
"DELIVERY_DATE": "TBD"
}
"shape": {
"name": "order-confirmation",
"subject": "Your order for {{{PRODUCT}}} is confirmed!",
"html": "<p>Item: {{{PRODUCT}}}</p><p>Total: {{{PRICE}}}</p>",
"fallback": "./templates/order_fallbacks.json", # resolved at call time
}
Precedence rules for fallback values:
- An explicit
fallbackValue(orfallback_value) on an item in thevariableslist wins unconditionally. - If no per-variable explicit value exists, the
fallbackdict is consulted by variable key. - If neither source supplies a value, the variable is registered without a fallback — Resend will require the caller to supply it at send time or return a validation error.
Adding images to a template — the images list is processed at template creation time. Each image is injected as an <img> tag directly into the HTML before the template is registered with Resend. Use "position": "prepend" to place the image at the top of the body; the default is "append" (bottom of the body).
"shape": {
"name": "order-confirmation",
"subject": "Your order for {{{PRODUCT}}} is confirmed!",
"html": (
"<h1>Thanks for your order!</h1>"
"<p>Item: <strong>{{{PRODUCT}}}</strong></p>"
),
"fallback": {"PRODUCT": "your item"},
# Images are injected into the HTML at template-creation time.
# Remote URLs are used as-is; local paths are base64-encoded by the SDK.
"images": [
# Brand logo from a CDN — prepended at the top of the email body
{
"url": "https://cdn.example.com/logo.png",
"alt": "Acme Corp",
"width": 160,
"height": 40,
"position": "prepend",
},
# A local banner image — the SDK reads the file and converts it
# to a base64 data URI before sending the payload to the server
{
"url": "./assets/order-banner.png",
"alt": "Order confirmed",
"width": 600,
},
],
}
Each image entry supports the following fields:
| Field | Required | Description |
|---|---|---|
url |
yes | Remote URL (https://…), data URI (data:image/…;base64,…), or local file path. Local paths are resolved to base64 data URIs by the SDK. |
alt |
no | alt attribute value for accessibility. |
width |
no | width attribute in pixels. |
height |
no | height attribute in pixels. |
position |
no | "append" (default) — inserted before </body> or at the end of the HTML. "prepend" — inserted after <body…> or at the start of the HTML. |
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Transcribe and summarize this call.",
audio=[
{
"data": "<base64-audio>",
"mime_type": "audio/mpeg",
"filename": "call.mp3",
}
],
)
Recipe 15: Policy and guardrail
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Assess this strategy.",
policy="Keep output in bullet points with risk-first framing.",
guardrail="Refuse prohibited trading instructions.",
)
Interpretation:
policyis caller runtime instruction overlay.guardrailaugments ADAM in the default SDK/API ingress path.
Recipe 16: Async streaming
Use in async-native applications that need to consume a streaming model response token-by-token without blocking the event loop.
import asyncio
from nineth import AsyncNinethClient
async def main() -> None:
async with AsyncNinethClient(default_model="1984-m3-0424") as client:
stream = await client.model.request(
"Stream a quick macro brief.",
stream=True,
)
async for event in stream:
if event["type"] == "model_delta":
print(event["data"]["text"], end="")
asyncio.run(main())
Recipe 17: Parallel deputies with use_deputy
Use when the task is complex enough that independent subtasks can be parallelised. The model governs the team and synthesises outputs into one final answer.
Set use_deputy=True to allow the model to call the deputy service. It is False by default to prevent unexpected token spend on simple tasks. When enabled, each deputy is an ephemeral nested worker with its own nonpersistent in-memory context across iterations and only services granted from the governor's request-scoped allowlist.
from nineth import NinethClient
with NinethClient(default_model="1984-m3-0424") as client:
response = client.model.request(
"Analyse this acquisition target: strengths, risks, and market context.",
use_deputy=True,
default_service=["search_web"],
reasoning="medium",
max_iterations=8,
)
print(response["final_response"])
When the governor model decides the task benefits from parallel work, it emits a deputy service call such as:
{
"total_deputies": 3,
"policy": "Cite specific evidence and flag uncertainty.",
"deputies": [
{
"id": "strengths",
"role": "strategic researcher",
"task": "Identify the top 3 durable competitive advantages of the target company.",
"temperature": 0.3,
"max_tokens": 1200
},
{
"id": "risks",
"role": "risk analyst",
"task": "Identify the top 3 material downside risks, including regulatory and market risks.",
"temperature": 0.2,
"max_tokens": 1200
},
{
"id": "context",
"role": "market analyst",
"task": "Provide current market context for the target's industry segment.",
"model": "1984-m2-light",
"temperature": 0.4,
"max_tokens": 800,
"reasoning_effort": "low"
}
]
}
All three deputies run concurrently. The governor receives a single observation with per-deputy outputs, success flags, merged token usage, and any service call/response traces, then synthesises the findings into its final response.
Key constraints:
- Deputies have no cross-deputy shared memory, no ability to contact the human, and cannot create further deputies.
- Each batch receives fresh deputy context IDs; no deputy hot-memory file is written or reused by a later batch.
taskis the objective.policyis a separate behavioral overlay inherited on top of the fixeddeputyPolicy.pybase policy.- Deputy service requests outside the governor's allowlist are rejected, and outbound messaging services are always removed.
total_deputiesmust exactly match the number of entries indeputies.- Total
max_tokensacross all deputies is capped by the server'sM_SERIES_DEPUTY_TOKEN_BUDGET(default 20 000). - If
use_deputy=Falseand the model attempts adeputycall, the call is rejected with an error observation; the model can still continue with other services. - You can narrow a deputy's service surface with
allowed_services, and you can override the per-deputy iteration cap withmax_iterationsorcontinuous. - An explicit
allowed_services=[]gives that deputy no task services; only the internaldonelifecycle service remains available.
When to enable use_deputy:
| Task complexity | Recommended setting |
|---|---|
| Simple Q&A, summaries, formatting | use_deputy=False (default) |
| Multi-angle research or independent sub-questions | use_deputy=True |
| Fact-checking + risk + recommendation in one step | use_deputy=True |
Response Shapes
Buffered response
Common caller-facing shape:
{
"final_response": "... or parsed JSON value ...",
"raw_response": "... optional original JSON string ...",
"usage": {
"prompt_tokens": 123,
"completion_tokens": 45,
"total_tokens": 168
},
"compute": 168,
"thinking": ["... optional reasoning trace ..."],
"service_calls": [{"service_name": "search_web", "call_id": "call_12"}],
"service_responses": [{"service_name": "search_web", "success": true}],
"artifacts": [{"type": "email_template", "name": "ticket_notice_primary"}],
"iterations": 3,
"events": [{"type": "service_response", "data": {"service_name": "search_web", "success": true}}],
"vcache": {"name": "ops-desk", "cache_id": "alice"},
"session_id": "proc_ops_desk_alice_01"
}
Field guide:
final_responseis always presentraw_responseis conditional and mainly appears when JSON mode preserves the original stringusageis the token ledger returned by the servercomputeis conditional oncompute=Truethinkingis conditional on reasoning visibilityservice_calls,service_responses,artifacts, andeventsare trace arrays and may be emptyvcacheis present when the request used a durable vcache scopesession_idis present whensession=Trueis active
Callback wait response (manual mode)
When waiting for caller-managed services, the SDK returns a paused response instead of a final model answer:
{
"status": "awaiting_client_services",
"session_id": "proc_123",
"vcache": {"name": "ops-desk", "cache_id": "alice"},
"pending_client_calls": [
{
"call_id": "call_1",
"service_name": "get_weather",
"params": {"location": "Lagos"}
}
],
"thinking": [],
"service_calls": [],
"service_responses": [],
"artifacts": [],
"events": []
}
Notes:
- if
session=False, the paused response exposesprocess_idinstead ofsession_id - resume by sending
client_service_resultswith the same client and same session/vcache scope
Stream accepted event
{
"type": "accepted",
"data": {}
}
Stream result event
{
"type": "result",
"process_id": "proc_ops_desk_alice_01",
"session_id": "proc_ops_desk_alice_01",
"data": {
"final_response": "...",
"iterations": 3,
"usage": {"prompt_tokens": 123, "completion_tokens": 45, "total_tokens": 168},
"vcache": {"name": "ops-desk", "cache_id": "alice"}
}
}
Stream service events
Service execution surfaces in stream mode as readable progress plus structured events:
{
"type": "model_delta",
"data": {
"text": "\n> Browsing the web\n",
"progress": true,
"synthetic": true
}
}
{
"type": "service_call",
"session_id": "proc_ops_desk_alice_01",
"data": {
"service_name": "search_web",
"client_managed": false,
"call_id": "call_12"
}
}
{
"type": "service_response",
"session_id": "proc_ops_desk_alice_01",
"data": {
"service_name": "search_web",
"success": true
}
}
Stream awaiting_client_services event
Emitted in stream mode when the model has paused to wait for caller-managed service results (manual include_service mode):
{
"type": "awaiting_client_services",
"session_id": "proc_abc123",
"data": {
"status": "awaiting_client_services",
"pending_client_calls": [
{
"call_id": "call_1",
"service_name": "get_weather",
"params": {"location": "Lagos"}
}
]
}
}
After receiving this event, collect results and resume with client_service_results using the same client and session scope (see Recipe 9).
VCache lifecycle responses
client.vcache.delete(...) returns:
{
"success": true,
"found": true,
"vcache": {"name": "ops-desk", "cache_id": "alice"},
"context_id": "sdk/ops-desk/alice",
"deleted_path": "/knowledge/sdk/ops-desk/alice",
"message": "vcache deleted."
}
client.vcache.rename(...) returns:
{
"success": true,
"previous_vcache": {"name": "ops-desk", "cache_id": "alice"},
"vcache": {"name": "ops-archive", "cache_id": "alice"},
"previous_context_id": "sdk/ops-desk/alice",
"context_id": "sdk/ops-archive/alice",
"moved_from": "/knowledge/sdk/ops-desk/alice",
"moved_to": "/knowledge/sdk/ops-archive/alice",
"message": "vcache renamed."
}
client.vcache.upsert(...) returns:
{
"success": true,
"vcache": {"name": "ops-desk", "cache_id": "alice"},
"context_id": "sdk/ops-desk/alice",
"written": 2,
"message": "wrote 2 state entries to vcache buffer."
}
Error Handling
SDK raises NinethAPIError for API/server failures.
from nineth import NinethClient, NinethAPIError
with NinethClient(default_model="1984-m3-0424") as client:
try:
client.model.request("test")
except NinethAPIError as exc:
print("request failed:", exc)
Authentication missing raises ValueError before request dispatch.
Practical Patterns
- Create one long-lived client per worker process to maximize HTTP connection reuse.
- Use
stream_timeoutwithread=Nonefor long-running SSE sessions. - Use
response_format="json"only when your prompt explicitly asks for strict JSON. - Prefer
default_service=[...]over broadTruein production to keep service scope tight. - For include-service workflows, choose one mode per integration:
- SDK-managed callback URL mode for autonomous orchestration.
- caller-managed mode when you need full deterministic control.
Troubleshooting
ValueError: Authentication required: setNINETH_API_KEYor passapi_key=.401/403 on health endpoint:/healthis protected; verifyNINETH_API_KEY(or explicitapi_key=) matches server-side key registry.ValueError: A model is required: set clientdefault_modelor passmodel=per request.client_service_results requires session=True: setsession=Trueand reuse the same client (and vcache scope, if provided) before sending callback results.cache_id is required unless this client has already remembered one for that vcache name.: pass an explicitcache_idtoclient.vcache.delete()/client.vcache.rename(), or make sure you have already made a successfulmodel.request()call with thatvcache.nameon the same client instance (which causes the SDK to remember the resolved id).- callback responses not progressing: verify callback endpoint returns HTTP 200 JSON object.
- stalled stream with include services: confirm pending calls are resumed via
client_service_results(manual mode) or callback endpoint handling (managed mode). 404 on /openapi.json or /docs: expected in hardened deployments. Ask operators to enableROOSTER_EXPOSE_OPENAPI=trueonly for controlled internal debugging.- deputy call rejected with
"Deputy execution is disabled": the request was sent withoutuse_deputy=True; add it when the task warrants parallel analysis. "total_deputies must exactly match"error in deputy observation: the governor emitted a mismatched count; hint the model to settotal_deputiesequal to the array length.- high token usage on deputy tasks: tune
max_tokensper deputy or reducetotal_deputiesto stay withinM_SERIES_DEPUTY_TOKEN_BUDGET. - shop appears stopped or stale: ask the model to call
shop_statusorshop_observe; server operators can trigger one cycle withmodal run entry-stub.py --mode onceor a build-only check withmodal run entry-stub.py --mode compile.
Versioning and Compatibility
- Public SDK API is centered on
NinethClient,AsyncNinethClient,AVAILABLE_MODELS, andNinethAPIError. - Legacy aliases (
system_prompt,debug,services,service_names) remain compatibility surfaces but should be considered migration paths, not preferred new usage.
Maintainer Link
For server architecture and internal operations, see README.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nineth-0.10.3.tar.gz.
File metadata
- Download URL: nineth-0.10.3.tar.gz
- Upload date:
- Size: 73.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1467ba6c654ea485945960149e5a32be10a9d5b1f6e6f9ea2e016cd53c31a85
|
|
| MD5 |
8720f7686b6e261a8daf64eaa117bc8a
|
|
| BLAKE2b-256 |
025b7f390355c69ac9eb4e40690938b7a45616424fdd4b3037c8bd19ae00725b
|
Provenance
The following attestation bundles were made for nineth-0.10.3.tar.gz:
Publisher:
build.yml on districtt/rooster
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nineth-0.10.3.tar.gz -
Subject digest:
e1467ba6c654ea485945960149e5a32be10a9d5b1f6e6f9ea2e016cd53c31a85 - Sigstore transparency entry: 1891872326
- Sigstore integration time:
-
Permalink:
districtt/rooster@883154a671e55944d5c99bca208319ef6381f800 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/districtt
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@883154a671e55944d5c99bca208319ef6381f800 -
Trigger Event:
push
-
Statement type:
File details
Details for the file nineth-0.10.3-py3-none-any.whl.
File metadata
- Download URL: nineth-0.10.3-py3-none-any.whl
- Upload date:
- Size: 51.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9d9a13520cfc5b68c815d5fa0872210eb07eb2ef860236b6a7ab32545abb2c1
|
|
| MD5 |
0a1b6b0ee689e50498f1b9ca6fe376ee
|
|
| BLAKE2b-256 |
dc8be00235fd7c188f718c015ee3775d53e9d0165363a06541c8ce3b6660f089
|
Provenance
The following attestation bundles were made for nineth-0.10.3-py3-none-any.whl:
Publisher:
build.yml on districtt/rooster
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nineth-0.10.3-py3-none-any.whl -
Subject digest:
b9d9a13520cfc5b68c815d5fa0872210eb07eb2ef860236b6a7ab32545abb2c1 - Sigstore transparency entry: 1891872394
- Sigstore integration time:
-
Permalink:
districtt/rooster@883154a671e55944d5c99bca208319ef6381f800 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/districtt
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@883154a671e55944d5c99bca208319ef6381f800 -
Trigger Event:
push
-
Statement type: