> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sailresearch.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tinker

> Drive Sail inference from Tinker loops with sail.SailTokenCompleter, and run rollout commands on Sailboxes with sail.TinkerSandbox

The `sail.tinker` helpers let you drive Sail inference from a
[Tinker](https://tinker-docs.thinkingmachines.ai/) RL or training loop. They
bridge Tinker's token-level sampling interface to Sail's raw-token Responses
path, so rollouts run against Sail-hosted models (optionally with a LoRA
adapter) while logprobs flow back into your training code.

<Note>
  These helpers require `tinker-cookbook` installed alongside `sail`.
  Constructing a `SailTokenCompleter` without `tinker-cookbook` available raises
  [`sail.InferenceError`](/voyages-sdk-errors).
</Note>

## sail.SailTokenCompleter

A Tinker `TokenCompleter` backed by Sail's raw-token Responses API: each call
sends the prompt token ids via the `raw_prompt_tokens` request parameter, which
skips server-side chat templating and tokenization and forwards the ids
verbatim to the model. Construct one with a model and sampling settings, then
`await` it on tokenized prompts to get sampled tokens and their logprobs.

```python theme={null}
import asyncio

import sail
from tinker import types


async def main() -> None:
    completer = sail.SailTokenCompleter(
        model="moonshotai/Kimi-K2.6",
        max_tokens=256,
        temperature=0.7,
        completion_window="balanced",
    )

    # Prompt token ids from your tokenizer, wrapped in Tinker's ModelInput.
    model_input = types.ModelInput.from_ints(tokens=[9906, 11, 1917, 0])

    result = await completer(model_input)
    print(result.tokens)         # list[int] of sampled token ids
    print(result.maybe_logprobs) # list[float] | None
    print(result.stop_reason)    # e.g. "stop", "length"


asyncio.run(main())
```

### Constructor

```python theme={null}
SailTokenCompleter(
    *,
    model: str,
    max_tokens: int,
    temperature: float = 1.0,
    top_p: float = 1.0,
    completion_window: str = "balanced",
    lora: str | None = None,
    tinker_lora_signed_url: str | None = None,
    adapter_config: Mapping[str, Any] | str | None = None,
    tinker_lora_name: str | None = None,
    metadata: Mapping[str, str] | None = None,
    timeout: float | None = None,
    poll_timeout: float | None = 1200.0,
    voyage: sail.Voyage | None = None,
    request_logprobs: bool = True,
)
```

| Parameter                | Description                                                                                                                                                                                                                                        |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`                  | **Required.** Model id to sample from. Raises `ValueError` if empty.                                                                                                                                                                               |
| `max_tokens`             | **Required.** Max tokens to generate; must be `> 0` (`ValueError` otherwise).                                                                                                                                                                      |
| `temperature`            | Sampling temperature. Default `1.0`.                                                                                                                                                                                                               |
| `top_p`                  | Nucleus sampling cutoff. Default `1.0`.                                                                                                                                                                                                            |
| `completion_window`      | Completion window for each request. Default `"balanced"`. K2.6 LoRA requests use `balanced` by default; `flex` is available for background work and `priority` remains accepted for existing clients. `"asap"` is not available for LoRA requests. |
| `lora`                   | Name of a Sail-registered LoRA adapter to apply. Mutually exclusive with `tinker_lora_signed_url`.                                                                                                                                                 |
| `tinker_lora_signed_url` | Signed URL to a Tinker checkpoint archive (see [`get_tinker_checkpoint_signed_url_async`](#get-tinker-checkpoint-signed-url-async)). Requires `adapter_config`. Mutually exclusive with `lora`.                                                    |
| `adapter_config`         | LoRA adapter config as a mapping or JSON string. **Required** when `tinker_lora_signed_url` is set.                                                                                                                                                |
| `tinker_lora_name`       | Optional human-readable name attached to the Tinker LoRA.                                                                                                                                                                                          |
| `metadata`               | Extra string metadata forwarded on each request.                                                                                                                                                                                                   |
| `timeout`                | Per-request timeout in seconds.                                                                                                                                                                                                                    |
| `poll_timeout`           | Seconds to wait for a sampled result before giving up. Default `1200` (20 minutes); `None` waits indefinitely.                                                                                                                                     |
| `voyage`                 | A [`sail.Voyage`](/voyages-sdk) to attribute requests to a voyage.                                                                                                                                                                                 |
| `request_logprobs`       | Whether to request logprobs from the server. Default `True`.                                                                                                                                                                                       |

Passing both `lora` and `tinker_lora_signed_url`, or setting
`tinker_lora_signed_url` without `adapter_config`, raises `ValueError`.

### `async __call__(model_input, stop=None)`

```python theme={null}
async def __call__(model_input, stop=None) -> TokensWithLogprobs
```

* `model_input`: must expose a callable `.to_ints()` returning the prompt
  token ids (this is Tinker's `ModelInput`). A non-callable `to_ints`, a
  non-integer token, or an empty prompt raises `TypeError`/`ValueError`.
* `stop`: optional stop condition. An `int` is wrapped as a single-element
  list; a tuple is converted to a list; other values pass through unchanged.

Returns a Tinker `TokensWithLogprobs`:

| Field            | Description                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `tokens`         | `list[int]`: the sampled token ids.                                                                        |
| `maybe_logprobs` | `list[float]` or `None`: per-token logprobs, or `None` when the response carried none.                     |
| `stop_reason`    | The model's stop reason (e.g. `"stop"`, `"length"`), falling back to the response `status` or `"unknown"`. |

If the Sail response is malformed (missing or non-integer token data, or
mismatched token and logprob lengths), a
[`sail.InferenceError`](/voyages-sdk-errors) is raised with the offending
response attached as `exc.response`.

## get\_tinker\_checkpoint\_signed\_url\_async

```python theme={null}
await get_tinker_checkpoint_signed_url_async(
    service_client,
    tinker_path: str,
    *,
    ttl_seconds: int | None = None,
) -> str
```

Resolves a Tinker checkpoint path to a signed archive URL, suitable for passing
as `tinker_lora_signed_url` to `SailTokenCompleter`. It resolves the path against
the Tinker service client and returns the signed URL.

This helper is async-only and requires a Tinker service client with async
checkpoint-URL support.

| Parameter        | Description                                                               |
| ---------------- | ------------------------------------------------------------------------- |
| `service_client` | A Tinker `ServiceClient` (exposes `create_rest_client()`).                |
| `tinker_path`    | The Tinker checkpoint path to resolve.                                    |
| `ttl_seconds`    | Optional checkpoint TTL to set or extend before resolving the signed URL. |

Raises [`sail.InferenceError`](/voyages-sdk-errors) if the Tinker client does
not provide async checkpoint URL methods, or if the response does not contain a
URL.

```python theme={null}
import sail

signed_url = await sail.get_tinker_checkpoint_signed_url_async(
    service_client,
    tinker_path,
    ttl_seconds=3600,
)

completer = sail.SailTokenCompleter(
    model="moonshotai/Kimi-K2.6",
    max_tokens=256,
    completion_window="balanced",
    tinker_lora_signed_url=signed_url,
    adapter_config={"r": 16, "alpha": 32},
)
```

## sail.TinkerSandbox

Runs tinker-cookbook sandbox workloads on a Sailbox. It implements the
cookbook's sandbox interface, so recipes that take a sandbox execute their
rollout commands in an isolated Sailbox instead of on the training machine.
Each instance wraps one Sailbox for its whole life.

```python theme={null}
import sail

sandbox = sail.TinkerSandbox(sailbox, timeout_seconds=1800)

result = await sandbox.run_command("python solve.py", workdir="/task")
print(result.exit_code, result.stdout)

await sandbox.cleanup()  # terminates the Sailbox
```

### Constructor

```python theme={null}
TinkerSandbox(
    sailbox: Sailbox,
    *,
    timeout_seconds: float | None = None,
)
```

| Parameter         | Description                                                                                                                                                                                                                                                                                                             |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sailbox`         | **Required.** The Sailbox the sandbox runs in, normally created by `tinker_sandbox_factory`. The sandbox owns it: `cleanup` and lifetime expiry terminate it. Give each sandbox its own Sailbox; two sandboxes sharing one would share its filesystem and processes, and the first cleanup would terminate it for both. |
| `timeout_seconds` | The sandbox's lifetime budget. Once it has elapsed, the next operation terminates the Sailbox and raises the cookbook's `SandboxTerminatedError`. Default no budget.                                                                                                                                                    |

Requires tinker-cookbook; constructing without it installed raises
`ImportError`.

### `async run_command(command, workdir=None, timeout=60, max_output_bytes=None)`

Runs a shell command and returns the cookbook's `SandboxResult`.

* `command`: the shell command string, run through bash.
* `workdir`: directory to run in.
* `timeout`: seconds before the command is killed; a killed command is
  reported with `metrics["timed_out"]` set.
* `max_output_bytes`: keeps the first bytes of each output stream, the part
  the cookbook's parsers read. Without it the full output is kept, up to a
  large safety ceiling.

A terminated or lost Sailbox raises the cookbook's
`SandboxTerminatedError`; other errors from the Sailbox are reported as a
result with exit code `-1`.

### `async read_file(path, max_bytes=None, timeout=60)`

Reads a file into the result's `stdout`, within `timeout` seconds; without
`max_bytes` the whole file is kept, up to a large safety ceiling. A
missing or unreadable file is reported as a result with exit code `1`
rather than raised, and so is a read that runs out of time.

### `async write_file(path, content, executable=False, timeout=60)`

Writes a file into the Sailbox, within `timeout` seconds; `executable=True`
marks it executable. A write that runs out of time is reported as a result
with exit code `1`.

### `async send_heartbeat(timeout=30)`

Checks the sandbox's lifetime budget. A Sailbox stays alive without
keep-alives, so the heartbeat sends nothing; it only terminates the Sailbox
and raises the cookbook's `SandboxTerminatedError` once `timeout_seconds`
has elapsed. `timeout` is part of the cookbook's heartbeat signature and is
unused here, since there is no request for it to bound.

### `async cleanup()`

Terminates the backing Sailbox. Safe to call more than once.

## tinker\_sandbox\_factory

```python theme={null}
await tinker_sandbox_factory(
    env_dir: str | Path,
    timeout_seconds: float | None = None,
    *,
    app: str | None = None,
    size: str = "s",
    image_ref: str | None = None,
    name_prefix: str = "tinker",
) -> TinkerSandbox
```

Creates a `TinkerSandbox` for a tinker-cookbook environment. Pass this
function, or a `functools.partial` of it to preset the keyword arguments,
wherever the cookbook accepts a sandbox factory. Being a module-level
function, it pickles by reference, so it survives the cookbook's process
boundaries.

| Parameter         | Description                                                                                                                                                                                  |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `env_dir`         | **Required.** The environment directory. Sail uses its parent's `[environment].docker_image` when present; otherwise it builds `env_dir / "Dockerfile"` with `env_dir` as the build context. |
| `timeout_seconds` | The sandbox's lifetime budget, measured from when the sandbox exists.                                                                                                                        |
| `app`             | The [app](/sailbox-sdk-apps) that groups the created Sailboxes. Default `$SAIL_APP` or `"tinker"`, created on first use.                                                                     |
| `size`            | Sailbox size (`"s"`, `"m"`, or `"l"`; see [pricing](/sailboxes-pricing)). Default `"s"`.                                                                                                     |
| `image_ref`       | Registry reference overriding the `task.toml` image. Docker-style short references work (`python:3.11`).                                                                                     |
| `name_prefix`     | Prefix for the Sailbox's name. Default `"tinker"`.                                                                                                                                           |

The registry image or Dockerfile must produce a Debian- or Ubuntu-based filesystem.

Raises `ImportError` without tinker-cookbook installed, and a `ValueError`
when `image_ref`, the `task.toml` image, and the environment's `Dockerfile` are all absent.
