Skip to content

Task tool silently downgrades subagent model to session model via multiplier guard (frontmatter and explicit model override both ignored) #3565

@ReefProctor

Description

@ReefProctor

Describe the bug

The Task tool silently downgrades a subagent's requested model to the parent session's model whenever the requested model has a higher cost multiplier than the session model. The downgrade happens both when:

  1. The custom agent file declares model: in its YAML frontmatter, and
  2. The caller passes an explicit "model": "<id>" field in the task tool payload.

Neither path produces a user-visible warning. The only signal is a [INFO] Downgrading subagent model from "<target>" to session model "<session>". line buried in ~/.copilot/logs/process-<pid>.log. Worse, the surfaced metadata for the resulting background agent (via list_agents / read_agent) reports the downgraded model as if it were the one selected, so the orchestrating agent has no reliable way to detect that its model choice was overridden.

Practical impact: a multi-agent setup where a routing/interface agent is pinned to a cheaper model (e.g. gpt-5.4) cannot escalate any individual specialist to a more expensive model (e.g. gpt-5.5, claude-opus-4.7) — every specialist gets clamped back to the routing agent's model. The frontmatter model: field becomes effectively a no-op for any subagent dispatched from a lower-tier parent.

Root cause (from decompiled ~/.cache/copilot/pkg/linux-x64/1.0.55/app.js)

// Task tool, on every subagent spawn:
enforceMultiplierGuard(targetModel) {
  const sessionModel = hp(service.agent.model).model;
  const result = Gbr(targetModel, sessionModel, availableModels);
  if (result !== targetModel)
    log.info(`Downgrading subagent model from "${targetModel}" to session model "${sessionModel}".`);
  return result;
}

function Gbr(t, e, r) {
  const sessionMult = r.find(m => m.id === e)?.multiplier;
  const targetMult  = r.find(m => m.id === t)?.multiplier;
  return (sessionMult !== undefined && targetMult !== undefined && OIe(sessionMult, targetMult))
    ? e   // downgrade to session model
    : t;  // honor requested model
}

function OIe(sessionMult, targetMult) {
  return (sessionMult === 0 && targetMult > 0) || targetMult > sessionMult * fga;
}

So OIe returns true (→ clamp) whenever the requested subagent model costs more than the session model by more than the fga factor. The behavior itself may be intentional cost protection, but its silent, log-only surfacing is the bug.

Affected version

GitHub Copilot CLI 1.0.55.

Also observed under the same code path in 1.0.54.

Steps to reproduce the behavior

Setup:

  1. Create ~/.copilot/agents/parent.agent.md with frontmatter:
    ---
    tools: ["agent", "task"]
    model: gpt-5.4
    description: "Parent agent pinned to a cheaper model."
    ---
  2. Create ~/.copilot/agents/expensive.agent.md with frontmatter:
    ---
    tools: ["read", "execute"]
    model: gpt-5.5
    description: "Specialist that wants gpt-5.5."
    ---

Repro:

  1. Start the CLI: copilot --agent=parent.
  2. From the parent agent, dispatch the specialist with an explicit model override:
    task(
      agent_type="expensive",
      mode="background",
      model="gpt-5.5",
      name="repro",
      prompt="Report which model you are running on."
    )
    
  3. Inspect the agent: list_agents / read_agent(agent_id="repro").

Observed:

  • list_agents and read_agent both report model: gpt-5.4 for the spawned agent, despite:
    • the agent file declaring model: gpt-5.5, and
    • the call site passing "model": "gpt-5.5".
  • ~/.copilot/logs/process-<pid>.log contains a line like:
    [INFO] Downgrading subagent model from "gpt-5.5" to session model "gpt-5.4".
    
  • No warning is surfaced to the orchestrating agent or to the user via the CLI UI / tool result.

If the parent's model: is bumped to gpt-5.5 (or the CLI is launched with --model gpt-5.5), the downgrade stops and the specialist runs on gpt-5.5 as requested. This confirms the clamp is a function of the session model, not of the spawn payload.

Expected behavior

When a subagent declares a model in its agent file frontmatter, or when a caller passes an explicit model in the task tool payload, one of the following should happen:

  1. Preferred: honor the explicitly requested model. Frontmatter and explicit overrides are intentional configuration, not a default to be silently overridden.
  2. If the downgrade is intentional cost protection, surface it visibly:
    • return it in the task tool result so the calling agent can react (e.g. retry, abort, inform the user), and
    • reflect the actually-selected model — not the requested one — in list_agents / read_agent output, with a field indicating the requested vs. effective model and the reason for the difference (e.g. "model_override_reason": "multiplier_guard").
  3. Provide a documented opt-out (env var, CLI flag, or per-agent frontmatter field like allow_higher_cost_subagents: true) for users who knowingly want to escalate a specialist above the parent's tier.

At minimum, the silent log-only behavior is a footgun: agent authors cannot tell that their model: declarations are being ignored.

Additional context

  • OS: Linux (Fedora 41, 6.x kernel), x86_64.
  • Shell: zsh.
  • Reproduces with both background and sync task dispatches.
  • Not the same as Background sub-agent silently hangs at total_turns=0 when model="gpt-5.5" #3547 (which reports a runtime hang on the gpt-5.5 background dispatch path after the guard has let gpt-5.5 through, i.e. when the session model is already high-multiplier). In this issue the guard fires and the agent runs to completion normally — just on the wrong model.

Relevant log line to grep for when reproducing:

grep -F "Downgrading subagent model" ~/.copilot/logs/process-*.log

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:agentsSub-agents, fleet, autopilot, plan mode, background agents, and custom agentsarea:modelsModel selection, availability, switching, rate limits, and model-specific behavior

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions