Repository navigation
Support AWF task-level model routing in the Copilot engine #64402
Description
Summary
AWF's task-level model routing (experimental.modelRouting + apiProxy.routing) is live and tested end to end in AWF, but gh-aw can't use it yet. Two things are missing:
- Compiler: gh-aw only validates the
experimental.modelRoutinggate (pkg/workflow/awf_config_test.go,TestValidateAWFConfigJSON_ModelRoutingGate). It never emitsapiProxy.routing. - Copilot harness:
actions/setup/js/copilot_harness.cjspicks the model, wire API, and (lack of) reasoning effort from the compile-timeCOPILOT_MODEL. Under routing, AWF's API proxy enforces the router's choice, so the agent is rejected.
Tracking on the AWF side: github/gh-aw-firewall#9238 (step 4). Evidence: github/gh-aw-firewall#9249.
How AWF routing works (agent-facing contract)
- Before the agent container starts, AWF classifies the task description in the routing conversation file and records one selection: provider, model, optional effort, and endpoint.
- The API proxy then pins every inference request to that selection:
body.modelmust equalwire_model.- An effort selection must go to
/responseswithreasoning.effort === effort. - An effortless selection must go to
/chat/completionswith noreasoning/reasoning_effort. - Anthropic selections go to
/v1/messageswithoutput_config.effort.
- Anything else gets
403 {"error":{"code":"model_routing_mismatch"}}, is recorded as a terminal routing failure, and AWF exits 78. - The agent learns the selection from
GET /reflect(AWF ≥ v0.28.29). It is available as soon as the agent starts:
"routing": {
"status": "selected",
"selection": {
"provider": "copilot",
"model": "github-copilot/gpt-5.4-mini",
"wire_model": "gpt-5.4-mini",
"effort": "xhigh",
"endpoint": "/responses"
}
}routing is null when routing is inactive; status may also be pending or failed (with failure_code).
Reproduction
github/gh-aw-firewall#9249 runs Copilot CLI 1.0.89 (DefaultCopilotVersion) inside AWF with routing enabled and allowedModels: [github-copilot/claude-haiku-4.5, github-copilot/gpt-5.4-mini]. The router picks gpt-5.4-mini / xhigh / /responses.
--model gpt-5.4-mini --reasoning-effort xhighalone: the CLI sends to/chat/completions, becauseCOPILOT_PROVIDER_WIRE_APIis unset. It gets403; the CLI printsAuthentication failed with provider at http://172.30.0.30:10002 (HTTP 403)and AWF exits 78 (run 36668364575).- Adding
COPILOT_PROVIDER_WIRE_API=responses, taken from the selection's endpoint:ROUTED_OK, exit 0 (run 36669199288).
So model, effort, and wire API all have to come from the selection.
Proposed fix
0. Bump the default firewall to v0.28.29
pkg/constants/version_constants.go pins DefaultFirewallVersion = "v0.28.27". Bump it to v0.28.29, which is the first release that has everything the harness needs:
- v0.28.28: Copilot
/modelseffort metadata is read fromcapabilities.supports.reasoning_effort(fix(api-proxy): read Copilot reasoning efforts from live /models shape gh-aw-firewall#9225). Without it every Copilot model is excluded and routing fails withno_route. It also adds native OpenAI/Anthropic routing (Add native OpenAI and Anthropic model routing gh-aw-firewall#9223). - v0.28.29: the
/reflectroutingfield (feat(api-proxy): expose the routed selection in /reflect gh-aw-firewall#9239), which the harness reads to learn the selection.
Also:
- Add a routing min-version constant (e.g.
AWFModelRoutingMinVersion = "v0.28.29"), and refuse to emitapiProxy.routingfor workflows pinned below it. - Recompile the lock files after the bump.
- The digest-pinned
container.imagesmanifest emitted for routed runs must use the v0.28.29 digests: agentsha256:edcf17ae…, api-proxysha256:5cc683af…, squidsha256:1d5e169c…, plus a pinnedrouterimage.
1. Compiler: emit routing config
- Add a frontmatter surface. The shape is open to discussion; for example:
engine: id: copilot model-routing: goal: cost # cost | cost-speed mode: balanced # economy | balanced | robust | auto allowed-models: [claude-haiku-4.5, gpt-5.4-mini]
- When it is set, emit into the AWF config:
experimental.modelRouting: trueapiProxy.routing: { objective: { goal, mode }, task: { conversationFile } }apiProxy.allowedModels(provider-qualified, e.g.github-copilot/gpt-5.4-mini)- Optionally
apiProxy.routing.provider
- Add a host-side pre-agent step that writes the routing conversation file, e.g.
/tmp/gh-aw/routing-conversation.json:[{"role":"user","parts":[{"text":"<task prompt>"}]}], built from the rendered workflow prompt. Its user messages are the task description the router classifies; the resulting labels are the classification. The step must run on the host, outside the agent. - A routed run requires a complete, digest-pinned
container.imagesmanifest that includesrouter. AWF rejects tag-only router images. - Routing is incompatible with
--keep-containers, DinD / split filesystems, Docker-socket exposure, and non-Linux/non-runc hosts. The compiler should reject those combinations early, or let AWF's validation surface them. - Gate on
AWFModelRoutingMinVersion(see step 0). - Don't emit a fixed
COPILOT_MODELfor routed workflows, or treat it as ignored (see below).
2. Copilot harness: honour the selection
The harness already fetches /reflect (AWF_REFLECT_ENABLED=1 → fetchAWFReflect). When reflectData.routing is non-null:
- Require
status === "selected". Onfailed/pending, exit non-zero without starting Copilot (AWF will report exit 78 either way). - Model: set
COPILOT_MODEL = selection.wire_model. SkipapplyModelFallbackandapplyCopilotModelAliasResolution: rewriting the model produces a mismatch. - Wire API: set
COPILOT_PROVIDER_WIRE_API = selection.endpoint === "/responses" ? "responses" : "completions". This must override any existing value; todayapplyCopilotWireAPIskips when the variable is already set, which is exactly the wrong precedence under routing. - Effort: when
selection.effortis non-null, pass--reasoning-effort <effort>to the CLI. In SDK mode, set the session's reasoning effort. When it is null, pass no effort. - Sub-agents / task tool: the headless sidecar propagates
COPILOT_MODELto sub-agent sessions. They must use the same model, effort and wire API; any other model (e.g. a cheaper sub-agent model) is rejected with 403 and fails the run. - Logging: log the applied selection (
inference routing: mode=awf-routed model=… effort=… wire_api=…), without auth values.
Threat detection and other separate AWF invocations without apiProxy.routing are unaffected.
3. Failure classification
AWF exit 78 under routing means a routing failure (no route, router error, or model_routing_mismatch). Classify it separately from generic agent failures and from HTTP 403 budget/permission handling, so it isn't retried (it's terminal) and the report names the cause.
Acceptance
-
DefaultFirewallVersionis bumped tov0.28.29,AWFModelRoutingMinVersionis added, and the lock files are recompiled. - A workflow with
model-routingcompiles to an AWF config containingexperimental.modelRouting,apiProxy.routing,allowedModels, a digest-pinnedrouterimage, and a conversation-file step. - The harness applies model, wire API and effort from
/reflectrouting.selection, overriding compile-time values. - A routed Copilot run completes with AWF exit 0. A deliberately mismatched model yields exit 78 and is classified as a routing failure.
- Unit tests for the harness selection logic; a compile test for the emitted config.
Activity
lpcox commentedon Sep 30, 2026
Update (2026-09-30): AWF v0.28.30 is now released: https://github.com/github/gh-aw-firewall/releases/tag/v0.28.30
This supersedes the v0.28.29 target in section 0 as the current routing baseline. v0.28.29 introduced the
/reflectrouting selection consumed by the harness; v0.28.30 adds broader routing smoke coverage: effort-routed Copilot, a two-candidate routing decision, live OpenAI and Anthropic routing, and running Copilot CLI against the routed selection (gh-aw-firewall#9243, #9245, #9247, #9249). The gh-aw implementation should pin/test against v0.28.30, while retaining the routing minimum-version gate as appropriate.lpcox commentedon Sep 30, 2026
Additional compatibility blocker: inline
## agent:sub-agents can declare their ownmodel:(commonlysmall;model: inheritedis only the default). In a routed run, AWF admits exactly the selectedwire_model/endpoint/effort tuple. If Copilot starts an inline sub-agent with its declared alternate model, that request will receive403 model_routing_mismatch; AWF records a terminal routing failure and the overall run exits 78, even if the parent could otherwise handle the task-tool failure.The routed implementation therefore must cover generated inline-agent definitions, not only the parent harness environment:
model:aliases such assmall.small/largealiases before applying the routing selection.model: small; verify the sub-agent actually usesrouting.selection.wire_modeland the run exits 0. Also keep a deliberate non-selected-model request test that exits 78.This affects the existing sub-agent acceptance item: propagating
COPILOT_MODELto the headless sidecar is insufficient if the.agent.mdfrontmatter still wins when the sub-agent session is created.lpcox commentedon Sep 30, 2026
Correction to my previous comment: forcing all inline agents to inherit the parent selection would avoid the 403, but it would regress a core sub-agent capability. The harness should remain able to create sub-agents with different models.
The underlying mismatch is that AWF currently makes one task-wide selection and admits only one
(provider, wire_model, endpoint, effort)tuple. Supporting gh-aw sub-agents properly requires session/agent-scoped routing, not merely propagating the parent environment.Suggested contract:
small,large, explicit model, orinherited).parentplusagents.<name>or through a child-session selection API.model: inheritedreuses the parent scope.model: small/largebecomes a routing constraint, not a concrete client-selected model. An explicit model may be treated as a singleton constraint, subject to policy.allowedModelsglobally: that would let any session bypass the router’s decision. Admission must be scoped to the parent or named child session.Acceptance should include a parent routed to one model and an inline
model: smallagent routed to a different model, with both requests accepted only in their respective scopes. Cross-using either selection must produce403 model_routing_mismatch.lpcox commentedon Sep 30, 2026
Correcting my two previous comments on sub-agents. Both assumed the API proxy must keep admitting only the routed tuple. That assumption is being dropped.
Model routing is configuration — it starts the agent on a model that suits the task — not a containment boundary. Cost and policy already have a routing-independent enforcement surface in AWF (
AWF_ALLOWED_MODELS/AWF_DISALLOWED_MODELS). Strict request pinning is therefore being removed on the AWF side: github/gh-aw-firewall#9267.What this means here:
## agent:withmodel: smallsimply runs on its own model. No403 model_routing_mismatch, no exit 78.The harness work in section 2 still stands, but its failure mode changes. Applying model, wire API, and effort from
/reflectrouting.selectionis what makes routing effective; not applying it now degrades to an unrouted run rather than exit 78. The reproduction in the issue body (/chat/completions→ 403 → exit 78) is specific to pinning and will no longer occur.Exit 78 remains meaningful for genuine routing failures —
no_route, router unreachable, contract errors — so section 3 stands as written.Suggest holding the harness changes until gh-aw-firewall#9267 lands, since it determines whether a mismatch is fatal.
lpcox commentedon Sep 30, 2026
The PR removing strict request pinning has merged into
github/gh-aw-firewallonmain:Summary of merged changes & current state
Selection is advisory, not enforced:
model,effort, andendpoint, header rejections, and403 model_routing_mismatchare completely removed.event: "model_routing",stage: "request",routed: "deviated" | "selected").Model policy remains the cost/allowlist guard:
AWF_ALLOWED_MODELS/AWF_DISALLOWED_MODELS(model-policy-guard.js), not the router.Sub-agents and alternate models work seamlessly:
## agent:sub-agents with their ownmodel:(e.g.small) or alternate parent model choices run cleanly without 403 or exit 78.Next steps for
gh-aw:main),gh-awcan proceed with the compiler and harness integration without any sub-agent model restrictions, forced inheritance, or scope-ID plumbing.COPILOT_MODEL,COPILOT_PROVIDER_WIRE_API, and reasoning effort from/reflectrouting.selectionto make the routing suggestion effective.