How to customize LLM calls and streamText parameters?

HEAD
1e03a55fix(app): persist defensiveness (#12973)
This post might have stale content, as HEAD is 62 commits ahead.

how much can i customize from the underlying llm calls / streamText? meaning, stuff like maxSteps/stopWhen, temperature, etc?

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

Now I have a comprehensive picture. Here's the summary:


##Customization of the underlying streamText call

There's a moderate amount of customization available, but opencode does not expose the raw AI SDK streamText options directly. Instead, it provides several layered mechanisms. Here's what you can and can't control:

###✅ What you CAN customize

####1. temperature — via agent config

Set per-agent in opencode.json:

{
  "agent": {
    "build": {
      "temperature": 0.7
    }
  }
}

Falls back to provider-specific defaults in ProviderTransform.temperature() (e.g., 0.55 for Qwen, 1.0 for Gemini, undefined/default for Claude, etc.). Only applied if the model declares capabilities.temperature as true.

####2. top_p — via agent config

{
  "agent": {
    "build": {
      "top_p": 0.95
    }
  }
}

Falls back to ProviderTransform.topP().

####3. steps (was maxSteps) — via agent config

Controls the maximum number of agentic tool-use iterations before the agent is forced to respond with text only:

{
  "agent": {
    "build": {
      "steps": 50
    }
  }
}

This is not the AI SDK's maxSteps parameter on streamText. OpenCode implements its own step-counting loop in SessionPrompt. When the limit is hit, it injects a max-steps prompt as a fake assistant message telling the model to stop using tools. Default is Infinity (no limit).

####4. options (provider-specific passthrough) — via agent config or model config

The options field is a catch-all Record<string, any> that gets merged into providerOptions on the streamText call. This is how reasoning effort, thinking budgets, store, etc. are set. You can configure this at two levels:

  • Per-agent in config (and any unknown keys on the agent config automatically flow into options):

    {
      "agent": {
        "build": {
          "options": {
            "reasoningEffort": "high"
          }
        }
      }
    }
    
  • Per-model via the provider config:

    {
      "provider": {
        "anthropic": {
          "models": {
            "claude-sonnet-4-20250514": {
              "options": { "thinking": { "type": "enabled", "budgetTokens": 10000 } }
            }
          }
        }
      }
    }
    

The merge order is (llm.ts line ~108-113):

base provider options → model.options → agent.options → variant options

####5. variant — model reasoning effort presets

Models with reasoning capabilities get auto-generated variants (e.g., low, medium, high for reasoning effort). You can set a default variant per agent:

{
  "agent": {
    "build": {
      "variant": "high"
    }
  }
}

Or cycle variants at runtime with Ctrl+T.

####6. Max output tokens — via environment variable

OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX=64000 opencode

Defaults to 32,000. This caps the maxOutputTokens passed to streamText (subject to model limits).

####7. Custom headers — via provider config or plugin

{
  "provider": {
    "my-provider": {
      "models": {
        "my-model": {
          "headers": { "X-Custom": "value" }
        }
      }
    }
  }
}

####8. Plugin hooks — most flexible escape hatch

The chat.params plugin hook lets you mutate temperature, topP, topK, and the entire options bag right before the streamText call:

"chat.params": async (input, output) => {
  output.temperature = 0.3
  output.options.reasoningEffort = "high"
}

There's also chat.headers for injecting custom headers, and experimental.chat.system.transform / experimental.chat.messages.transform for modifying the system prompt and message history.

###❌ What you CANNOT customize

  • stopWhen — not used anywhere in the codebase. There's no support for it.
  • maxSteps (the AI SDK parameter) — OpenCode does not pass maxSteps to the AI SDK's streamText() at all. Instead, it runs its own while-loop that calls streamText one step at a time and manually manages iteration. The steps config controls that loop.
  • maxRetries — hardcoded to 0 (or an internally-passed retries param); not user-configurable.
  • topK — there's no config field for this. It's only set by ProviderTransform.topK() based on the model, or via the chat.params plugin hook.
  • seed, presencePenalty, frequencyPenalty — no config or passthrough.
  • Arbitrary streamText parameters (like toolChoice, experimental_activeTools filtering beyond permissions, etc.) — not directly exposed, though toolChoice could potentially be snuck in via options depending on provider.

###Summary Table

ParameterConfigurable?How
temperatureAgent config, plugin hook
top_p / topPAgent config, plugin hook
topK⚠️ Plugin onlychat.params plugin hook
steps (agentic loop limit)Agent config (steps field)
maxSteps (AI SDK)Not used; opencode runs its own loop
stopWhenNot implemented
maxOutputTokensOPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX env var
providerOptionsAgent options / model options / plugin
headersModel config, provider config, plugin
maxRetriesHardcoded to 0
seedNot exposed
presencePenaltyNot exposed
frequencyPenaltyNot exposed
system promptAgent prompt, instructions, plugin hooks

The plugin chat.params hook is your most powerful escape hatch — it gives you mutable access to temperature, topP, topK, and the entire options object right before it hits streamText.


END OF POST

How to customize LLM calls and streamText parameters? — anomalyco/opencode