Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions .changeset/engine-model-override-precedence.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 0 additions & 10 deletions .changeset/engine-model-to-top-level.md

This file was deleted.

27 changes: 15 additions & 12 deletions docs/adr/46570-add-top-level-model-field.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# ADR-46570: Add Top-Level `model` Field, Deprecate `engine.model`
# ADR-46570: Add Top-Level `model` Field, Allow `engine.model` as a Per-Instance Override

**Date**: 2026-07-19
**Status**: Draft
**Deciders**: Unknown
**Status**: Accepted
**Deciders**: @pelikhan

---

### Context

Workflow frontmatter currently requires users to nest a single scalar (the LLM model name) inside the `engine` object as `engine.model`. This forces users to understand the engine object structure just to set a model, even when they do not need to customize any other engine behavior. Other top-level scalar configuration fields (`max-turns`, `max-runs`, `max-tool-denials`) follow a flat, top-level convention. `engine.model` is therefore an ergonomic outlier that increases cognitive load and verbosity for a common operation. A top-level `model` field would align model selection with the existing convention and reduce friction.

A follow-up issue (github/gh-aw#54047) showed that unconditionally deprecating `engine.model` in favor of the top-level field breaks a legitimate use case: workflows that run a different engine (and therefore a differently-namespaced model) for `safe-outputs.threat-detection` than for the main agent. In that split-engine scenario the top-level `model:` can only hold one model ID, so `threat-detection.engine.model` is the *only* way to select a different model for the detection engine. Deprecating it, and nudging users toward the top-level field via `gh aw fix`, made that scenario unsupportable.

### Decision

We will introduce a canonical top-level `model` field in workflow frontmatter for specifying the LLM model used by the agentic engine. The top-level field takes precedence over `engine.model` when both are present. We will simultaneously deprecate `engine.model`, emit a compile-time warning when it is encountered, and provide an automated codemod (`engine-model-to-top-level` via `gh aw fix`) to migrate existing workflows.
We introduce a canonical top-level `model` field in workflow frontmatter for specifying the LLM model used by the agentic engine. `engine.model` remains a fully supported, non-deprecated way to set (or override) the model for a *specific* engine instance: when both are present, the nested `engine.model` takes precedence over the top-level `model` for that engine instance, since it is the more specific setting. This makes it possible to set a workflow-wide default via top-level `model` while still overriding it per engine instance (e.g. `safe-outputs.threat-detection.engine.model`). We removed the `engine-model-to-top-level` codemod and the compile-time deprecation warning, since automatically migrating `engine.model` to the top level would silently break the split-engine use case.

### Alternatives Considered

Expand All @@ -24,23 +26,24 @@ Retain the existing `engine.model` syntax with no changes. Users who want to cha

Introduce a top-level `llm-model` field instead of `model` to avoid any potential ambiguity with other hypothetical uses of `model`. This eliminates the name-collision risk but introduces a less intuitive, hyphenated field name that diverges from both YAML conventions and common agentic tooling vocabulary. Given that `model` is already the universally understood name for LLM model selection, the disambiguation is not worth the usability cost.

#### Alternative 3: Deprecate `engine.model` and Auto-Migrate It to the Top Level

The original version of this ADR deprecated `engine.model`, emitted a compile-time warning, and shipped a `gh aw fix` codemod to move it to the top-level `model` field. This was reverted (github/gh-aw#54047) because it broke split-engine workflows (e.g. main agent on one engine, `safe-outputs.threat-detection.engine` on another) where the two engine instances legitimately need different models. There is no single top-level value that can represent both, so nudging users to migrate away from `engine.model` — and having `gh aw fix` do so automatically — actively broke working configurations.

### Consequences

#### Positive
- Simpler, flatter YAML: users can set `model: gpt-5.4` without understanding the engine object structure.
- Consistent with the existing convention for top-level scalar fields (`max-turns`, `max-runs`, etc.).
- Schema deprecation markers and a compile-time warning guide existing users toward migration automatically.
- The `gh aw fix` codemod enables zero-effort automated migration for the common case.
- `engine.model` remains available, without warnings, as the mechanism for overriding the model on a specific engine instance (e.g. `safe-outputs.threat-detection.engine.model`) independently of the top-level default.

#### Negative
- A deprecation period means two syntactically valid ways to configure the model coexist, increasing implementation complexity (precedence logic) and potential user confusion.
- The inline-map engine syntax (`engine: { id: copilot, model: gpt-4 }`) cannot be automatically migrated by the codemod and requires manual intervention.
- Maintaining the codemod and deprecation warning adds ongoing test surface area that must be kept in sync.
- Two syntactically valid ways to configure the model coexist indefinitely (top-level default vs. per-instance override), which slightly increases precedence logic complexity and requires clear documentation of "nested wins over top-level".

#### Neutral
- The `FrontmatterConfig` struct gains a new `Model` field; consumers that serialize or deserialize frontmatter must account for it.
- The JSON schema gains a top-level `model` property and `deprecated`/`x-deprecation-message` markers on both `engine_config` variants — tooling that surfaces schema metadata will reflect this.
- The `FrontmatterConfig` struct has a `Model` field; consumers that serialize or deserialize frontmatter must account for it.
- The JSON schema has a top-level `model` property; `engine.model` is documented as a per-instance override with no `deprecated`/`x-deprecation-message` markers.

---

*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
*ADR created by [adr-writer agent]. Amended following github/gh-aw#54047 to reverse the `engine.model` deprecation.*
2 changes: 0 additions & 2 deletions docs/public/editor/autocomplete-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1007,8 +1007,6 @@
"model": {
"type": "string",
"desc": "Optional specific LLM model to use (e.g., 'claude-3-5-sonnet-20241022', 'gpt-4').",
"deprecated": true,
"x-deprecation-message": "'engine.model' is deprecated. Use top-level 'model' instead. Run 'gh aw fix' to automatically migrate.",
"leaf": true
},
"model-provider": {
Expand Down
24 changes: 19 additions & 5 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -2409,6 +2409,14 @@ engine:
# (optional)
version: null

# Optional specific LLM model to use (e.g., 'claude-3-5-sonnet-20241022',
# 'gpt-4'). Has sensible defaults and can typically be omitted. Overrides the
# top-level 'model' field for this engine instance, which is useful when a nested
# engine (e.g. safe-outputs.threat-detection.engine) needs a different model than
# the main agent engine.
# (optional)
model: "example-value"

# Optional inference provider override for this engine. Defaults to the engine's
# native provider (copilot: github, claude: anthropic, codex: openai, pi: github).
# (optional)
Expand Down Expand Up @@ -3133,7 +3141,10 @@ engine:
# committing to a specific engine. The runtime selects an appropriate engine using
# its default, and the model preference is applied to it.
engine:

# Model preference or size category (e.g. 'small', 'large', 'gpt-4.1'). Applied to
# the default engine when engine.id is not specified. Overrides the top-level
# 'model' field.
model: "example-value"

# AWF turn cap (`max_turns`) applied consistently across all agentic engines.
# Supports GitHub Actions expressions (e.g. '${{ inputs.max-turns }}') for
Expand Down Expand Up @@ -21234,10 +21245,13 @@ github-app:
import-schema:
{}

# Optional top-level LLM model override. Sets the model used by the agentic engine
# for this workflow. Takes precedence over engine.model when both are specified.
# Supports full model IDs (e.g. 'claude-3-5-sonnet-20241022', 'gpt-5.4') and model
# aliases (e.g. 'small', 'large').
# Optional top-level LLM model override. Sets the default model used by the
# agentic engine for this workflow. Acts as a fallback when an engine instance
# does not specify its own 'model'; a nested 'engine.model' (e.g.
# safe-outputs.threat-detection.engine.model) takes precedence over this field
# for that engine instance. Supports full model IDs (e.g.
# 'claude-3-5-sonnet-20241022', 'gpt-5.4') and model aliases (e.g. 'small',
# 'large').
# (optional)
model: "example-value"

Expand Down
30 changes: 0 additions & 30 deletions pkg/cli/codemod_engine_model.go

This file was deleted.

189 changes: 0 additions & 189 deletions pkg/cli/codemod_engine_model_test.go

This file was deleted.

1 change: 0 additions & 1 deletion pkg/cli/fix_codemods.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ func GetAllCodemods() []Codemod {
getRunInstallScriptsToRuntimesNodeCodemod(), // Move top-level run-install-scripts under runtimes.node
getMentionsAllowTeamMembersCodemod(), // Rename allow-team-members to allowed-collaborators in safe-outputs.mentions
getEngineCopilotSDKDriverToDriverCodemod(), // Rename deprecated engine.copilot-sdk-driver to engine.driver
getEngineModelToTopLevelCodemod(), // Move engine.model to top-level model
}
fixCodemodsLog.Printf("Loaded codemod registry: %d codemods available", len(codemods))
return codemods
Expand Down
2 changes: 0 additions & 2 deletions pkg/cli/fix_codemods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ func TestGetAllCodemods_ContainsExpectedCodemods(t *testing.T) {
"run-install-scripts-to-runtimes-node",
"mentions-allow-team-members-to-allowed-collaborators",
"engine-copilot-sdk-driver-to-driver",
"engine-model-to-top-level",
}

for _, expectedID := range expectedIDs {
Expand Down Expand Up @@ -267,6 +266,5 @@ func expectedCodemodOrder() []string {
"run-install-scripts-to-runtimes-node",
"mentions-allow-team-members-to-allowed-collaborators",
"engine-copilot-sdk-driver-to-driver",
"engine-model-to-top-level",
}
}
Loading
Loading