Metadata-Version: 2.4
Name: nineth
Version: 0.7.10
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 district's 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)
- [VCache Lifecycle](#vcache-lifecycle)
- [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(...)`
- `client.vcache.delete(...)`
- `client.vcache.rename(...)`

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

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

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

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

### Representative emitted HTTP payload

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

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

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

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

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

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

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

Recommended callback response envelope for any callback that can influence the next model turn:

```json
{
    "status": "warn",
    "message": "Applicant is already blocked; reply with appeal instructions.",
    "success": true
}
```

Successful service response example:

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

```json
{
    "status": "warn",
    "message": "Applicant is already blocked; use the appeal template.",
    "success": true,
    "parameters": {
        "location": "Nairobi",
        "units": "metric"
    }
}
```

Failure response example:

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

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

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

```json
// ./templates/order_fallbacks.json
{
    "PRODUCT": "your item",
    "PRICE": "N/A",
    "DELIVERY_DATE": "TBD"
}
```

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

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



```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 caller-facing shape:

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

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

```json
{
    "type": "accepted",
    "data": {}
}
```

### Stream `result` event

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

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

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

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

### VCache lifecycle responses

`client.vcache.delete(...)` returns:

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

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

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