Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions docs/adding_a_model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
title: Adding a Model
---

# Adding a Model

**You do not need a new Trailblaze release to use a new model.** Model definitions are
config, not code. Add the model to your workspace's `trails/config/trailblaze.yaml` and
Trailblaze picks it up on the next run.

The built-in registry ([Built-in Models](generated/LLM_MODELS.md)) exists so common models
work with zero configuration and correct specs. It is a convenience, not a gate — a model
missing from it is not a model Trailblaze refuses to run. Model families ship faster than
release cycles, so this page is the escape hatch that keeps you off the upgrade treadmill.

## The 30-second version

```yaml
# trails/config/trailblaze.yaml
llm:
providers:
google:
models:
- id: gemma-4-31b-it
context_length: 262144
max_output_tokens: 32768
defaults:
model: gemma-4-31b-it
```

```bash
trailblaze config models # confirm it's listed
trailblaze config llm google/gemma-4-31b-it
```

Set the provider's API key (`GOOGLE_API_KEY` here — see the
[env var table](llm_configuration.md#environment-variables)) and you're running.

## How your entry combines with the built-ins

Workspace config **adds to** the built-in registry — it does not replace it. Entries are
matched by `id`:

| Your `id` | Result |
|---|---|
| Not in the built-in registry | A new model, defined entirely by your entry |
| Already in the built-in registry | Field-by-field merge — the fields you set win, the rest are inherited |

So a workspace entry is equally the way to **add** a model and the way to **pin or correct**
one that ships in the box (stale pricing, a context window your gateway caps lower).

Anything you omit falls back to a safe default rather than failing: `context_length`
defaults to **131072** and `max_output_tokens` to **8192**. Both are conservative — set them
explicitly for any model whose real limits you know, or you'll silently leave context on the
table.

## Filling in the values

| Field | Set it when | Where to find it |
|---|---|---|
| `id` | Always | The exact model string the provider's API expects — copy it from the provider's model list, not from marketing copy |
| `context_length` | Always, in practice | The provider's model card / docs |
| `max_output_tokens` | Always, in practice | Same. If the provider publishes no figure, use the largest any host documents |
| `vision` | Text-only models | Set `false`. Defaults to `true` |
| `cost.*` | You want spend reported accurately | Provider pricing page. Local / free-tier models: `0.0` |
| `temperature` | The model needs a non-default | Provider guidance |
| `screenshot.max_dimensions` | The model has a tighter image limit | Provider image-input docs |

Full field reference: [LLM Configuration → Model fields](llm_configuration.md#model-fields).

**What Trailblaze actually needs from a model.** The agent loop drives a device by looking at
an annotated screenshot and emitting tool calls, so a model wants **image input** and
**function/tool calling** to be useful. A text-only model (`vision: false`) still works for
text-only flows, but it will struggle on anything that needs to read the screen. Smaller
local models frequently accept the tools and then ignore them — try before you commit a
team-wide default.

## Recipes

### A hosted model on a built-in provider

`openai`, `anthropic`, `google`, `ollama`, and `openrouter` already know their endpoint and
auth env var. You only supply the model:

```yaml
llm:
providers:
openrouter:
models:
- id: google/gemma-4-31b-it:free
context_length: 262144
max_output_tokens: 32768
cost:
input_per_million: 0.0
output_per_million: 0.0
```

### A local Ollama model

```yaml
llm:
providers:
ollama:
models:
- id: "gemma4:31b"
context_length: 262144
max_output_tokens: 8192
defaults:
model: "gemma4:31b"
```

Quote Ollama ids — the `:` makes them look like YAML mappings otherwise. Trailblaze also
discovers whatever `ollama list` reports at runtime, so a model already pulled locally shows
up without any config; listing it explicitly is how you tell teammates which model the
project expects. Nothing is auto-downloaded — they run `ollama pull gemma4:31b`.

### A model behind your own gateway

A provider Trailblaze has never heard of is the same amount of work, plus the endpoint:

```yaml
llm:
providers:
acme_gateway:
type: openai_compatible
base_url: "https://ai.acme.example.com/v1"
auth:
env_var: ACME_AI_TOKEN
models:
- id: acme-vision-large
context_length: 200000
max_output_tokens: 32768
defaults:
model: acme-vision-large
```

See [Enterprise gateway](llm_configuration.md#enterprise-gateway) for headers, custom
completion paths, and the on-device story.

### Override a built-in model's specs

Specify only what you're changing:

```yaml
llm:
providers:
openai:
models:
- id: gpt-4.1
max_output_tokens: 16384 # our gateway caps output lower than the default
```

## Where to put the file

| Scope | Path |
|---|---|
| Whole team (commit it) | `<workspace>/trails/config/trailblaze.yaml` |
| Just you | `~/.trailblaze/trailblaze.yaml` |

The workspace file wins over the user file; environment variables win over both. Full
precedence table: [Configuration → Precedence](configuration.md#precedence).

Committing the workspace file is the recommended shape for teams — everyone who clones the
repo gets a working model with no per-machine setup, and the project is pinned to models you
have actually validated rather than to whatever the current release happens to ship.

## Verifying

```bash
trailblaze config models # every model Trailblaze can see, per provider
trailblaze config llm # the provider/model currently selected
trailblaze config show # all persisted settings
```

If your model isn't listed, the usual causes are: the file isn't at
`trails/config/trailblaze.yaml` (Trailblaze walks up from the current directory looking for
exactly that path — see [Project Layout](project_layout.md)), the entry is nested under the
wrong provider key, or an unquoted `id` containing `:` parsed as a map.

## Contributing a model to the built-in registry

Once a model is worth having work out of the box for everyone, send it upstream — but note
you never have to wait for that to use it.

1. Add the entry to the matching provider file in
[`trailblaze-models/src/commonMain/resources/trails/config/providers/`](https://github.com/block/trailblaze/tree/main/trailblaze-models/src/commonMain/resources/trails/config/providers).
2. Regenerate the docs — [`docs/generated/LLM_MODELS.md`](generated/LLM_MODELS.md) is
generated from those provider files, so hand-editing it drifts and fails CI:

```bash
./gradlew :docs:generator:run
```

3. Commit both the provider YAML and the regenerated `LLM_MODELS.md`.
123 changes: 122 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,124 @@
title: Configuration
---

# Configuration

Most of Trailblaze needs no configuration — a single `.trail.yaml` file is a complete
project. When you do want project-level settings (which app to target, which LLM to use,
which toolsets your agent sees), they live in one file: **`trailblaze.yaml`**.

## `trailblaze.yaml` — the workspace config file

```
my-project/
└── trails/
├── config/
│ └── trailblaze.yaml ← this file
└── login/
└── trail.yaml
```

Trailblaze walks up from the current directory (or from the directory containing the trail
you invoked) until it finds `trails/config/trailblaze.yaml`. The owning `trails/` directory
becomes the **workspace root**, and every relative path inside the file resolves against
`trails/config/`. See [Project Layout](project_layout.md) for the discovery rules and
[External Config](generated/external-config.md) for the full `trails/config/` directory shape.

`trailblaze.yaml` is config, never a trail — Trailblaze will not try to run it.

**Every section is optional**, and an empty file is valid. Committing it is the point: it's
how a team pins one target, one model, and one toolset surface for everybody who clones the
repo, with no per-machine setup.

```yaml
# trails/config/trailblaze.yaml — everything below is optional
defaults:
target: my-app
max-llm-calls: 25

targets:
- my-app

llm:
providers:
openai:
models:
- id: gpt-4.1
defaults:
model: gpt-4.1
```

### Top-level keys

| Key | Type | What it does |
|---|---|---|
| `defaults` | map | Workspace-wide defaults — see below |
| `targets` | list of ids | Target-trailmap ids this workspace opts into. **Omit to auto-discover** every target trailmap under `<workspace>/trailmaps/`. Listing ids is how a workspace with many trailmaps loads only a subset. Each id must be a *target* trailmap (one with a `target:` block); library trailmaps reach scope through a target's `dependencies:`. |
| `toolsets` | list | Extra toolsets, either written inline or pulled in with `ref: path/to/toolset.yaml` |
| `tools` | list | Extra tools, same inline-or-`ref:` shape |
| `providers` | list | Reserved for standalone LLM provider files. Provider and model definitions are read from the `llm:` block today — put them there. |
| `llm` | map | LLM providers, models, and defaults — see [LLM Configuration](llm_configuration.md) |

### `defaults`

| Key | What it does |
|---|---|
| `target` | Target-trailmap id used when nothing more specific is set. Must match a loaded target (case-sensitive); an unknown id is logged and skipped rather than failing the run. |
| `max-llm-calls` | Team-wide cap on LLM calls per objective, so every developer and CI runner inherits the same budget without passing `--max-llm-calls`. Positive integer. |

### `ref:` entries

`toolsets`, `tools`, and `providers` each accept either the entry written inline or a
pointer to a separate file:

```yaml
toolsets:
- ref: toolsets/my-toolset.yaml # relative to trails/config/
- name: inline-toolset # or write the whole thing here
tools: [tapOn, assertVisible]
```

Ref paths are always resolved relative to the directory holding `trailblaze.yaml` — a
leading `/` is stripped and treated the same way, so `/foo.yaml` is not an escape to the
filesystem root. A `ref:` entry may not carry any sibling keys.

## Precedence

Later rows win.

| Priority | Source | Scope |
|---|---|---|
| 1 (lowest) | Built-in defaults shipped in the binary | Everyone |
| 2 | `~/.trailblaze/trailblaze.yaml` | Just you, every workspace |
| 3 | `<workspace>/trails/config/trailblaze.yaml` | Everyone in this project |
| 4 | Persisted per-machine settings (`trailblaze config …`) | Just you, this machine |
| 5 (highest) | Environment variables and per-run CLI flags | This invocation |

Two clarifications worth knowing:

- **Workspace beats user file, but per-run beats everything.** A committed workspace file is
the team's baseline; `--target`, `-d`, `TRAILBLAZE_DEFAULT_MODEL` and friends still win for
a single run, which is what makes CI overrides work.
- **`defaults.target` is deliberately ranked below a real user selection but above the
neutral built-in target** — and a persisted selection of the neutral `default` target does
*not* count as a real selection, so it can't mask the committed workspace default. Full
ordering in [Project Layout → Workspace defaults](project_layout.md#workspace-defaults).

## Common tasks

| I want to… | Go to |
|---|---|
| Use a model that isn't built in | [Adding a Model](adding_a_model.md) |
| Point at a private LLM gateway | [LLM Configuration](llm_configuration.md#enterprise-gateway) |
| See what's currently in effect | `trailblaze config show` ([CLI](CLI.md#trailblaze-config)) |
| Set a default target for the team | `defaults.target`, above |
| Understand the `trails/config/` directory | [External Config](generated/external-config.md) |
| Add custom tools to a project | [Your First Trailmap](your-first-trailmap.md) |

Per-machine settings (`trailblaze config llm`, `trailblaze config target`, …) live in
`~/.trailblaze/` and are documented with the [`trailblaze config`](CLI.md#trailblaze-config)
command.

## On-Device Android Instrumentation Arguments
* `trailblaze.aiEnabled` (defaults to `true`) - This will have the Trailblaze SDK send all requests to the LLM. When `false`, only recordings can be used.
* `trailblaze.reverseProxy` (defaults to `false`) - This will enable the reverse proxy for all Trailblaze traffic.
Expand All @@ -13,11 +131,14 @@ title: Configuration
* `trailblaze.httpsPort` (defaults to `52526`, i.e. `trailblaze.port` + 1) - The HTTPS port for the Trailblaze server. Override this when running multiple Trailblaze instances.
* `trailblaze.logsEndpoint` - Defaults to the same values as the `reverseProxy` uses. You can use this value if you want to use a remote logs server. NOTE: Logging timeouts are set to 5 seconds as they are expected to be fast.

LLM selection for on-device runs has its own resolution order — see
[LLM Configuration → On-Device Android Agent](llm_configuration.md#on-device-android-agent).

## Scripting Callback Channel

Tuning knobs for the `/scripting/callback` endpoint that backs the TypeScript scripting SDK's `client.tools.<name>(args)` round-trip (the wire-protocol callback name inside the framework is `callTool`). Defaults are production-ready; override only when a slow emulator or unusual composition graph needs more headroom.

* `-Dtrailblaze.callback.timeoutMs` (JVM system property, defaults to `120000`) — Per-callback dispatch timeout on the daemon side. Bounds how long a single `client.tools.<name>(args)` dispatch can run before the daemon returns a structured timeout error. Raise when a target tool is legitimately slow (e.g. waiting for a screen to settle on a slow emulator).
* `TRAILBLAZE_CLIENT_FETCH_TIMEOUT_MS` (env var, defaults to `32000` standalone) — Client-side fetch timeout in the subprocess. At runtime the daemon forwards its own timeout value + 2 s as this variable, so the daemon is normally the one that surfaces a structured timeout. **If you raise `trailblaze.callback.timeoutMs`, raise this in lockstep** — otherwise the client aborts the HTTP request before the daemon can return and the daemon-side override is defeated. Sampled once at SDK module load; must be set before `import { trailblaze } from "@trailblaze/scripting"`.
* `-Dtrailblaze.callback.maxDepth` (JVM system property, defaults to `16`) — Reentrance cap for recursive callback chains. A subprocess tool that calls back into the daemon to dispatch another subprocess tool counts as one level; the cap prevents runaway recursion from wedging a session until the outer agent timeout fires. Raise only if you have a legitimate deep-composition use case (e.g. recursive tree-walker).
* `-Dtrailblaze.callback.maxBodyBytes` (JVM system property, defaults to `1048576` / 1 MB) — Maximum accepted `JsScriptingCallbackRequest` body size. Requests whose declared `Content-Length` exceeds this are rejected with HTTP 413 before buffering. Real callback payloads are tiny (invocation id, session id, a single action with a JSON-string args field) so the cap is pure belt-and-suspenders against a buggy subprocess emitting a runaway args string. Raise only if a legitimate tool needs to pass a very large args payload through the callback channel.
* `-Dtrailblaze.callback.maxBodyBytes` (JVM system property, defaults to `1048576` / 1 MB) — Maximum accepted `JsScriptingCallbackRequest` body size. Requests whose declared `Content-Length` exceeds this are rejected with HTTP 413 before buffering. Real callback payloads are tiny (invocation id, session id, a single action with a JSON-string args field) so the cap is pure belt-and-suspenders against a buggy subprocess emitting a runaway args string. Raise only if a legitimate tool needs to pass a very large args payload through the callback channel.
7 changes: 6 additions & 1 deletion docs/generated/LLM_MODELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ Trailblaze ships with the following built-in models. When you reference a model
| `gemini-3.1-flash-lite-preview` | 1M | 65K | $0.25 | $1.50 | $0.03 | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemini-3.1-pro-preview` | 1M | 65K | $2.00 | $12.00 | $0.20 | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemini-3.1-pro-preview-customtools` | 1M | 65K | $2.00 | $12.00 | $0.20 | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemma-4-31b-it` | 262K | 65K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemma-4-12b-it` | 262K | 32K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemma-4-26b-a4b-it` | 262K | 32K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemma-4-31b-it` | 262K | 32K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |

## Ollama

| Model ID | Context | Max Output | Input $/1M | Output $/1M | Cached Input $/1M | Capabilities |
|----------|---------|------------|-----------|------------|-------------------|--------------|
| `gemma4:12b` | 262K | 8K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemma4:26b` | 262K | 8K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gemma4:31b` | 262K | 8K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gpt-oss:120b` | 131K | 65K | free | free | free | basic-json-schema, completion, document, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `gpt-oss:20b` | 131K | 65K | free | free | free | basic-json-schema, completion, document, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
| `qwen3-vl:2b` | 131K | 8K | free | free | free | basic-json-schema, completion, document, image, multipleChoices, openai-endpoint-chat-completions, openai-endpoint-responses, speculation, standard-json-schema, temperature, toolChoice, tools |
Expand Down
Loading
Loading