Repository navigation
Description
Problem
engine.model-routing works only with engine.id: copilot. The Claude, Codex and pi engines can already run on the Copilot API (copilot/<model> with permissions: copilot-requests: write), but a workflow that enables routing on any of them fails to compile.
Most of the routing path is already engine-agnostic:
BuildAWFConfigJSON(pkg/workflow/awf_config_build.go) writesapiProxy.routing, the candidate models and the experimental flag for any engine.sandbox_agent_images.goadds the routing images wheneverisModelRoutingEnabledis true.generateModelRoutingConversationStepruns before every engine's execution step (compiler_yaml_ai_execution.go).run_awf_with_startup_retries.shalready treats exit 78 as terminal for any engine whenGH_AW_MODEL_ROUTING=1.- Threat detection already drops
ModelRoutingfor every engine (Keep model routing out of threat detection jobs #66670).
Three things block the other engines:
- Compile time.
validateModelRouting(pkg/workflow/model_routing.go) rejects anyengine.idother thancopilot. - Step env. Only
addCopilotWorkflowStepEnv(copilot_engine_execution.go) setsGH_AW_MODEL_ROUTING=1, and onlyaddCopilotModelEnvskips the fixed-model env when routing is on. - Runtime. Only
copilot_harness.cjsreadsrouting.selectionfrom AWF/reflect(resolveAWFModelRoutingSelection,applyCopilotRoutingSelection,applyCopilotRoutingArgs) and refuses to start when no valid selection exists.claude_harness.cjs,codex_harness.cjs,pi_models_json.cjsandpi_provider.cjsalready fetch/reflectbut ignorerouting.
Evidence
We simulated pi with routing. For each task, we pinned pi to the model and effort the router had picked, then ran the Copilot engine pinned the same way on the same day. Neither run had apiProxy.routing in awf-config.json. A blind judge scored quality the same for both engines (85 vs 85 out of 100 over five tasks). pi used 37% fewer credits overall and about half on Opus.
The table shows three of those pairs, from gh aw audit (dev build 303b402810):
| Task | Model, effort | Engine | Run | Wire API | Requests | Tokens | Duration | Credits |
|---|---|---|---|---|---|---|---|---|
| t07 | claude-opus-5, high | pi | 37703112781 | /v1/messages |
32 | 2.05M | 12.3m | 204 |
| t07 | claude-opus-5, high | Copilot | 37702286157 | /chat/completions |
61 | 6.04M | 18.2m | 476 |
| t10 | claude-opus-5, xhigh | pi | 37703132300 | /v1/messages |
23 | 1.33M | 13.5m | 187 |
| t10 | claude-opus-5, xhigh | Copilot | 37702304919 | /chat/completions |
33 | 3.25M | 16.3m | 349 |
| t05 | gpt-5.6-sol, xhigh | pi | 37703092721 | /responses |
17 | 0.43M | 7.5m | 38 |
| t05 | gpt-5.6-sol, xhigh | Copilot | 37702265831 | /responses |
19 | 0.81M | 9.3m | 61 |
All six runs succeeded with no errors. Each pair ran the same model at the same effort:
- pi set the effort with
engine.config.settings.defaultThinkingLevel, and its session log recordsthinkingLevelon every message (high ×64, xhigh ×46, xhigh ×34). - Copilot set the effort with
reasoningEffort(high, xhigh, xhigh).
pi chooses the wire API for each model (resolvePiApiForModel in pi_models_json.cjs): Claude on native /v1/messages, GPT on /responses. The Copilot CLI uses a single wire API for each BYOK session (see github/gh-aw-firewall#9558), so it sent Opus over /chat/completions.
pi's savings come from two sources:
- Fewer requests on Opus: 32 vs 61, and 23 vs 33.
- Smaller context per request: about 53–61k cache-read tokens per request vs about 93–96k on Opus, and about 23k vs 40k on GPT-5.6-sol.
pi also accepted thinking level max.
Supporting routing on these engines would let authors combine the router's per-task choice of model and effort with a cheaper engine.
Proposed solution
1. Allow the engines at compile time (pkg/workflow/model_routing.go)
-
In
validateModelRouting, acceptclaude,codexandpialongsidecopilot, and keep the existing checks for firewall, goal, mode, model policy and AWF version. -
For the non-Copilot engines, require Copilot inference: the engine's
ResolveLLMProvidermust resolve to the GitHub provider. The router selects only Copilot models, andresolveModelRoutingAllowedModelsqualifies every candidate asgithub-copilot/. -
Add a per-engine compatibility check on
allowed-models, called fromvalidateModelRouting:- claude: Claude models only, because Claude Code speaks the native Messages API.
- codex: GPT models served on
/responsesonly. - pi: any Copilot model.
- copilot: unchanged.
The compiler has no wire-API catalog, so the check would classify models by family name. Candidates that contain expressions cannot be checked at compile time, so the runtime endpoint check in section 3 is the backstop.
-
Have
warnRoutedSubAgentModelswarn when a declared sub-agent model is incompatible with the engine. -
Update the
engine.model-routingdescription inmain_workflow_schema.jsonand the validation error text, which currently say "Copilot" and "supported only by the Copilot engine".
2. Set the routing env and drop fixed-model env for each engine
Factor the routing env into a shared helper used by all four engines, so GH_AW_MODEL_ROUTING=1 is set in one place:
- Claude (
claude_engine.go): set the flag inbuildClaudeCommandEnv. SkipapplyClaudeModelEnvVarswhen routing is on, which removesANTHROPIC_MODEL,GH_AW_MODEL_AGENT_CLAUDEand the fallback. That also stopsbuildClaudeCommandStringfrom appending--model. - Codex (
codex_engine.go): set the flag inbuildCodexExecutionEnv. Skip the model env and the fallback expression, and drop the--modelexpansion inbuildCodexCommand. - pi (
pi_engine.go): set the flag inbuildPiExecutionEnv, and skipGH_AW_PI_MODELandGH_AW_PI_NATIVE_PROVIDER.buildPiModelsJSONSetupcurrently returns nothing unlessengine.modelis set, and bakes--model aw-gateway/<id>into the command. Under routing, it should always emit themodels.jsonsetup without a compile-time model ID or--model.
Fixed-model settings should not override the routed selection. When routing is on, the compiler should warn about engine.model, model variables in engine.env, and engine.config.settings.defaultThinkingLevel, as the schema says it does for Copilot.
3. Apply the selection in each engine's runtime
Move resolveAWFModelRoutingSelection from copilot_harness.cjs into a shared module next to awf_reflect.cjs, and pass in the endpoints each engine accepts. ROUTING_REASONING_EFFORTS from copilot_routing_effort.cjs moves with it.
When GH_AW_MODEL_ROUTING=1, each engine should:
- Fetch
/reflect, whether or notAWF_REFLECT_ENABLEDis set. - Require
routing.statusto beselected. - Check that
wire_modelis advertised and thatendpointmatches the engine's wire API. - Translate the effort to the engine's own setting.
- Log the same
inference routing: mode=awf-routed model=… effort=…line the Copilot harness logs, so audits can read it.
If any step fails, the engine refuses to start, as the Copilot harness does, and never falls back to a default model.
- pi:
pi_models_json.cjsalready runs inside AWF before the pi CLI and fetches/reflect.- Under routing, it reads the selection and writes the
models.jsonentry forwire_model. The API comes fromendpoint(/v1/messages→anthropic-messages,/responses→openai-responses,/chat/completions→openai-completions) and is cross-checked withresolvePiApiForModel. --modelis currently compiled into the command, so the selected model must reach pi another way. For example, the runtime can write it tosettings.json(whichpi_runtime.cjsalready manages) or to a file the command reads.- Set
GH_AW_PI_MODELsopi_provider.cjsand the drivers (pi_agent_core_driver.cjs,pi_rpc_driver.cjs) see the routed model. Otherwise, reject routing in driver mode. - Write the effort to
settings.defaultThinkingLevel, mappingnonetooff.
- Under routing, it reads the selection and writes the
- Claude: in
claude_harness.cjsmain(), resolve the selection from the/reflectdata it already fetches.- Require
/v1/messages. - Set
ANTHROPIC_MODELinbuildClaudeChildEnv. - Replace any
--modelininitialArgs, asapplyCopilotRoutingArgsdoes, sonormalizeClaudeModelArgscannot restore a fixed model. - Pass the effort with
--effortorCLAUDE_CODE_EFFORT_LEVEL, which accept low, medium, high, xhigh and max.
- Require
- Codex:
codex_harness.cjsmain()resolves the model (normalizeCodexModelArgs,injectModelFlagAfterExec) before its best-effort/reflectfetch. Under routing, fetch first.- Require
/responses. - Inject
wire_modelas the model. - Set
model_reasoning_effortthrough a config override or theconfig.tomlthatcodex_config.cjswrites.
- Require
The router's effort set (none, minimal, low, medium, high, xhigh, max) does not map exactly onto every engine. Claude Code has no none or minimal, and Codex has no max. I suggest mapping only where the meaning is preserved, such as pi's none to off, and failing closed otherwise with the effort named in the error, matching how resolveAWFModelRoutingSelection handles an unsupported effort today. Clamping, or giving the router each engine's supported effort set, are alternatives worth discussing.
4. Keep threat detection unrouted
#66670 clears ModelRouting in resetDetectionEngineTaskSettings and cloneThreatDetectionEngineConfig, and both work for any engine. pi's detection normalizes to Copilot. Add per-engine regression tests asserting that the detection job has no GH_AW_MODEL_ROUTING, apiProxy.routing or router images. The tests should also check that inheritedDetectionModel behaves correctly when engine.model is unset under routing.
5. Docs and tests
- Docs: update
docs/src/content/docs/reference/model-routing.md:- the description and Requirements section, which currently say "Routing requires the Copilot engine";
- per-engine
allowed-modelsconstraints; - a per-engine effort mapping table;
- the routing log line for each engine;
- the threat-detection limitation, now that Model routing: threat detection fails in routed workflows, so safe outputs are applied without a threat verdict #66224 is closed.
- Go tests:
model_routing_test.go: the accepted and rejected engines, incompatible candidates for each engine, and the non-Copilot-provider rejection.- Lock-level tests in
claude_engine_test.go,codex_engine_test.goandpi_engine_test.go: routing flag present, fixed-model env and--modelabsent,apiProxy.routingpresent.
- JS tests:
- The shared selection module, moved from
copilot_harness.test.cjs. claude_harness.test.cjs,codex_harness.test.cjs,pi_models_json.test.cjsandpi_runtime.test.cjs: selection applied; missing, pending or mismatched-endpoint selections fail closed; effort mapping.
- The shared selection module, moved from
- Live validation: rerun t05, t07 and t10 in
githubnext/gh-aw-routing-sandboxwith routing on for pi, Claude and Codex. Confirm the audit's routing section, the selected model and effort in the engine logs, and the credits.
Out of scope
The Copilot CLI's single wire API per BYOK session (github/gh-aw-firewall#9558).
Activity
SivaKesava1 commentedon Oct 8, 2026
Some context from the experiment behind this issue:
Sample size. One run per cell, judged blind by agents (four criteria, 1–5 each, outputs anonymized). Treat it as a strong signal, not a measurement. The full set, at the router's picks:
pi isn't cheaper everywhere: on t06 it used 36 requests where Copilot used 16. The saving is concentrated on the expensive models.
Versions. gh-aw main
303b402810with its default AWF v0.28.44, Copilot CLI 1.0.90, and@earendil-works/pi-coding-agent@1.0.0. pi was pinned withengine.model: copilot/<model>plusengine.config.settings.defaultThinkingLevel. Copilot was pinned withengine.modelplusengine.args: ["--reasoning-effort", …].Wire API. Copilot sending Opus over
/chat/completionscomes from how gh-aw and AWF configure Copilot CLI's BYOK provider for Claude models. It isn't the single-wire-per-session limit itself. That limit matters for sub-agents in the other model family (Make Copilot wire-API routing compatible with subagent tool requests gh-aw-firewall#9558). We haven't shown that/v1/messagesvs/chat/completionsaffects cost. The savings track request count and context size.Sub-agents. These interact with routing on each engine:
explore,research) fall back to the session model under AWF's BYOK mode. A routed Opus pick therefore made them Opus too: an earlier t10 run cost 933 credits with them, against 349 for a run without them. pi has no built-in sub-agents, so a routed pick isn't amplified.## agent:) never reach pi. Activation writes them to.github/agentswhile the artifact and restore use.pi/agents, and pi has no delegation tool in gh-aw (pi engine silently ignores inline sub-agents and their models; agent fakes delegation on the main model #66715). In run 37702323259, pi improvisedpisubprocesses that fell back to Luna, and its output still credited the sub-agents. Until pi engine silently ignores inline sub-agents and their models; agent fakes delegation on the main model #66715 is fixed,warnRoutedSubAgentModelsshould warn that declared sub-agents aren't supported on pi, not just check model compatibility.Logs. Full run artifacts are in a private repo. I can share an archive on request.