Skip to content

Commit d202156

Browse files
Copilotpelikhan
andauthored
Align Pi provider wiring with AWF spec and correct provider metadata for multi-provider engines (#31758)
* Plan pi provider compliance fixes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Fix pi provider setup and metadata Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Validate pi provider integration Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Polish pi provider test comments Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore: start addressing PR feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Update dev workflow to Pi with GitHub provider Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 6d841f5 commit d202156

17 files changed

Lines changed: 313 additions & 44 deletions

.github/workflows/dev.lock.yml

Lines changed: 15 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/dev.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ description: Daily status report for gh-aw project
99
timeout-minutes: 30
1010
strict: false
1111
engine:
12-
id: pi
13-
model: copilot/claude-sonnet-4-20250514
12+
runtime:
13+
id: pi
14+
provider:
15+
id: github
16+
model: claude-sonnet-4-20250514
1417

1518
permissions:
1619
contents: read

.github/workflows/smoke-pi.lock.yml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/pi_provider.cjs

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
/**
44
* Pi Provider Extension for gh-aw
55
*
6-
* Calls the AWF API proxy /reflect endpoint at session start to dynamically
7-
* discover the open LLM inference paths configured for this run. This gives
8-
* operators runtime visibility into which provider/model combination is active
9-
* and verifies that the expected gateway port is reachable before the agent
10-
* starts working.
6+
* Registers Pi providers from the AWF-injected environment and calls the AWF
7+
* API proxy /reflect endpoint at session start to dynamically discover the
8+
* open LLM inference paths configured for this run. This gives operators
9+
* runtime visibility into which provider/model combination is active and
10+
* verifies that the expected gateway port is reachable before the agent starts
11+
* working.
1112
*
1213
* When the model uses provider/model format (e.g. "copilot/claude-sonnet-4"),
1314
* the extension logs the matched endpoint so failures can be diagnosed without
@@ -18,7 +19,10 @@
1819
* configuration is required.
1920
*
2021
* Configuration (read from environment variables):
21-
* PI_MODEL The engine.model value; may be "provider/model" or bare "model".
22+
* GH_AW_PI_MODEL The original engine.model value; may be "provider/model"
23+
* or bare "model". Preferred over PI_MODEL so gh-aw can pass
24+
* model context to extensions without changing Pi CLI behavior.
25+
* PI_MODEL Legacy fallback used when GH_AW_PI_MODEL is not set.
2226
*/
2327

2428
"use strict";
@@ -29,6 +33,18 @@ const { fetchAWFReflect, AWF_API_PROXY_REFLECT_URL, AWF_REFLECT_OUTPUT_PATH, AWF
2933
// prettier-ignore
3034
const DEFAULT_LOGGER = /** @type {(msg: string) => void} */ (msg => process.stderr.write(`[gh-aw/pi-provider] ${new Date().toISOString()} ${msg}\n`));
3135

36+
/**
37+
* Return the workflow-configured model string exposed to Pi extensions.
38+
* GH_AW_PI_MODEL takes precedence because gh-aw sets it explicitly for extensions
39+
* while continuing to pass the CLI model via --model. PI_MODEL remains a legacy
40+
* fallback for older callers.
41+
*
42+
* @returns {string}
43+
*/
44+
function getConfiguredModel() {
45+
return process.env.GH_AW_PI_MODEL || process.env.PI_MODEL || "";
46+
}
47+
3248
/**
3349
* Extract the provider prefix from a "provider/model" string.
3450
* Returns an empty string when no slash is present (bare model name).
@@ -66,14 +82,91 @@ function resolveGatewayUrl(provider) {
6682
return `http://api-proxy:${port}`;
6783
}
6884

85+
/**
86+
* Register a Pi provider and any aliases.
87+
*
88+
* @param {any} pi
89+
* @param {string[]} names
90+
* @param {Record<string, any>} config
91+
* @param {(msg: string) => void} logger
92+
*/
93+
function registerProviderAliases(pi, names, config, logger) {
94+
for (const name of names) {
95+
pi.registerProvider(name, config);
96+
logger(`registered provider=${name}`);
97+
}
98+
}
99+
100+
/**
101+
* Register all supported Pi providers discovered from the environment.
102+
*
103+
* @param {any} pi
104+
* @param {(msg: string) => void} logger
105+
* @returns {number}
106+
*/
107+
function registerConfiguredProviders(pi, logger) {
108+
let registeredCount = 0;
109+
110+
const copilotToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
111+
if (copilotToken) {
112+
registerProviderAliases(
113+
pi,
114+
["github-copilot", "copilot"],
115+
{
116+
apiKey: copilotToken,
117+
api: "openai-completions",
118+
...(process.env.GITHUB_COPILOT_BASE_URL ? { baseUrl: process.env.GITHUB_COPILOT_BASE_URL } : {}),
119+
},
120+
logger
121+
);
122+
registeredCount += 2;
123+
}
124+
125+
if (process.env.ANTHROPIC_API_KEY) {
126+
registerProviderAliases(
127+
pi,
128+
["anthropic"],
129+
{
130+
apiKey: process.env.ANTHROPIC_API_KEY,
131+
api: "anthropic",
132+
...(process.env.ANTHROPIC_BASE_URL ? { baseUrl: process.env.ANTHROPIC_BASE_URL } : {}),
133+
},
134+
logger
135+
);
136+
registeredCount += 1;
137+
}
138+
139+
const openAIKey = process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY;
140+
if (openAIKey) {
141+
registerProviderAliases(
142+
pi,
143+
["openai", "codex"],
144+
{
145+
apiKey: openAIKey,
146+
api: "openai-completions",
147+
...(process.env.OPENAI_BASE_URL ? { baseUrl: process.env.OPENAI_BASE_URL } : {}),
148+
},
149+
logger
150+
);
151+
registeredCount += 2;
152+
}
153+
154+
if (registeredCount === 0) {
155+
logger("no provider credentials detected for Pi provider registration");
156+
}
157+
158+
return registeredCount;
159+
}
160+
69161
/**
70162
* Pi provider extension for gh-aw.
71163
*
72-
* Subscribes to the `agent_start` and `agent_end` Pi SDK events and calls the AWF /reflect
73-
* endpoint to discover and log the open LLM inference paths before the agent begins its
74-
* first turn and again after it finishes. The post-run fetch is the authoritative snapshot
75-
* used by the step summary; the pre-run fetch captures the initial proxy state for diagnostics
76-
* in case the session exits unexpectedly before reaching `agent_end`.
164+
* Registers providers immediately, then subscribes to the `agent_start` and `agent_end`
165+
* Pi SDK events and calls the AWF /reflect endpoint to discover and log the open LLM
166+
* inference paths before the agent begins its first turn and again after it finishes.
167+
* The post-run fetch is the authoritative snapshot used by the step summary; the pre-run
168+
* fetch captures the initial proxy state for diagnostics in case the session exits
169+
* unexpectedly before reaching `agent_end`.
77170
* Both calls are best-effort: any network or parse error is logged but does not abort the
78171
* agent session.
79172
*
@@ -82,9 +175,10 @@ function resolveGatewayUrl(provider) {
82175
*/
83176
function piProviderExtension(pi) {
84177
const log = DEFAULT_LOGGER;
178+
registerConfiguredProviders(pi, log);
85179

86180
pi.on("agent_start", async () => {
87-
const model = process.env.PI_MODEL || "";
181+
const model = getConfiguredModel();
88182
const provider = extractProviderFromModel(model);
89183

90184
if (provider) {
@@ -123,5 +217,7 @@ function piProviderExtension(pi) {
123217
}
124218

125219
module.exports = piProviderExtension;
220+
module.exports.getConfiguredModel = getConfiguredModel;
126221
module.exports.extractProviderFromModel = extractProviderFromModel;
127222
module.exports.resolveGatewayUrl = resolveGatewayUrl;
223+
module.exports.registerConfiguredProviders = registerConfiguredProviders;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
describe("pi_provider.cjs", () => {
4+
let module;
5+
let originalEnv;
6+
let originalFetch;
7+
let stderrOutput;
8+
9+
beforeEach(async () => {
10+
originalEnv = { ...process.env };
11+
originalFetch = global.fetch;
12+
stderrOutput = [];
13+
vi.spyOn(process.stderr, "write").mockImplementation(msg => {
14+
stderrOutput.push(String(msg));
15+
return true;
16+
});
17+
module = await import("./pi_provider.cjs?" + Date.now());
18+
});
19+
20+
afterEach(() => {
21+
process.env = originalEnv;
22+
global.fetch = originalFetch;
23+
vi.restoreAllMocks();
24+
});
25+
26+
it("prefers GH_AW_PI_MODEL over PI_MODEL", () => {
27+
process.env.GH_AW_PI_MODEL = "copilot/claude-sonnet-4";
28+
process.env.PI_MODEL = "anthropic/claude-opus-4";
29+
30+
expect(module.getConfiguredModel()).toBe("copilot/claude-sonnet-4");
31+
});
32+
33+
it("registers configured providers and aliases from the environment", () => {
34+
process.env.COPILOT_GITHUB_TOKEN = "copilot-token";
35+
process.env.GITHUB_COPILOT_BASE_URL = "https://copilot.example.test";
36+
process.env.ANTHROPIC_API_KEY = "anthropic-token";
37+
process.env.ANTHROPIC_BASE_URL = "https://anthropic.example.test";
38+
process.env.CODEX_API_KEY = "codex-token";
39+
process.env.OPENAI_BASE_URL = "https://openai.example.test";
40+
41+
const calls = [];
42+
const pi = {
43+
registerProvider: vi.fn((name, config) => {
44+
calls.push([name, config]);
45+
}),
46+
on: vi.fn(),
47+
};
48+
49+
const count = module.registerConfiguredProviders(pi, () => {});
50+
51+
expect(count).toBe(5);
52+
expect(calls).toEqual([
53+
["github-copilot", { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }],
54+
["copilot", { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }],
55+
["anthropic", { apiKey: "anthropic-token", api: "anthropic", baseUrl: "https://anthropic.example.test" }],
56+
["openai", { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }],
57+
["codex", { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }],
58+
]);
59+
});
60+
61+
it("logs the configured provider using GH_AW_PI_MODEL during agent_start", async () => {
62+
process.env.GH_AW_PI_MODEL = "copilot/claude-sonnet-4";
63+
global.fetch = vi.fn().mockRejectedValue(new Error("network disabled"));
64+
65+
const handlers = {};
66+
const pi = {
67+
registerProvider: vi.fn(),
68+
on: vi.fn((event, handler) => {
69+
handlers[event] = handler;
70+
}),
71+
};
72+
73+
module.default(pi);
74+
await handlers.agent_start();
75+
76+
expect(stderrOutput.some(line => line.includes("provider=copilot model=copilot/claude-sonnet-4"))).toBe(true);
77+
});
78+
});

0 commit comments

Comments
 (0)