Repository navigation
Description
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.
- 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 withNo GitHub OAuth token or Copilot HMAC key provided. When routing is off, the harness adds thecopilot/prefix and SDK mode works under org billing (smoke-copilot-sub-agents usescopilot-requests: writeand 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/reflectare also bare, so those paths fail the same way. - Declared sub-agents are never loaded. The session config sets neither
customAgentsnorenableConfigDiscovery, and the SDK's default forenableConfigDiscoveryis false. The inline agents that gh-aw writes to.github/agents/*.agent.mdare therefore ignored. When the prompt asks for delegation, the main agent quietly runs the built-ingeneral-purposeagent on its own model. This is why smoke-copilot-sub-agents has never exercised its Haiku agent. - 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. - Every Copilot model shares one wire API.
resolveMultiProviderFromReflectbuilds one SDK provider per AWF endpoint with a singlewireApi, 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: copilotwithcopilot-sdk: true- Copilot CLI 1.0.90 headless sidecar,
@github/copilot-sdk1.0.16, AWF v0.28.50 - Org billing (
permissions: copilot-requests: write) model-routingon, withallowed-models: [gpt-5.6-luna]- The workflow declares an inline sub-agent
file-summarizerwithmodel: 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=responsesandsdk-mode(multi): resolved provider="copilot" ... wireApi="responses" ... baseUrl="http://api-proxy:10002". token-usage.jsonlcontains only the routing classifier request (purpose=routing_classification, gpt-5.6-luna/responses200). 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 4model.call_startevents and 4 connections toapi-proxy, with no direct Copilot host. These match 4/responsesrequests intoken-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 asnot_invokedwithOverall status: FAILand an unrendered variant name, yet the run was green. The same unrendered prompt appears in 37550743356 and 37246639853. - The
nano-whoamiagent declaresgpt-5-nano, which the Copilot endpoint does not offer in that run's/reflectmodel list.
Root cause in the code (at 9e12379)
copilot_harness.cjs. Thecopilot/prefix is applied only when there is nomodelRoutingSelection. That guard was added in Support AWF task-level model routing in the Copilot engine #64593, so a routedCOPILOT_MODELreaches the SDK driver bare. The harness also overwrites the primary provider'swireApiwith the routing endpoint, which forces every model on that provider onto one wire API.copilot_sdk_driver.cjsmain. The driver chooses the routedCOPILOT_MODEL, then theapplyModelFallbackresult, thenmultiProviderConfig.model. Nothing qualifies any of the three.copilot_sdk_session.cjsrunWithCopilotSDK. The session is created with that model, falling back toresolveModelWithFallback, with nocustomAgentsand noenableConfigDiscovery.awf_reflect.cjsresolveMultiProviderFromReflect. It builds one provider per endpoint with a singlewireApi, even thoughrouting_models[].supported_endpointsgives the wire APIs for each model. That data is present in/reflectwhether or not routing is on.model_fallback.cjs.GH_AW_MODEL_FALLBACKis 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
/reflectrouting_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 routedCOPILOT_MODEL, theGH_AW_MODEL_FALLBACKmodel 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 ascustomAgents. 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 assmalleither resolves to a concrete catalog model or leaves the agent on the session model. An unknown model produces a clear warning instead of a silentgeneral-purposefallback. - 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 examplecopilot-completions/claude-haiku-4.5) to the served model. model.call_final_resultis captured. Persisting it, withisByokand 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 existinggetCatalogModelEntry,inferWireApiForModelandendpointBaseUrlhelpers.actions/setup/js/copilot_harness.cjs: the model qualification guard and the primary-providerwireApioverride near theresolveMultiProviderFromReflectcall, andbuildCopilotSDKChildEnv.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: therunWithCopilotSDKsession config (qualified model,customAgents), plusCAPTURED_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 ofresolveModelWithFallbackandapplyModelFallbackin SDK mode.actions/setup/js/extract_inline_sub_agents.cjs: reuse its agent parsing to buildcustomAgentsfrom the files thatinterpolate_prompt.cjswrites.actions/setup/js/copilot_workflow_events.cjsand the unified session writer: keep thesubagent.*events and their models inusage/aw_session.jsonl.pkg/cli/token_usage_subagent_session.go,token_usage_subagent.goandmodel_identity.go: attribution with provider-qualified sub-agent models. Add a fixture next topkg/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/reflectfixture that containsrouting_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.cjsand the session tests that the session model is never bare and that declared agents arrive ascustomAgentswith qualified models. - smoke-copilot-sub-agents should assert from
usage/aw_session.jsonlandtoken-usage.jsonlthat 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).
Activity