LLM Configuration
Trailblaze supports configurable LLM providers and models via YAML files. This allows teams to use enterprise endpoints, custom gateways, self-hosted models, and project-specific defaults without modifying source code.
Just want to use a model Trailblaze doesn’t ship with? You don’t need a new release — add it to your workspace config. Adding a Model is the short, task-focused version of this page.
Configuration Loading Order¶
Configuration is loaded from multiple locations. Later sources override earlier ones:
| Priority | Location | Purpose |
|---|---|---|
| 1 (lowest) | Built-in defaults | Ship with Trailblaze binary |
| 2 | ~/.trailblaze/trailblaze.yaml (under llm: key) |
User-level preferences |
| 3 | ./trails/config/trailblaze.yaml (under llm: key) |
Project/workspace defaults |
| 4 (highest) | Environment variables | CI/CD and runtime overrides |
If no YAML config files exist, Trailblaze uses the built-in defaults with API keys from environment variables (same behavior as before YAML config was introduced).
Quick Start¶
Minimal: Just set an API key¶
No YAML needed. Set your provider’s API key:
export OPENAI_API_KEY="sk-..."
Trailblaze will use the built-in model list for that provider.
Project defaults via trails/config/trailblaze.yaml¶
Create trails/config/trailblaze.yaml in your workspace to set defaults for everyone on the team:
llm:
providers:
openai:
models:
- id: gpt-5.6-terra
- id: gpt-5.6-luna
defaults:
model: gpt-5.6-terra
When anyone clones the repo and launches Trailblaze, they get these models by default.
Enterprise gateway¶
For organizations with a private Gen AI gateway (e.g., Azure OpenAI, a corporate proxy, or a managed LLM service):
# trails/config/trailblaze.yaml
llm:
providers:
corp_gateway:
type: openai_compatible
base_url: "https://llm-gateway.internal.example.com/v1"
headers:
x-team-id: "ui-testing"
auth:
env_var: CORP_LLM_API_KEY
models:
- id: gpt-5.6-terra
context_length: 1050000
max_output_tokens: 128000
- id: gpt-5.6-luna
context_length: 1050000
max_output_tokens: 128000
defaults:
model: gpt-5.6-terra
Team members only need to set CORP_LLM_API_KEY in their environment. The gateway URL, headers, and model selection are all defined in the project.
YAML Schema Reference¶
Full example¶
providers:
# Standard provider (uses built-in API endpoint)
openai:
enabled: true
auth:
env_var: OPENAI_API_KEY
models:
- id: gpt-5.6-terra
- id: gpt-5.6-luna
- id: gpt-5.6-sol
cost:
input_per_million: 5.00
output_per_million: 30.00
# Custom OpenAI-compatible endpoint
azure_openai:
type: openai_compatible
base_url: "https://my-resource.openai.azure.com/openai/deployments"
headers:
api-version: "2024-02-15-preview"
auth:
env_var: AZURE_OPENAI_API_KEY
models:
- id: my-gpt4-deployment
context_length: 1048576
max_output_tokens: 32768
# Local models (no API key needed)
ollama:
enabled: true
models:
- id: qwen3-vl:8b
- id: llama3.2:latest
context_length: 131072
max_output_tokens: 8192
defaults:
model: gpt-5.6-terra
Provider fields¶
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | No | Whether this provider is active (default: true) |
type |
string | No | Provider type (see below). Inferred from key for standard providers. |
description |
string | No | Human-readable description (supports Markdown). Shown in UI and logs. |
base_url |
string | No | Custom API endpoint. Required for openai_compatible. |
chat_completions_path |
string | No | Custom path for chat completions (e.g., serving-endpoints/{{model_id}}/invocations) |
headers |
map | No | Additional HTTP headers sent with every request |
auth.env_var |
string | No | Environment variable containing the API key |
auth.required |
boolean | No | Whether auth is mandatory (default: true). Set false for local models. |
models |
list | Yes | List of model configurations for this provider |
Provider types¶
| Type | Description | Default base URL |
|---|---|---|
openai |
OpenAI API | https://api.openai.com/v1 |
anthropic |
Anthropic Claude API | https://api.anthropic.com |
google |
Google Gemini API | https://generativelanguage.googleapis.com |
ollama |
Ollama local server | http://localhost:11434 |
openrouter |
OpenRouter API | https://openrouter.ai/api/v1 |
openai_compatible |
Any OpenAI-compatible API | (must specify base_url) |
Standard provider keys (openai, anthropic, google, ollama, openrouter) infer their type automatically. Use openai_compatible for Azure, vLLM, LM Studio, custom gateways, and similar.
Model fields¶
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Model identifier sent to the API |
tier |
string | No | Model tier hint (e.g., inner, outer, both) |
vision |
boolean | No | Whether the model supports image input (default: true). Set false for text-only models. |
temperature |
number | No | Default temperature for this model. When set, used for all requests to this model. |
context_length |
integer | No | Maximum context window in tokens |
max_output_tokens |
integer | No | Maximum output tokens |
cost.input_per_million |
number | No | Cost per 1M input tokens (USD) |
cost.output_per_million |
number | No | Cost per 1M output tokens (USD) |
cost.cached_input_per_million |
number | No | Cost per 1M cached input tokens (USD) |
screenshot.max_dimensions |
string | No | Max screenshot dimensions as WIDTHxHEIGHT (e.g., 1536x768). Overrides project default. |
When id matches a built-in model (see Built-in Models), all specs are used automatically and any fields you specify override them. For custom models not in the built-in registry, specify context_length and max_output_tokens explicitly (they default to 131K/8K if omitted).
Default model selection¶
defaults:
model: gpt-5.6-terra
screenshot:
max_dimensions: 1536x768 # Default screenshot scaling (default if omitted)
These can be overridden by environment variables:
- TRAILBLAZE_DEFAULT_MODEL
Workspace Defaults¶
trails/config/trailblaze.yaml sets defaults for everyone working in that workspace. This is the recommended way for teams and organizations to configure LLM providers.
Why set workspace defaults?
- Built-in model lists can change between Trailblaze releases (models added, pricing updated, etc.)
- A workspace config pins your project to specific models and providers
- New team members get working defaults without manual setup
- Enterprise gateways are configured once, not per-developer
Example: An organization using a private gateway:
# trails/config/trailblaze.yaml (committed to repo)
llm:
providers:
acme_gateway:
type: openai_compatible
base_url: "https://ai.acme.internal/v1"
auth:
env_var: ACME_AI_TOKEN
models:
- id: gpt-5.6-terra
context_length: 1050000
max_output_tokens: 128000
- id: gpt-5.6-luna
context_length: 1050000
max_output_tokens: 128000
defaults:
model: gpt-5.6-terra
Individual developers can still override by creating ~/.trailblaze/trailblaze.yaml in their home directory (user-level config takes lower priority, but environment variables take highest priority).
Ollama (Local Models)¶
Runtime discovery¶
When Ollama is installed, Trailblaze automatically discovers locally available models by running ollama list. These appear alongside any models configured in YAML.
Project-configured models¶
You can list Ollama models in your workspace’s trails/config/trailblaze.yaml even if they are not currently installed on the developer’s machine. This is useful when a project recommends specific local models:
llm:
providers:
ollama:
models:
- id: qwen3-vl:8b
- id: qwen3.5:27b
defaults:
model: qwen3-vl:8b
Both ids above are in the built-in registry, so they need no spec. A model the registry doesn’t know falls back to a generic context length — give those an explicit spec, as Custom model specs below shows.
Models that are not installed will show a warning in the desktop app UI indicating the model is not available via Ollama. Developers can install them with:
ollama pull qwen3-vl:8b
Trailblaze will not auto-download models. The project config serves as documentation of which models the team recommends.
Custom model specs¶
For Ollama models not in the built-in registry, provide context length and output token limits:
- id: my-custom-gguf:latest
context_length: 131072
max_output_tokens: 8192
For a text-only model, also set vision: false — otherwise Trailblaze attaches screenshots
to requests (including AI-backed assertions) and Ollama rejects them with a 400.
Context window (num_ctx)¶
Trailblaze requests a 64K context window (num_ctx: 65536) on every Ollama call, clamped
down to the model’s declared context_length when that is lower.
Trailblaze now owns this setting. A num_ctx on the request sits at the top of
Ollama’s precedence chain — above a PARAMETER num_ctx in the model’s Modelfile, and above
the server’s OLLAMA_CONTEXT_LENGTH — so neither of those takes effect for Trailblaze’s
requests any more. Use TRAILBLAZE_OLLAMA_NUM_CTX (below) rather than a server-side
setting, which will look like it is being ignored.
The clamp only applies to a model whose context_length Trailblaze knows: one in the
built-in registry, or one you declared with an explicit spec. A model with neither falls
back to a generic context length, and the clamp does nothing for it — so if such a model’s
real window is smaller than the requested value, declare its context_length (see
Custom model specs) instead of relying on the clamp.
When a request doesn’t ask for a context window, Ollama picks one itself, sized to the
memory it has available rather than to what the model supports — so the same model gets a
large window on a workstation and a very small one on a laptop. A single Trailblaze agent
turn (screenshot + view hierarchy + tool definitions) is ~20K tokens on a content-heavy
screen, which is more than the low end of that range, and the turn fails with
exceed_context_size_error. Asking explicitly is what makes the context predictable
across machines instead of a property of the developer’s hardware.
64K is a deliberate middle: several multi-turn agent loops fit, and the KV cache still fits a laptop. On a machine with a lot of memory Ollama’s automatic choice can be larger than 64K, and an explicit request replaces it — raise it with the override below if you run very long loops on such a machine.
Override the requested value with TRAILBLAZE_OLLAMA_NUM_CTX:
TRAILBLAZE_OLLAMA_NUM_CTX=32768 trailblaze run --no-daemon login.trail.yaml
Lower it if your machine can’t afford the 64K KV cache for a larger model; raise it for very long agent loops. Malformed or non-positive values fall back to the default.
The value is read from the process that builds the Ollama client. trailblaze run
normally hands the run to a background daemon, which inherits the environment it was
started with — so a one-shot prefix like the above only applies with --no-daemon. To
change it for daemon-backed runs, restart the daemon with the variable set:
trailblaze stop
TRAILBLAZE_OLLAMA_NUM_CTX=32768 trailblaze app start
The override applies to host-side clients only. Android on-device runs always request the 64K default, because the instrumentation process has no host environment to read. If you set the override and run on-device AI legs against the same Ollama server, the two ends request different context lengths and Ollama reloads the model on every alternation — leave it unset for those runs.
Environment Variables¶
Standard environment variables for authentication:
| Provider | Environment Variable |
|---|---|
| OpenAI | OPENAI_API_KEY |
| Anthropic | ANTHROPIC_API_KEY |
GOOGLE_API_KEY |
|
| OpenRouter | OPENROUTER_API_KEY |
| Ollama | (none required) |
Custom providers specify their env var via auth.env_var in the YAML config.
On-Device Android Agent¶
On Android, LLM configuration works differently depending on the execution mode.
Host-driven (desktop app or CLI)¶
When running trails from the desktop app or CLI, the host selects the LLM provider and model (from YAML config, built-in defaults, or the UI), then passes the full TrailblazeLlmModel to the device via the RunYamlRequest RPC message. The device agent uses whatever the host sends — no local configuration is needed on the Android side.
Standalone instrumentation tests¶
When the Android agent runs standalone (e.g., AndroidTrailblazeRule in an instrumentation test), AndroidLlmClientResolver resolves the model automatically using this priority order:
| Priority | Source | Description |
|---|---|---|
| 1 (highest) | trails/config/trailblaze.yaml classpath resource |
On-device config bundled in the test APK |
| 2 | trailblaze.llm.default_model instrumentation arg |
Passed by the host at runtime |
| 3 | Auto-detect from provider tokens | First provider with an available API key wins |
Recommended: Add a config file to your test module
Create src/androidTest/resources/trails/config/trailblaze.yaml in your test module:
llm:
defaults:
model: openai/gpt-5.6-terra
The model key uses provider/model_id format (e.g., openai/gpt-5.6-terra, anthropic/claude-sonnet-5). AGP strips dot-prefixed directories from classpath resources, so the config lives under trails/config/ instead of .trailblaze/.
Then use AndroidTrailblazeRule with zero-arg defaults:
class MyTests {
@get:Rule val rule = AndroidTrailblazeRule()
@Test
fun myTest() = rule.runFromAsset()
}
API keys are still passed as instrumentation arguments (the config file only selects the model, not the credentials):
adb shell am instrument \
-e trailblaze.llm.auth.token.openai "sk-..." \
-w com.example.test/androidx.test.runner.AndroidJUnitRunner
Alternative: Instrumentation arg
If you don’t want a config file, pass the model as an instrumentation arg:
adb shell am instrument \
-e trailblaze.llm.default_model "openai/gpt-5.6-terra" \
-e trailblaze.llm.auth.token.openai "sk-..." \
-w com.example.test/androidx.test.runner.AndroidJUnitRunner
Alternative: Auto-detection
If neither a config file nor a trailblaze.llm.default_model arg is present, AndroidLlmClientResolver auto-detects the provider from the first available API key token. The provider priority order is: OpenAI, OpenRouter, Anthropic, Google, Ollama. The provider’s default_model (defined in the built-in provider YAML) is used. This order is defined in PROVIDER_PRIORITY in AndroidLlmClientResolver.
Custom openai_compatible providers on-device¶
Scope of this section. The args below describe the contract honored by the daemon-driven on-device path — i.e. when the desktop app or CLI ships a trail to the device through
AndroidStandaloneServerTest, which delegates toOnDeviceOpenAICompatibleLlmClientFactory. The instrumentation-test path documented in “Standalone instrumentation tests” above (runningAndroidTrailblazeRuledirectly without a host) goes through a separate, narrower openai_compatible code path inAndroidLlmClientResolver.createClientthat currently only honorsbase_urland the per-provider auth token —provider.type,chat_completions_path,headers, andauth_requiredare not yet wired through it. Consolidating those two on-device paths is tracked as a follow-up; until that lands, the args below apply only to the daemon-driven path.
A workspace openai_compatible provider (Azure, vLLM, LM Studio, custom gateways, etc.) reaches the on-device APK through a small set of additional instrumentation args. When you run trails via the desktop daemon, the host emits these args automatically. When you run am instrument directly (e.g. CI pipelines outside the daemon, custom test rigs), you can pass them yourself.
Security note. Tokens and credential-bearing headers passed as
-eargs on theam instrumentcommand line appear inps -efoutput (visible to other users on the device for the lifetime of the process), shell history files (~/.bash_history,~/.zsh_history), and CI build logs (which often persist for weeks). For real credentials, prefer the desktop daemon path — it resolves tokens from env vars via theauth.env_varfield in your workspacetrailblaze.yamland never puts them on a command line. The directam instrumentrecipe below is best treated as a debugging tool, not a production CI pattern. Header values passed viaprovider.headerscan also carry secrets (tenant tokens, signed routing keys, etc.) — treat them with the same care.
adb shell am instrument \
-e trailblaze.llm.default_model "my_gateway/some-model-id" \
-e trailblaze.llm.provider.type "openai_compatible" \
-e trailblaze.llm.provider.base_url "https://my-gateway.example.com" \
-e trailblaze.llm.provider.chat_completions_path "v1/chat/completions" \
-e trailblaze.llm.provider.headers '{"X-Tenant":"acme","X-Route":"default"}' \
-e trailblaze.llm.provider.auth_required "true" \
-e trailblaze.llm.auth.token.my_gateway "your-token-here" \
-w com.example.test/androidx.test.runner.AndroidJUnitRunner
Each arg, in detail:
| Arg | Required | Value |
|---|---|---|
trailblaze.llm.provider.type |
Yes | Must be openai_compatible (case-insensitive). Without this, the on-device factory skips custom-provider registration and falls back to the built-in OpenAI/Ollama/NONE map. |
trailblaze.llm.provider.base_url |
Yes | Gateway endpoint (e.g. https://my-gateway.example.com). |
trailblaze.llm.provider.chat_completions_path |
No | Custom path appended to base_url. Supports {{model_id}} substitution at the device — useful for per-deployment serving endpoints. |
trailblaze.llm.provider.headers |
No | JSON-encoded Map<String, String> of static request headers (e.g. tenant/routing keys). Malformed JSON is tolerated with a warning and proceeds with no static headers. |
trailblaze.llm.provider.auth_required |
No | "true" (default) or "false". When false, the on-device client is constructed even if no auth token is present — useful for local LLM servers that don’t need auth. |
trailblaze.llm.auth.token.<provider_id> |
Yes (unless auth_required=false) |
The provider’s auth token. The <provider_id> segment must match the id of the active provider (e.g. trailblaze.llm.auth.token.my_gateway if default_model=my_gateway/...). |
Important: the host already encodes provider metadata correctly when you use the desktop daemon — the args above are mostly relevant when you’re running am instrument directly. The on-device factory (OnDeviceOpenAICompatibleLlmClientFactory) is the authoritative parser of these keys.
If auth_required=true but no token arg is passed, the on-device runner fails fast with a clear error message naming the provider id and pointing at the remediation paths (set the env var, or set auth.required: false in your workspace yaml). This is by design — without it, the failure would surface several frames later as a generic “Unsupported provider” error that’s hard to diagnose without source access. Note: this fail-fast lives in OnDeviceOpenAICompatibleLlmClientFactory; the older AndroidLlmClientResolver openai_compatible branch used by AndroidTrailblazeRule doesn’t yet have the equivalent guard — it silently skips registering the custom client when the token is missing, and DefaultDynamicLlmClient.createLlmClient() then throws the generic “Unsupported provider” error a few frames later. The consolidation follow-up above brings both paths into parity.
Built-in Models¶
Trailblaze ships with a registry of models from major providers. See Built-in LLM Models for the full list with pricing and capabilities.
When referencing a built-in model by id in your YAML config, all specs (pricing, context length, capabilities) are inherited automatically. You only need to specify fields you want to override.
Built-in model specs are updated with each Trailblaze release. If you need stable, predictable pricing or specs, override them in your workspace config.
A model missing from the registry is not blocked — the registry only saves you from typing the specs. Declare any model your provider serves in trails/config/trailblaze.yaml and it works on the next run, no upgrade needed. See Adding a Model, which also covers contributing the model back to the built-in registry.