Skip to main content

model sdk built by the 9th ditrict at tooig

Project description

nineth

nineth is the public Python 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

pip install nineth
export NINETH_API_KEY="your-api-key"

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:

  • accepted
  • model_delta
  • service_call
  • service_response
  • awaiting_client_services (manual callback mode)
  • result
  • error

Public Surface

Most applications only need:

  • NinethClient
  • AsyncNinethClient
  • client.health()
  • client.model.request(...)
  • client.vcache.delete(...)
  • client.vcache.rename(...)

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_KEY
  • NINETH_BASE_URL
  • NINETH_DEFAULT_MODEL (or NINETH_MODEL)

Provider Notes

The SDK talks to the Rooster API endpoint. Provider routing happens server-side.

Common server provider modes:

  • Together-backed models (slash-form provider model names)
  • Ollama/cloud models (:cloud / -cloud conventions)
  • OpenRouter models (openrouter/<slug> or latest aliases like ~openai/gpt-latest)
  • OpenRouter-tagged provider names ending with :free (for example google/gemma-4-26b-a4b-it:free)
  • public aliases in the amari-* family

If your server is configured for OpenRouter, ensure OPENROUTER_API exists in the server runtime.

Example SDK request that targets an OpenRouter model slug:

from nineth import NinethClient

with NinethClient(default_model="openrouter/openai/gpt-4o") as client:
    response = client.model.request("Summarize this incident report in 5 bullets.")
    print(response["final_response"])

Example SDK request using a server alias that resolves to OpenRouter:

from nineth import NinethClient

with NinethClient(default_model="amari-0524") as client:
    response = client.model.request("Summarize this incident report in 5 bullets.")
    print(response["final_response"])

Model Catalog

Current SDK AVAILABLE_MODELS:

  • 1984-m0-brute
  • 1984-m0-sm
  • 1984-m1-unified
  • 1984-m2-light
  • 1984-m2-preview
  • 1984-m3-0317
  • 1984-m3-0404
  • 1984-m3-0421
  • 1984-m3-0424
  • 1984-c0-0427

Note:

  • AVAILABLE_MODELS is the SDK's baked-in convenience list.
  • Server deployments can expose additional aliases (for example 1984-c1-mini or amari-0524) that are still valid when passed as model= or default_model=.

Request Arguments (Complete)

client.model.request(...) supports:

Task identity

  • task_input (required)
  • model (optional if client default exists)

Generation controls

  • reasoning: disabled|low|medium|high
  • show_reasoning: include model reasoning output
  • temperature, top_p, min_p, top_k
  • repetition_penalty, presence_penalty, frequency_penalty
  • seed

Loop controls

  • max_iterations
  • continuous

Inputs

  • images: list of base64 strings
  • audio: list of base64 strings or objects {data, mime_type?, filename?}

Runtime controls

  • policy: caller runtime policy text
  • guardrail: ADAM extension text
  • base_system: legacy provider-path compatibility control

Memory continuity

  • session: keep using the active hot conversational session for the same memory scope
  • vcache: caller-owned persistent memory scope rooted at /knowledge/sdk/{name}/{cache_id}
  • vcache.cache_id: optional on first request; omitted ids are generated server-side and returned in the response
  • later explicit vcache.cache_id values override a previously remembered generated id for the same vcache.name on that client instance

Service controls

  • default_service: False, True, or allowlist list
  • include_service: caller-managed list, or callback object via callback: true / callback: false / callback: "https://..."
  • client_service_results: manual callback resume payloads
  • callback_url: global callback URL

Output controls

  • stream
  • response_format: text|json
  • compute
  • verbose (legacy alias: debug)

Messaging transport

  • messaging.email
  • messaging.telegram

Payload Mapping Reference

_build_payload(...) in the SDK maps request arguments into the API payload with the following rules.

Core fields always emitted:

  • task_input
  • model
  • max_iterations
  • show_reasoning
  • continuous (continuous arg or derived as max_iterations > 10)
  • session
  • base_system (legacy compatibility)
  • default_service (boolean or expanded/merged service list)
  • verbose

Conditionally emitted fields:

  • reasoning -> reasoning_effort
  • temperature -> temperature
  • top_p -> top_p
  • min_p -> min_p
  • top_k -> top_k
  • repetition_penalty -> repetition_penalty
  • presence_penalty -> presence_penalty
  • frequency_penalty -> frequency_penalty
  • seed -> seed
  • include_service -> include_service (deduplicated list/object form; callback: false is preserved)
  • client_service_results -> client_service_results
  • images -> images
  • audio -> audio
  • policy -> policy
  • guardrail -> guardrail
  • messaging -> normalized messaging
  • callback_url -> normalized callback_url
  • response_format="json" -> response_format: "json"
  • compute=True -> compute: true
  • vcache -> vcache (name-only requests are allowed; the server fills cache_id when omitted)
  • remembered SDK session id -> process_id (internal resume wire field when session=True)

Vcache continuity rules:

  • if the first request uses vcache={"name": "workspace"}, the server returns a generated cache_id
  • the SDK remembers that generated id per vcache.name for 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_id for subsequent name-only requests on the same client instance

Validation rule:

  • client_service_results requires session=True and an already established session for that client + vcache scope.

Service auto-enable rule:

  • Email messaging can auto-enable send_email/send_reply when outbound behavior is needed.
  • Telegram messaging auto-enables Telegram send/edit services.

Callback propagation rule:

  • Top-level callback_url is normalized and propagated into email templates (inbound or outbound) missing url.
  • If include_service is list-form and callback_url exists, payload is promoted to object-form with callback + schema.
  • If include_service.callback is false, the SDK preserves that flag in the emitted object even when a global callback_url is present.
  • If include_service.callback is true, the SDK treats it as a mode flag and uses the top-level callback_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:

  • both lifecycle methods require an API key, like normal model requests
  • both methods accept cache_id= explicitly; when it is omitted, the SDK tries to reuse the remembered cache_id for that name
  • client.vcache.delete(...) removes the whole /knowledge/sdk/{name}/{cache_id}/ tree
  • client.vcache.rename(...) moves the same durable memory tree to /knowledge/sdk/{new_name}/{cache_id}/
  • successful delete clears the client's remembered cache_id and session id for that vcache scope
  • successful rename transfers the client's remembered cache_id and session id from the old name to the new name

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_name
  • template_type (inbound or outbound)
  • required_fields
  • known_parameters
  • received and resend_email context when available

Callback responses for model-resume surfaces may also include:

  • status: caller-defined state such as ok, warn, failed, or an application-specific value
  • message: 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:

  1. include_service.callback.url
  2. inbound email template url
  3. top-level callback_url when the SDK or API inherits it into include_service or email templates

Model-visible behavior:

  • include_service service_call and interlude callbacks preserve status and message into the resumed client_service_results
  • inbound email inbound_email_received pre-model callbacks preserve status and message into the email runtime context before the model replies
  • email template interludes (inbound_email_interlude, outbound_email_interlude) preserve status and message alongside returned parameters
  • post-model callbacks such as final_response and inbound_email_model_response accept the same envelope, but any returned status and message are 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 in response["vcache"]
  • on later session=True requests the SDK may inject process_id automatically 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_response is the caller-facing result; in response_format="json" mode it is parsed into a dict/list when possible
  • raw_response is present only when JSON mode preserved the original raw string alongside parsed output
  • usage always reports token accounting when the server provides it
  • compute is present only when compute=True
  • thinking is present only when reasoning output is surfaced
  • service_calls and service_responses are ordered traces of executed tools
  • artifacts contains durable side effects the runtime chose to expose
  • events is the broader execution trace, including messaging and callback milestones
  • vcache is present when a durable vcache scope is active
  • session_id is present when session=True is active; it is the caller-facing alias of the underlying resume process_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 NinethClient instance when you want session=True to auto-reuse the latest session id.

Recipe 6b: Persistent memory partitions with vcache

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_id on the first request creates one UUID and returns it as response["vcache"]["cache_id"]
  • subsequent requests from the same NinethClient may keep passing only {"name": ...}; the client reuses the resolved cache_id
  • a later explicit {"name": ..., "cache_id": ...} overrides the previously remembered generated id for that same name
  • the filesystem scope persists independently of individual sessions
  • session=True reuses 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

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

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:

  • rename keeps the same cache_id and moves the durable files to the new name
  • delete removes the full provisioned memory tree for that name + cache_id
  • after a successful lifecycle operation, the SDK updates or clears its remembered local state for that vcache scope

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=True enables all built-ins.
  • default_service=False disables built-ins.
  • List mode accepts group aliases such as browser, knowledge, computer, workspace, voice, trading, shop.
  • Alias expansion is automatic, and duplicate service names are deduplicated.

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)

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)

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)

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_call
  • interlude (missing caller-side parameters)
  • final_response

Recipe 10b: Callback endpoint contract (request/response)

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-Key on callback HTTP requests.
  • Callback responses must be JSON objects; non-object JSON is treated as a callback error.
  • status and message are recognized the same way whether the callback URL was set directly on include_service, directly on an inbound template, or inherited from top-level callback_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.py path
  • 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)

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.
  • Email messaging auto-enables email send services except inbound-only setup flows.

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_configured inbound IDs by sender address per client instance.
  • Subsequent requests can automatically reuse the remembered inbound_uuid for the same address.

Recipe 13: Inbound/outbound email templates with global 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

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

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:

  1. An explicit fallbackValue (or fallback_value) on an item in the variables list wins unconditionally.
  2. If no per-variable explicit value exists, the fallback dict is consulted by variable key.
  3. 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:

  • policy is caller runtime instruction overlay.
  • guardrail augments ADAM in the default SDK/API ingress path.

Recipe 16: Async streaming

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())

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_response is always present
  • raw_response is conditional and mainly appears when JSON mode preserves the original string
  • usage is the token ledger returned by the server
  • compute is conditional on compute=True
  • thinking is conditional on reasoning visibility
  • service_calls, service_responses, artifacts, and events are trace arrays and may be empty
  • vcache is present when the request used a durable vcache scope
  • session_id is present when session=True is 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 exposes process_id instead of session_id
  • resume by sending client_service_results with 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
    }
}

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."
}

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_timeout with read=None for long-running SSE sessions.
  • Use response_format="json" only when your prompt explicitly asks for strict JSON.
  • Prefer default_service=[...] over broad True in 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: set NINETH_API_KEY or pass api_key=.
  • 401/403 on health endpoint: /health is protected; verify NINETH_API_KEY (or explicit api_key=) matches server-side key registry.
  • ValueError: A model is required: set client default_model or pass model= per request.
  • client_service_results requires session=True: set session=True and reuse the same client (and vcache scope, if provided) before sending callback results.
  • 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 enable ROOSTER_EXPOSE_OPENAPI=true only for controlled internal debugging.

Versioning and Compatibility

  • Public SDK API is centered on NinethClient, AsyncNinethClient, AVAILABLE_MODELS, and NinethAPIError.
  • 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

nineth-0.7.9.tar.gz (55.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nineth-0.7.9-py3-none-any.whl (42.8 kB view details)

Uploaded Python 3

File details

Details for the file nineth-0.7.9.tar.gz.

File metadata

  • Download URL: nineth-0.7.9.tar.gz
  • Upload date:
  • Size: 55.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nineth-0.7.9.tar.gz
Algorithm Hash digest
SHA256 95c3845ca874aa98732543067ad50f4b279691567ee4427c98f1becbe0e3e58f
MD5 7e82c204f18dc8b882bb34dfc321ce30
BLAKE2b-256 f33741746305265c2015aa2862809e7dc31a91137466a39344bf937d30493acd

See more details on using hashes here.

File details

Details for the file nineth-0.7.9-py3-none-any.whl.

File metadata

  • Download URL: nineth-0.7.9-py3-none-any.whl
  • Upload date:
  • Size: 42.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nineth-0.7.9-py3-none-any.whl
Algorithm Hash digest
SHA256 0273ac3807ae3a3fb99315eec915f05608096ef5037f672883ffd4cd104b9b5f
MD5 ed7d4d1e05e6cc90d90472affb9961ea
BLAKE2b-256 cbcb2f044350fdd2a67f1d21fd36d09687d7b712595348ee4f06e6ab2bfdd1df

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page