Repository navigation
Copilot CLI model-selection persistence aborts routed attribution and token usage #69965
Description
Problem
Routed Copilot CLI runs execute on the router-selected model, but the runner's post-step rejects their model-selection log and skips both routing attribution and token-usage accounting. This is a runtime metadata regression, not a failure to select or enforce a route.
The affected path is shared by routed Copilot CLI workflows, not just the smoke tests: an initially unset COPILOT_MODEL becomes a concrete model after routing, but the observer retains the earlier empty configured value. The non-routed environment-fallback path has the same defect.
Verified run evidence
The six supplied runs were inspected: three failing CLI runs on main, the failing CLI run for #69848, and two passing controls. Dates below are UTC. The four October 18 main runs use eb42b26807ac37c20f0a43ba3beba68375ab43e1.
| Workflow / comparison | Run | Result and evidence |
|---|---|---|
smoke-copilot-routed |
39147795806, October 18, 2026, workflow_dispatch |
Post-step reports the selection-validation error; R1 fails; R2-R4 pass. The selected model is gpt-5.6-luna on /responses, with all four main-agent requests on allowed models. |
smoke-copilot-routed-claude |
39148386697, October 18, 2026, workflow_dispatch |
Same post-step error and R1 failure; R2-R4 pass. The selected model is claude-sonnet-5 on /chat/completions, with all three main-agent requests on allowed models. |
smoke-copilot-routed-steering |
39148388768, October 18, 2026, workflow_dispatch |
Same post-step error and R1 failure; R2-R4 pass. The selected model is gpt-5.6-luna on /responses, with all 95 main-agent requests on allowed models. |
Same-commit SDK control: smoke-copilot-sdk-routed |
39147798082, October 18, 2026, workflow_dispatch |
Passes, including R1-R4. The token-usage summary is written and runner metadata records the selected route. |
Last successful smoke-copilot-routed run on main at investigation time |
38984582061, October 17, 2026, schedule, commit 354b5c3423ea95046f4db235f9fea7bb2885aaa8 |
Passes R1-R4 and writes the token-usage summary. This is a scheduled baseline, unlike the dispatched October 18 runs. |
#69848 branch: smoke-copilot-routed |
39147289566, October 18, 2026, workflow_dispatch, commit d5581f929d571c41b5d4db343f4699d4f542f5cd |
Same selection-validation error and R1 failure, while R2-R4 pass. This regression blocks verification of that PR; it is not evidence that its ranked-choice changes caused R1 to fail. |
In each failing CLI run, the agent's Parse token usage for step summary step reports ERR_PARSE: Cannot record Copilot model selection: Invalid Copilot model selection. The later smoke assertion reports runner-written routing status is missing, expected selected. The parse step is configured with continue-on-error; the smoke assertion is the job-failing boundary.
The downloaded agent artifact from run 39147795806 contains:
sandbox/agent/logs/copilot-model-selection.json:statusisobserved,configured_modelis the empty string, and bothresolved_modelandeffective_modelaregpt-5.6-luna. The other two failingmainartifacts have the same empty configured value, with their selected model in the resolved/effective fields.- Both
agent/aw_info.jsonandruntime/aw_info.json:engine_idiscopilot,modelremainsauto, and neithermodel_routingnormodel_selectionis present. sandbox/firewall/logs/api-proxy-logs/model-routing.jsonl: realmodel-routing/v0.28.62selection and successful request records corroborate the selected model and endpoint. The token-usage log also exists, but the agent's aggregatedagent_usage.jsonis absent.
The workflow.info event in usage/aw_session.jsonl has data.model and data.requestedModel equal to auto, but no data.modelRouting or data.modelSelection. Its workflow provenance points to runtime/aw_info.json. In the same-commit SDK control, the corresponding event has model equal to gpt-5.6-luna and modelRouting.status equal to selected; its runner metadata and token-usage aggregate are populated.
Root cause and regression
The relevant source was checked against the failing revision, not inferred only from the smoke assertion.
- In
copilot_harness.cjs,main(),configuredCopilotModelcapturesCOPILOT_MODELbefore routing and beforeapplyModelFallback. The routed agent job has noCOPILOT_MODEL, so the captured value is empty. resolveCopilotModelRoutingcallsapplyCopilotRoutingSelection, which setsCOPILOT_MODELto the selected wire model. The laterverifyModelcheck and observer construction therefore enable CLI observation, but pass the stale empty configured value tocreateCopilotModelObserver. Its observed record has correct resolved/effective models and an emptyconfigured_model.persistCopilotModelSelectioncorrectly rejects an empty configured identifier. Inparse_token_usage.cjs,main(), persistence runs first in the same error boundary asrecordModelRoutingFromArtifactsand token accounting. Its throw skips both operations. The final snapshot still runs, but copies metadata without the missing attribution.- SDK mode does not enable this CLI observer, so it produces no such invalid selection log and the post-step proceeds normally.
The new rejecting persistence function and its position before routing were introduced by commit c29fcddb1aa4b0161ebecbc7e7efec86da01e5a0, "Keep Copilot model metadata writes on the runner", within #69162. The PR's merged title is "Harden Copilot CLI and Cloud Hypervisor compatibility"; it merged on October 17, 2026, at 19:33:35 UTC. Previously, the post-step began with routing attribution rather than this new failure point.
Fallback is also confirmed locally, not merely suspected. With routing disabled, no COPILOT_MODEL or model argument, and GH_AW_MODEL_FALLBACK set to gpt-5.6-luna, an offline invocation of the real harness with a local CLI test double applies the fallback and records the same empty configured value. The real runner persistence function then rejects it. No hosted fallback run was inspected.
An additional offline replay used the unchanged selection, workflow metadata, routing, outcome, and token-usage records from run 39147795806. The real post-step reported the same error, wrote no token summary or aggregate, and left model as auto. Calling recordModelRoutingFromArtifacts independently on those records succeeded with status selected and wire model gpt-5.6-luna. No workflows were dispatched for this investigation.
Intended behavior and possible solution
For these routed runs, define configured_model as the requested dynamic-routing input, auto, consistent with the original workflow metadata. resolved_model is the router-selected concrete model; effective_model is the model observed for the main CLI invocation. For the representative run those latter fields are both gpt-5.6-luna. This is a metadata meaning for auto, not an instruction to use Copilot's native auto picker: actual CLI requests must continue using the router-selected model.
For non-routed CLI runs, retain the explicit configured model or alias before resolution. When only the environment fallback supplies the model, configured_model must instead be that fallback value, never the earlier empty environment value.
The focused fix should:
- Resolve the source of the requested/configured model before freezing the value used by the observer. Preserve the distinction between the requested identifier and the resolved wire model; do not blindly recapture the environment after alias/routing resolution.
- Isolate model-selection persistence errors from routing attribution and token accounting. Retain a visible warning or step failure with the original selection error, but still attempt
recordModelRoutingFromArtifacts, the token-usage summary/aggregate, outputs, and the final runner metadata snapshot. Invalid selection evidence must remain invalid, not become a synthetic successful selection. - Preserve runner-only workflow metadata writes, validated selection fields, exact-session observation, and proxy-corroborated routing attribution. Do not restore sandbox metadata writes, relax validation to accept an empty configured model, or weaken R1 to trust the agent-written routing outcome.
Acceptance cases and covering tests
The test names below are proposed regressions to add or existing coverage to retain, not claims that the missing cases already exist.
| Case | Required result | Covering tests |
|---|---|---|
Routed CLI, initially unset COPILOT_MODEL, router selects gpt-5.6-luna |
The harness writes configured_model: "auto" with the real observed resolved/effective model. Persistence succeeds. Runner aw_info.json has model_routing.status: "selected", model_selection.effective_model: "gpt-5.6-luna", and model: "gpt-5.6-luna". The workflow.info event has the corresponding normalized data.modelRouting and data.modelSelection, with the same effective model. |
Add a copilot_harness.test.cjs regression that exercises harness startup through observation with routing enabled and the model environment unset, not only the routing helper. Add fixture-backed parse_token_usage.test.cjs coverage for successful persistence/attribution and unified_session.test.cjs coverage for the final runner snapshot and normalized event. |
| Invalid or empty selection log on a routed run | The selection error remains visible, but runner metadata still receives the independently verified model_routing and the token-usage summary and aggregate are still written. Invalid selection metadata is not persisted. |
Add a parameterized parse_token_usage.test.cjs regression, "records routing and usage when Copilot model selection persistence fails", including the real empty-configured_model record, an empty file, and malformed JSON. Assert the diagnostic, routing fields, summary write, and usage output, rather than just the absence of a throw. |
Non-routed CLI with an explicit model: |
Preserve the configured model/alias and existing observed / substituted behavior, including rejection of unauthorized substitution. |
Retain and extend copilot_model_selection.test.cjs explicit-model, alias, and startup-substitution tests, plus the native invocation-model coverage. |
Non-routed fallback, no model: and initially unset COPILOT_MODEL |
With GH_AW_MODEL_FALLBACK set, configured_model is the fallback model, not empty; resolved/effective values and runner persistence remain correct. |
Add a copilot_harness.test.cjs regression, "records the fallback configured model when COPILOT_MODEL is unset", exercising the actual startup order and then persistCopilotModelSelection. Include existing alias-resolution behavior if the fallback is an alias. |
| SDK mode, routed and unrouted | No change to SDK execution or attribution; do not introduce the CLI observer into SDK mode. | Retain routed/unrouted SDK coverage in copilot_harness.test.cjs and the post-step routing tests, and explicitly assert that SDK mode does not create this CLI selection record. |
| End to end on the implementation PR branch | smoke-copilot-routed, smoke-copilot-routed-claude, and smoke-copilot-routed-steering pass R1 again, while R2-R4 remain valid and smoke-copilot-sdk-routed still passes. |
Use those four smoke workflows on the reviewed PR branch as the hosted acceptance tests; record the tested commit and run links. These are future acceptance runs, not runs performed for this issue. |
Fixture provenance is required: build the regression fixtures from sandbox/agent/logs/copilot-model-selection.json and agent/aw_info.json / runtime/aw_info.json in the real agent artifact of run 39147795806. Use that artifact's real routing and token-usage records for the post-step test as needed. Preserve the actual field names, types, absent fields, and empty configured value; do not substitute invented record shapes or older routing-schema examples. Document any redaction of unrelated metadata.
The implementation PR must carry the smoke-routing label.
Scope
- Keep the PR to this one fix. Do not change files or behavior unrelated to this model-selection/runner-accounting regression and its directly related tests or documentation.
- Before fixing a failing check, compare it with the same check on
main. If it also fails onmain, do not fix it in this PR; mergemainoncemainis fixed, and note in the PR which checks are known failures onmain. - This applies even if a bot comment asks to fix a failing check: fix only failures caused by this PR's changes.
Activity