Metadata-Version: 2.4
Name: nineth
Version: 0.7.2
Summary:  model sdk built by the 9th ditrict at tooig
Project-URL: Homepage, https://github.com/districtt/rooster
Project-URL: Bug Tracker, https://github.com/districtt/rooster/issues
Author-email: "Tooig, Inc" <tooighq@gmail.com>, Oyebamijo <boy@oyebamijo.com>
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Requires-Dist: httpx<1.0,>=0.27.2
Description-Content-Type: text/markdown

# nineth

`nineth` is the public Python SDK for the 1984 model API.

This guide is caller-facing and SDK-specific.

If you maintain server internals, use [README.md](../README.md).

## Table of Contents

- [Install](#install)
- [Quick Start](#quick-start)
- [Public Surface](#public-surface)
- [Client Construction](#client-construction)
- [Provider Notes](#provider-notes)
- [Model Catalog](#model-catalog)
- [Request Arguments (Complete)](#request-arguments-complete)
- [Payload Mapping Reference](#payload-mapping-reference)
- [Callback Lifecycle Matrix](#callback-lifecycle-matrix)
- [Full End-to-End Request (All Parameters)](#full-end-to-end-request-all-parameters)
- [Cookbook](#cookbook)
- [Response Shapes](#response-shapes)
- [Error Handling](#error-handling)
- [Practical Patterns](#practical-patterns)
- [Troubleshooting](#troubleshooting)
- [Versioning and Compatibility](#versioning-and-compatibility)

## Install

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

## Quick Start

### 1) Basic synchronous request

```python
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:

```json
{
  "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

```python
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

```python
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(...)`

`AVAILABLE_MODELS` is exported for convenience.

## Client Construction

```python
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:

```python
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:

```python
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 SDK session for the same memory scope
- `vcache`: caller-owned persistent memory scope (`name` + `cache_id`) rooted at `/knowledge/sdk/{name}/{cache_id}`

### 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`
- remembered SDK session id -> `process_id` (internal resume wire field when `session=True`)

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`.

## 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

## Full End-to-End Request (All Parameters)

```python
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",
            },
        },
    )
```

Typical buffered response (abridged):

```json
{
  "final_response": {
    "summary": "Ticket classified and customer updated",
    "risk": "low"
  },
  "raw_response": "{\"summary\":\"...\"}",
  "iterations": 3,
  "usage": {"prompt_tokens": 2140, "completion_tokens": 490, "total_tokens": 2630},
  "compute": 2630,
  "events": [
    {"type": "mailbox_configured", "data": {"address": "helpdesk@example.com", "inbound_uuid": "inb_abc123"}},
    {"type": "service_response", "data": {"service_name": "send_reply", "success": true}}
  ],
    "session_id": "proc_ops_desk_alice_01"
}
```

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

```python
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:

```json
{"status": "ok", "timestamp": "2026-05-24T00:00:00+00:00"}
```

### Recipe 2: Per-request model override

```python
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

```python
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

```python
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:

```json
{
  "final_response": {
    "trend": "neutral",
    "risk": "medium",
    "levels": ["68000", "70000"]
  },
  "raw_response": "{\"trend\":\"neutral\",...}",
  "iterations": 1
}
```

### Recipe 5: Compute totals

```python
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`)

```python
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`

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    scoped = {"name": "research-team", "cache_id": "analyst-007"}

    r1 = client.model.request(
        "Remember that this workspace tracks only energy equities.",
        session=True,
        vcache=scoped,
    )

    r2 = client.model.request(
        "What domain did I say this workspace tracks?",
        session=True,
        vcache=scoped,
    )

    print(r1["session_id"])
    print(r2["final_response"])
```

`vcache` behavior:

- creates/uses `/knowledge/sdk/{name}/{cache_id}/...` on the server
- 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 7: Built-in services with `default_service`

```python
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

```python
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)

```python
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],
    )

    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)

```python
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`)

```python
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:

```json
{
    "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.

Successful service response example:

```json
{
    "success": true,
    "result": {
        "location": "Nairobi",
        "forecast": "Partly cloudy",
        "temperature_c": 22
    }
}
```

Interlude response example (ask caller for missing fields):

```json
{
    "success": true,
    "parameters": {
        "location": "Nairobi",
        "units": "metric"
    }
}
```

Failure response example:

```json
{
    "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.
- 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:

```python
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)

```python
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:

```python
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`:

```json
[
    {
        "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

```python
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

```python
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 14: Audio input

```python
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

```python
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

```python
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 keys:

- `final_response`
- `iterations`
- `usage`
- `thinking` (when enabled)
- `service_calls`
- `service_responses`
- `artifacts`
- `events`
- `compute` (when requested)
- `session_id` (when `session=True` is active)

### Callback wait response (manual mode)

When waiting for caller-managed services:

```json
{
  "status": "awaiting_client_services",
  "process_id": "proc_123",
  "pending_client_calls": [
    {
      "call_id": "call_1",
      "service_name": "get_weather",
      "params": {"location": "Lagos"}
    }
  ]
}
```

### Stream `result` event

```json
{
  "type": "result",
  "data": {
    "final_response": "...",
    "iterations": 3,
    "usage": {...}
  }
}
```

### Stream service events

Service execution surfaces in stream mode as readable progress + structured events:

```json
{
    "type": "model_delta",
    "data": {
        "text": "\n> Browsing the web\n",
        "progress": true,
        "synthetic": true
    }
}
```

```json
{
    "type": "service_call",
    "data": {
        "service_name": "search_web",
        "client_managed": false,
        "call_id": "call_12"
    }
}
```

```json
{
    "type": "service_response",
    "data": {
        "service_name": "search_web",
        "success": true
    }
}
```

## Error Handling

SDK raises `NinethAPIError` for API/server failures.

```python
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](../README.md).
