Skip to content

Copilot SDK mode: routed model passed unqualified, declared sub-agents never loaded, and one wire API shared by every Copilot model #67507

Description

@SivaKesava1

Problem

In Copilot SDK mode (engine.copilot-sdk: true), the session that gh-aw builds for the Copilot runtime has four gaps. Together they mean a routed workflow under org billing fails outright, and a declared sub-agent never runs on its own model.

  1. The session model is passed unqualified. With BYOK providers configured, the Copilot runtime only uses a provider when the model is written as <provider>/<model>. A bare name falls back to Copilot's built-in inference. Inside AWF that path has no token, so the session fails before any model call with No GitHub OAuth token or Copilot HMAC key provided. When routing is off, the harness adds the copilot/ prefix and SDK mode works under org billing (smoke-copilot-sub-agents uses copilot-requests: write and passes). The failure happens whenever AWF model routing supplies the model, because the prefix is skipped when a routing selection exists. The fallback model and the model taken from /reflect are also bare, so those paths fail the same way.
  2. Declared sub-agents are never loaded. The session config sets neither customAgents nor enableConfigDiscovery, and the SDK's default for enableConfigDiscovery is false. The inline agents that gh-aw writes to .github/agents/*.agent.md are therefore ignored. When the prompt asks for delegation, the main agent quietly runs the built-in general-purpose agent on its own model. This is why smoke-copilot-sub-agents has never exercised its Haiku agent.
  3. Bare agent models are rejected. If config discovery is turned on, the agent file loads, but its frontmatter model (claude-haiku-4.5) is rejected because only provider-qualified names are offered.
  4. Every Copilot model shares one wire API. resolveMultiProviderFromReflect builds one SDK provider per AWF endpoint with a single wireApi, which is inferred from the configured model or overridden by the routing selection. A Claude sub-agent under a GPT main model is then sent on /responses, and AWF rejects it because that model only supports /chat/completions.

Setup

  • gh-aw main 9e123795f5, compiled with --gh-aw-ref
  • engine: copilot with copilot-sdk: true
  • Copilot CLI 1.0.90 headless sidecar, @github/copilot-sdk 1.0.16, AWF v0.28.50
  • Org billing (permissions: copilot-requests: write)
  • model-routing on, with allowed-models: [gpt-5.6-luna]
  • The workflow declares an inline sub-agent file-summarizer with model: claude-haiku-4.5, and the task asks the main agent to delegate to it.

The custom-driver runs use .github/drivers/xfam_sdk_driver.cjs in the sandbox repo. It calls gh-aw's own runWithCopilotSDK and changes only the providers, models, session model and customAgents before the session starts. Its [xfam] lines in agent-stdio.log record every model call (isByok, model, result) and every sub-agent event. A green agent step does not show that the sub-agent worked. In several runs the main agent recovered by doing the work itself or by switching the sub-agent to another model. The evidence below therefore comes from the [xfam] lines and from sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl.

The "Ingest agent output" failure in all of these runs is unrelated. It is the separate memory_schema_contract.cjs setup regression on main.

Evidence

Stock gh-aw SDK driver fails: 38077047915

This is the only run where "Execute GitHub Copilot CLI" fails. All four attempts fail about 200 ms after session.start:

[copilot-sdk-driver] [sdk-driver] creating session with model="gpt-5.6-luna" providers=1 models=4
session.error ... "Execution failed: InvalidArg, No GitHub OAuth token or Copilot HMAC key provided"
  • The CLI process log shows Received session.create request: {"model":"gpt-5.6-luna", ...}, with no provider prefix.
  • The harness had resolved the provider correctly: inference routing: mode=awf-routed model=gpt-5.6-luna effort=xhigh wire_api=responses and sdk-mode(multi): resolved provider="copilot" ... wireApi="responses" ... baseUrl="http://api-proxy:10002".
  • token-usage.jsonl contains only the routing classifier request (purpose=routing_classification, gpt-5.6-luna /responses 200). No agent request reached the api-proxy.

Custom driver experiments (same workflow, only the provider configuration changes)

Run Configuration What happened
38078591666 (control) Main model prefixed (copilot/gpt-5.6-luna), one provider copilot:responses, customAgents=0 The session works, but no declared agent exists: subagent.started agent=general-purpose model=copilot/gpt-5.6-luna. Every request is gpt-5.6-luna /responses.
38078594175 (split) Two providers, copilot-responses:responses and copilot-completions:completions, customAgents=0 Same as control: general-purpose on copilot-responses/gpt-5.6-luna, and Haiku never runs.
38079427704 (discovery) One provider, enableConfigDiscovery=true The agent file loads. Its bare model is rejected, and the qualified Haiku model then fails on the wrong wire API. The main agent retries file-summarizer on copilot/gpt-5-mini.
38079430608 (split + discovery) Split providers, enableConfigDiscovery=true Same rejection of the bare model. The main agent runs file-summarizer on copilot-responses/gpt-5-mini instead of Haiku.
38078597395 (split + customAgents) Split providers, models mapped by supported_endpoints, declared agents passed as customAgents Works. file-summarizer runs on Haiku over /chat/completions.

In the discovery run, the bare model is rejected. The listed models are also duplicated:

Model 'claude-haiku-4.5' is not available. Available models: copilot/claude-haiku-4.5, copilot/gpt-5-mini, copilot/gpt-5.4-mini, copilot/gpt-5.6-luna, copilot/claude-haiku-4.5, ...

The qualified Haiku model is then sent with the session's single wire API:

[xfam] subagent.started agent=file-summarizer model=copilot/claude-haiku-4.5
[xfam] model.call_final_result model=claude-haiku-4.5 isByok=true result=http_400
[xfam] session.error ... 400 Cannot translate Copilot request feature 'include' between Responses and Chat Completions. Routing model "claude-haiku-4.5" to /chat/completions is incompatible: this request needs /responses to preserve 'include'
[xfam] subagent.failed agent=file-summarizer model=copilot/claude-haiku-4.5
[xfam] subagent.started agent=file-summarizer model=copilot/gpt-5-mini

In the working run, the driver maps each model from /reflect routing_models[].supported_endpoints:

[xfam] model claude-haiku-4.5: endpoints=["/chat/completions","/v1/messages"] -> provider copilot-completions
[xfam] model gpt-5-mini: endpoints=["/chat/completions","/responses","ws:/responses"] -> provider copilot-responses
[xfam] model gpt-5.6-luna: endpoints=["/responses","ws:/responses"] -> provider copilot-responses
[xfam] createSession model=copilot-responses/gpt-5.6-luna providers=["copilot-responses:responses","copilot-completions:completions"] customAgents=2
[xfam] subagent.started agent=file-summarizer model=copilot-completions/claude-haiku-4.5
[xfam] model.call_final_result model=claude-haiku-4.5 isByok=true result=success   (x2)
[xfam] subagent.completed agent=file-summarizer

AWF's token-usage.jsonl for the same session shows claude-haiku-4.5 on /chat/completions (200, twice) and gpt-5.6-luna on /responses (200). The run's usage/aw_session.jsonl already contains subagent.started, subagent.configured, subagent.selected and subagent.completed events with provider-qualified models, plus firewall.token_usage per model and path.

The second declared agent, quick-checker, has model: small. That is an alias, not a catalog model, so the driver left it on the session model.

gh-aw's own smoke runs

They do not silently bypass the api-proxy.

  • smoke-copilot-sdk 37862583327 uses a secret token and model: copilot/gpt-5.4. The CLI process log has 4 model.call_start events and 4 connections to api-proxy, with no direct Copilot host. These match 4 /responses requests in token-usage.jsonl.
  • smoke-copilot-sub-agents 37863614500 uses org billing with session model copilot/gpt-5.3-codex. It has 3 calls, 3 api-proxy connections and 3 matching requests. Runs 37550743356 and 37246639853 look the same.

Both smoke tests pass because their model is provider-qualified and routing is off. Neither one exercises routing, an unqualified model or a real sub-agent.

The stock driver does not persist model.call_final_result, because CAPTURED_EPHEMERAL_EVENTS adds only model.call_failure. As a result, isByok cannot be read from stock artifacts.

smoke-copilot-sub-agents does not test sub-agents. In 37863614500, the requests are all gpt-5.3-codex and no sub-agent was invoked. Its checks compare the agents' text replies, which any model can produce. It also has two problems that should be tracked separately and should not block this fix:

  • The rendered prompt contains every {{#if experiments.sub_agent_strategy == ...}} block, unevaluated. The agent followed the first block ("Do not call any sub-agent"). Its own report listed all agents as not_invoked with Overall status: FAIL and an unrendered variant name, yet the run was green. The same unrendered prompt appears in 37550743356 and 37246639853.
  • The nano-whoami agent declares gpt-5-nano, which the Copilot endpoint does not offer in that run's /reflect model list.

Root cause in the code (at 9e12379)

  • copilot_harness.cjs. The copilot/ prefix is applied only when there is no modelRoutingSelection. That guard was added in Support AWF task-level model routing in the Copilot engine #64593, so a routed COPILOT_MODEL reaches the SDK driver bare. The harness also overwrites the primary provider's wireApi with the routing endpoint, which forces every model on that provider onto one wire API.
  • copilot_sdk_driver.cjs main. The driver chooses the routed COPILOT_MODEL, then the applyModelFallback result, then multiProviderConfig.model. Nothing qualifies any of the three.
  • copilot_sdk_session.cjs runWithCopilotSDK. The session is created with that model, falling back to resolveModelWithFallback, with no customAgents and no enableConfigDiscovery.
  • awf_reflect.cjs resolveMultiProviderFromReflect. It builds one provider per endpoint with a single wireApi, even though routing_models[].supported_endpoints gives the wire APIs for each model. That data is present in /reflect whether or not routing is on.
  • model_fallback.cjs. GH_AW_MODEL_FALLBACK is a bare model name.

Intended behavior

  • One SDK provider per wire API for the Copilot endpoint. Build a responses provider and a completions provider, both pointing at the same api-proxy base URL. Map each Copilot model to one of them using /reflect routing_models[].supported_endpoints.
    • A model that supports both wire APIs keeps today's inference.
    • The routed model goes on the wire API from the routing selection.
    • Other endpoints (openai, anthropic) stay as they are.
  • Provider-qualified models everywhere. The session model is passed as <provider>/<model>, including the routed COPILOT_MODEL, the GH_AW_MODEL_FALLBACK model and the model taken from /reflect. A bare model can no longer fall through to built-in inference inside AWF.
  • Declared sub-agents are loaded. The workflow's sub-agents (inline agents and .github/agents/*.agent.md) are passed to the session as customAgents. Name, description, prompt and tools come from the agent files, and each agent's model is provider-qualified with the same mapping. A model alias such as small either resolves to a concrete catalog model or leaves the agent on the session model. An unknown model produces a clear warning instead of a silent general-purpose fallback.
  • Sub-agent activity is recorded. Sub-agent model, usage and outcome events stay in usage/aw_session.jsonl, the unified session file. Audit and logs must attribute sub-agent spend to the sub-agent's model, which means normalizing the new provider-qualified names (for example copilot-completions/claude-haiku-4.5) to the served model.
  • model.call_final_result is captured. Persisting it, with isByok and the result, in the stock driver's captured events would make this class of problem visible from the standard artifacts.

Files and functions that change

  • actions/setup/js/awf_reflect.cjs: resolveMultiProviderFromReflect (per-wire-API Copilot providers and per-model mapping), using the existing getCatalogModelEntry, inferWireApiForModel and endpointBaseUrl helpers.
  • actions/setup/js/copilot_harness.cjs: the model qualification guard and the primary-provider wireApi override near the resolveMultiProviderFromReflect call, and buildCopilotSDKChildEnv.
  • actions/setup/js/copilot_sdk_driver.cjs: main, so that the routed, fallback and reflect models are qualified.
  • actions/setup/js/copilot_sdk_session.cjs: the runWithCopilotSDK session config (qualified model, customAgents), plus CAPTURED_EPHEMERAL_EVENTS.
  • actions/setup/js/copilot_sdk_multi_provider.cjs: parseMultiProviderJson, if the multi-provider shape needs to carry the per-model provider.
  • actions/setup/js/model_fallback.cjs: callers of resolveModelWithFallback and applyModelFallback in SDK mode.
  • actions/setup/js/extract_inline_sub_agents.cjs: reuse its agent parsing to build customAgents from the files that interpolate_prompt.cjs writes.
  • actions/setup/js/copilot_workflow_events.cjs and the unified session writer: keep the subagent.* events and their models in usage/aw_session.jsonl.
  • pkg/cli/token_usage_subagent_session.go, token_usage_subagent.go and model_identity.go: attribution with provider-qualified sub-agent models. Add a fixture next to pkg/cli/testdata/subagent_attribution/copilot-cross-family/ that uses qualified names.
  • .github/workflows/smoke-copilot-sub-agents.md: the assertions (see Tests).

Tests

  • Unit tests in awf_reflect.test.cjs, driven by a /reflect fixture that contains routing_models[].supported_endpoints, for:
    • the provider split
    • per-model provider mapping
    • routed-model and fallback-model qualification
    • alias handling
  • Unit tests in copilot_sdk_driver.test.cjs and the session tests that the session model is never bare and that declared agents arrive as customAgents with qualified models.
  • smoke-copilot-sub-agents should assert from usage/aw_session.jsonl and token-usage.jsonl that each declared sub-agent ran on its own model and wire API. Checking only that the run passed, or that the reply text names a model, is not enough.

Acceptance

In SDK mode under org billing, two workflows complete successfully: one with a GPT main model and a declared Claude sub-agent, and one the other way round. In both:

  • The sub-agent's requests go through the api-proxy on its own model and wire API.
  • Nothing falls back to the main model.
  • Audit shows the sub-agent's model.

Scope

This issue covers SDK mode only. CLI mode (the default) is not covered and still depends on github/copilot-cli#5103 (BYOK sub-agents always use the session's wire API).

Related: #67485 (CLI-mode wire API selection from AWF endpoint data) and #67460 (haiku sub-agent 'include' failure).

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions