Skip to content

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 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 to OnDeviceOpenAICompatibleLlmClientFactory. The instrumentation-test path documented in “Standalone instrumentation tests” above (running AndroidTrailblazeRule directly without a host) goes through a separate, narrower openai_compatible code path in AndroidLlmClientResolver.createClient that currently only honors base_url and the per-provider auth token — provider.type, chat_completions_path, headers, and auth_required are 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 -e args on the am instrument command line appear in ps -ef output (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 the auth.env_var field in your workspace trailblaze.yaml and never puts them on a command line. The direct am instrument recipe below is best treated as a debugging tool, not a production CI pattern. Header values passed via provider.headers can 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.