diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index e0a70adb3d0..6d0e03a39c3 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"5b756a1dc53a828105ac79a0d3777e19eb27d8f303b70b3ee0d27a600517e396","agent_id":"pi","agent_model":"copilot/claude-sonnet-4-20250514"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"93f606ec4137ce1d4e464dfc3e2a005b05697bcf07b216a065d1ae25e8c43455","agent_id":"pi","agent_model":"claude-sonnet-4-20250514"}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.43"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
@@ -147,7 +147,7 @@ jobs:
env:
GH_AW_INFO_ENGINE_ID: "pi"
GH_AW_INFO_ENGINE_NAME: "Pi"
- GH_AW_INFO_MODEL: "copilot/claude-sonnet-4-20250514"
+ GH_AW_INFO_MODEL: "claude-sonnet-4-20250514"
GH_AW_INFO_VERSION: "0.72.1"
GH_AW_INFO_AGENT_VERSION: "0.72.1"
GH_AW_INFO_WORKFLOW_NAME: "Dev"
@@ -258,20 +258,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF'
+ cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF'
- GH_AW_PROMPT_b88579a1f7017635_EOF
+ GH_AW_PROMPT_477b040d2fd91ec8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF'
+ cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF'
Tools: create_issue, missing_tool, missing_data, noop
- GH_AW_PROMPT_b88579a1f7017635_EOF
+ GH_AW_PROMPT_477b040d2fd91ec8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF'
+ cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF'
The following GitHub context information is available for this workflow:
{{#if __GH_AW_GITHUB_ACTOR__ }}
@@ -300,14 +300,14 @@ jobs:
{{/if}}
- GH_AW_PROMPT_b88579a1f7017635_EOF
+ GH_AW_PROMPT_477b040d2fd91ec8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF'
+ cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF'
{{#runtime-import .github/workflows/shared/observability-otlp.md}}
{{#runtime-import .github/workflows/shared/noop-reminder.md}}
{{#runtime-import .github/workflows/dev.md}}
- GH_AW_PROMPT_b88579a1f7017635_EOF
+ GH_AW_PROMPT_477b040d2fd91ec8_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -518,9 +518,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b0544374a465e7c6_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bd7f9d4e4dd0c216_EOF'
{"create_issue":{"expires":168,"max":1,"title_prefix":"[Daily Report] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
- GH_AW_SAFE_OUTPUTS_CONFIG_b0544374a465e7c6_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_bd7f9d4e4dd0c216_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -771,6 +771,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_AW_PHASE: agent
+ GH_AW_PI_MODEL: claude-sonnet-4-20250514
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
GH_AW_VERSION: dev
@@ -1255,6 +1256,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_AW_PHASE: detection
+ GH_AW_PI_MODEL: claude-sonnet-4-20250514
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_VERSION: dev
GITHUB_AW: true
@@ -1367,7 +1369,7 @@ jobs:
GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }}
GH_AW_ENGINE_ID: "pi"
- GH_AW_ENGINE_MODEL: "copilot/claude-sonnet-4-20250514"
+ GH_AW_ENGINE_MODEL: "claude-sonnet-4-20250514"
GH_AW_WORKFLOW_ID: "dev"
GH_AW_WORKFLOW_NAME: "Dev"
outputs:
diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md
index 95b50c33127..8f29d707bdc 100644
--- a/.github/workflows/dev.md
+++ b/.github/workflows/dev.md
@@ -9,8 +9,11 @@ description: Daily status report for gh-aw project
timeout-minutes: 30
strict: false
engine:
- id: pi
- model: copilot/claude-sonnet-4-20250514
+ runtime:
+ id: pi
+ provider:
+ id: github
+ model: claude-sonnet-4-20250514
permissions:
contents: read
diff --git a/.github/workflows/smoke-pi.lock.yml b/.github/workflows/smoke-pi.lock.yml
index 86963d04dc2..bf97479d14b 100644
--- a/.github/workflows/smoke-pi.lock.yml
+++ b/.github/workflows/smoke-pi.lock.yml
@@ -841,6 +841,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_AW_PHASE: agent
+ GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
GH_AW_VERSION: dev
@@ -1342,6 +1343,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_AW_PHASE: detection
+ GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_VERSION: dev
GITHUB_AW: true
diff --git a/actions/setup/js/pi_provider.cjs b/actions/setup/js/pi_provider.cjs
index adf137faaaa..4238479a69d 100644
--- a/actions/setup/js/pi_provider.cjs
+++ b/actions/setup/js/pi_provider.cjs
@@ -3,11 +3,12 @@
/**
* Pi Provider Extension for gh-aw
*
- * Calls the AWF API proxy /reflect endpoint at session start to dynamically
- * discover the open LLM inference paths configured for this run. This gives
- * operators runtime visibility into which provider/model combination is active
- * and verifies that the expected gateway port is reachable before the agent
- * starts working.
+ * Registers Pi providers from the AWF-injected environment and calls the AWF
+ * API proxy /reflect endpoint at session start to dynamically discover the
+ * open LLM inference paths configured for this run. This gives operators
+ * runtime visibility into which provider/model combination is active and
+ * verifies that the expected gateway port is reachable before the agent starts
+ * working.
*
* When the model uses provider/model format (e.g. "copilot/claude-sonnet-4"),
* the extension logs the matched endpoint so failures can be diagnosed without
@@ -18,7 +19,10 @@
* configuration is required.
*
* Configuration (read from environment variables):
- * PI_MODEL The engine.model value; may be "provider/model" or bare "model".
+ * GH_AW_PI_MODEL The original engine.model value; may be "provider/model"
+ * or bare "model". Preferred over PI_MODEL so gh-aw can pass
+ * model context to extensions without changing Pi CLI behavior.
+ * PI_MODEL Legacy fallback used when GH_AW_PI_MODEL is not set.
*/
"use strict";
@@ -29,6 +33,18 @@ const { fetchAWFReflect, AWF_API_PROXY_REFLECT_URL, AWF_REFLECT_OUTPUT_PATH, AWF
// prettier-ignore
const DEFAULT_LOGGER = /** @type {(msg: string) => void} */ (msg => process.stderr.write(`[gh-aw/pi-provider] ${new Date().toISOString()} ${msg}\n`));
+/**
+ * Return the workflow-configured model string exposed to Pi extensions.
+ * GH_AW_PI_MODEL takes precedence because gh-aw sets it explicitly for extensions
+ * while continuing to pass the CLI model via --model. PI_MODEL remains a legacy
+ * fallback for older callers.
+ *
+ * @returns {string}
+ */
+function getConfiguredModel() {
+ return process.env.GH_AW_PI_MODEL || process.env.PI_MODEL || "";
+}
+
/**
* Extract the provider prefix from a "provider/model" string.
* Returns an empty string when no slash is present (bare model name).
@@ -66,14 +82,91 @@ function resolveGatewayUrl(provider) {
return `http://api-proxy:${port}`;
}
+/**
+ * Register a Pi provider and any aliases.
+ *
+ * @param {any} pi
+ * @param {string[]} names
+ * @param {Record} config
+ * @param {(msg: string) => void} logger
+ */
+function registerProviderAliases(pi, names, config, logger) {
+ for (const name of names) {
+ pi.registerProvider(name, config);
+ logger(`registered provider=${name}`);
+ }
+}
+
+/**
+ * Register all supported Pi providers discovered from the environment.
+ *
+ * @param {any} pi
+ * @param {(msg: string) => void} logger
+ * @returns {number}
+ */
+function registerConfiguredProviders(pi, logger) {
+ let registeredCount = 0;
+
+ const copilotToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
+ if (copilotToken) {
+ registerProviderAliases(
+ pi,
+ ["github-copilot", "copilot"],
+ {
+ apiKey: copilotToken,
+ api: "openai-completions",
+ ...(process.env.GITHUB_COPILOT_BASE_URL ? { baseUrl: process.env.GITHUB_COPILOT_BASE_URL } : {}),
+ },
+ logger
+ );
+ registeredCount += 2;
+ }
+
+ if (process.env.ANTHROPIC_API_KEY) {
+ registerProviderAliases(
+ pi,
+ ["anthropic"],
+ {
+ apiKey: process.env.ANTHROPIC_API_KEY,
+ api: "anthropic",
+ ...(process.env.ANTHROPIC_BASE_URL ? { baseUrl: process.env.ANTHROPIC_BASE_URL } : {}),
+ },
+ logger
+ );
+ registeredCount += 1;
+ }
+
+ const openAIKey = process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY;
+ if (openAIKey) {
+ registerProviderAliases(
+ pi,
+ ["openai", "codex"],
+ {
+ apiKey: openAIKey,
+ api: "openai-completions",
+ ...(process.env.OPENAI_BASE_URL ? { baseUrl: process.env.OPENAI_BASE_URL } : {}),
+ },
+ logger
+ );
+ registeredCount += 2;
+ }
+
+ if (registeredCount === 0) {
+ logger("no provider credentials detected for Pi provider registration");
+ }
+
+ return registeredCount;
+}
+
/**
* Pi provider extension for gh-aw.
*
- * Subscribes to the `agent_start` and `agent_end` Pi SDK events and calls the AWF /reflect
- * endpoint to discover and log the open LLM inference paths before the agent begins its
- * first turn and again after it finishes. The post-run fetch is the authoritative snapshot
- * used by the step summary; the pre-run fetch captures the initial proxy state for diagnostics
- * in case the session exits unexpectedly before reaching `agent_end`.
+ * Registers providers immediately, then subscribes to the `agent_start` and `agent_end`
+ * Pi SDK events and calls the AWF /reflect endpoint to discover and log the open LLM
+ * inference paths before the agent begins its first turn and again after it finishes.
+ * The post-run fetch is the authoritative snapshot used by the step summary; the pre-run
+ * fetch captures the initial proxy state for diagnostics in case the session exits
+ * unexpectedly before reaching `agent_end`.
* Both calls are best-effort: any network or parse error is logged but does not abort the
* agent session.
*
@@ -82,9 +175,10 @@ function resolveGatewayUrl(provider) {
*/
function piProviderExtension(pi) {
const log = DEFAULT_LOGGER;
+ registerConfiguredProviders(pi, log);
pi.on("agent_start", async () => {
- const model = process.env.PI_MODEL || "";
+ const model = getConfiguredModel();
const provider = extractProviderFromModel(model);
if (provider) {
@@ -123,5 +217,7 @@ function piProviderExtension(pi) {
}
module.exports = piProviderExtension;
+module.exports.getConfiguredModel = getConfiguredModel;
module.exports.extractProviderFromModel = extractProviderFromModel;
module.exports.resolveGatewayUrl = resolveGatewayUrl;
+module.exports.registerConfiguredProviders = registerConfiguredProviders;
diff --git a/actions/setup/js/pi_provider.test.cjs b/actions/setup/js/pi_provider.test.cjs
new file mode 100644
index 00000000000..d2adeb24dbf
--- /dev/null
+++ b/actions/setup/js/pi_provider.test.cjs
@@ -0,0 +1,78 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+describe("pi_provider.cjs", () => {
+ let module;
+ let originalEnv;
+ let originalFetch;
+ let stderrOutput;
+
+ beforeEach(async () => {
+ originalEnv = { ...process.env };
+ originalFetch = global.fetch;
+ stderrOutput = [];
+ vi.spyOn(process.stderr, "write").mockImplementation(msg => {
+ stderrOutput.push(String(msg));
+ return true;
+ });
+ module = await import("./pi_provider.cjs?" + Date.now());
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ global.fetch = originalFetch;
+ vi.restoreAllMocks();
+ });
+
+ it("prefers GH_AW_PI_MODEL over PI_MODEL", () => {
+ process.env.GH_AW_PI_MODEL = "copilot/claude-sonnet-4";
+ process.env.PI_MODEL = "anthropic/claude-opus-4";
+
+ expect(module.getConfiguredModel()).toBe("copilot/claude-sonnet-4");
+ });
+
+ it("registers configured providers and aliases from the environment", () => {
+ process.env.COPILOT_GITHUB_TOKEN = "copilot-token";
+ process.env.GITHUB_COPILOT_BASE_URL = "https://copilot.example.test";
+ process.env.ANTHROPIC_API_KEY = "anthropic-token";
+ process.env.ANTHROPIC_BASE_URL = "https://anthropic.example.test";
+ process.env.CODEX_API_KEY = "codex-token";
+ process.env.OPENAI_BASE_URL = "https://openai.example.test";
+
+ const calls = [];
+ const pi = {
+ registerProvider: vi.fn((name, config) => {
+ calls.push([name, config]);
+ }),
+ on: vi.fn(),
+ };
+
+ const count = module.registerConfiguredProviders(pi, () => {});
+
+ expect(count).toBe(5);
+ expect(calls).toEqual([
+ ["github-copilot", { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }],
+ ["copilot", { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }],
+ ["anthropic", { apiKey: "anthropic-token", api: "anthropic", baseUrl: "https://anthropic.example.test" }],
+ ["openai", { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }],
+ ["codex", { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }],
+ ]);
+ });
+
+ it("logs the configured provider using GH_AW_PI_MODEL during agent_start", async () => {
+ process.env.GH_AW_PI_MODEL = "copilot/claude-sonnet-4";
+ global.fetch = vi.fn().mockRejectedValue(new Error("network disabled"));
+
+ const handlers = {};
+ const pi = {
+ registerProvider: vi.fn(),
+ on: vi.fn((event, handler) => {
+ handlers[event] = handler;
+ }),
+ };
+
+ module.default(pi);
+ await handlers.agent_start();
+
+ expect(stderrOutput.some(line => line.includes("provider=copilot model=copilot/claude-sonnet-4"))).toBe(true);
+ });
+});
diff --git a/pkg/constants/constants_test.go b/pkg/constants/constants_test.go
index bc8d6cb2b9d..1e7a527ec36 100644
--- a/pkg/constants/constants_test.go
+++ b/pkg/constants/constants_test.go
@@ -4,6 +4,7 @@ package constants
import (
"path/filepath"
+ "reflect"
"testing"
"time"
)
@@ -659,3 +660,26 @@ func TestGetAllEngineSecretNames(t *testing.T) {
seen[s] = true
}
}
+
+func TestEngineOptions_MultiProviderAlternatives(t *testing.T) {
+ tests := []struct {
+ engine string
+ want []string
+ }{
+ {"opencode", []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}},
+ {"crush", []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}},
+ {"pi", []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.engine, func(t *testing.T) {
+ opt := GetEngineOption(tt.engine)
+ if opt == nil {
+ t.Fatalf("GetEngineOption(%q) returned nil", tt.engine)
+ }
+ if !reflect.DeepEqual(tt.want, opt.AlternativeSecrets) {
+ t.Fatalf("AlternativeSecrets = %#v, want %#v", opt.AlternativeSecrets, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/constants/engine_constants.go b/pkg/constants/engine_constants.go
index 349f59283a9..dff534836a5 100644
--- a/pkg/constants/engine_constants.go
+++ b/pkg/constants/engine_constants.go
@@ -94,7 +94,7 @@ var EngineOptions = []EngineOption{
Label: "OpenCode",
Description: "OpenCode multi-provider AI coding agent (BYOK)",
SecretName: "COPILOT_GITHUB_TOKEN",
- AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "GEMINI_API_KEY"},
+ AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"},
KeyURL: "https://github.com/anomalyco/opencode",
WhenNeeded: "OpenCode engine workflows (default: Copilot routing)",
},
@@ -103,17 +103,18 @@ var EngineOptions = []EngineOption{
Label: "Crush",
Description: "Crush multi-provider AI coding agent (BYOK)",
SecretName: "COPILOT_GITHUB_TOKEN",
- AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "GEMINI_API_KEY"},
+ AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"},
KeyURL: "https://github.com/charmbracelet/crush#installation",
WhenNeeded: "Crush engine workflows (default: Copilot routing)",
},
{
- Value: string(PiEngine),
- Label: "Pi",
- Description: "Pi AI coding agent (experimental)",
- SecretName: "COPILOT_GITHUB_TOKEN",
- KeyURL: "https://github.com/settings/personal-access-tokens/new",
- WhenNeeded: "Pi engine workflows",
+ Value: string(PiEngine),
+ Label: "Pi",
+ Description: "Pi AI coding agent (experimental)",
+ SecretName: "COPILOT_GITHUB_TOKEN",
+ AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"},
+ KeyURL: "https://github.com/settings/personal-access-tokens/new",
+ WhenNeeded: "Pi engine workflows",
},
}
diff --git a/pkg/workflow/agent_validation.go b/pkg/workflow/agent_validation.go
index 2cb4a9277c1..40a32d4c67c 100644
--- a/pkg/workflow/agent_validation.go
+++ b/pkg/workflow/agent_validation.go
@@ -174,6 +174,23 @@ func (c *Compiler) validateUniversalLLMConsumerModel(frontmatter map[string]any,
return nil
}
+// validatePiEngineRequirements validates Pi's required tool configuration.
+func (c *Compiler) validatePiEngineRequirements(tools *ToolsConfig, engine CodingAgentEngine) error {
+ if engine.GetID() != "pi" {
+ return nil
+ }
+
+ if tools == nil || tools.GitHub == nil || tools.GitHub.Mode != "gh-proxy" {
+ return fmt.Errorf("engine 'pi' requires tools.github.mode: gh-proxy")
+ }
+
+ if !tools.CLIProxy {
+ return fmt.Errorf("engine 'pi' requires tools.cli-proxy: true")
+ }
+
+ return nil
+}
+
// validateWebSearchSupport validates that web-search tool is only used with engines that support this feature
func (c *Compiler) validateWebSearchSupport(tools map[string]any, engine CodingAgentEngine) {
// Check if web-search tool is requested
diff --git a/pkg/workflow/agent_validation_model_test.go b/pkg/workflow/agent_validation_model_test.go
index 25e32c3847f..14c4e2de298 100644
--- a/pkg/workflow/agent_validation_model_test.go
+++ b/pkg/workflow/agent_validation_model_test.go
@@ -78,3 +78,36 @@ func TestValidateUniversalLLMConsumerModel(t *testing.T) {
assert.NoError(t, err, "Supported provider/model should pass")
})
}
+
+func TestValidatePiEngineRequirements(t *testing.T) {
+ compiler := NewCompiler()
+
+ t.Run("non pi engine skips validation", func(t *testing.T) {
+ err := compiler.validatePiEngineRequirements(NewTools(map[string]any{}), NewCopilotEngine())
+ assert.NoError(t, err)
+ })
+
+ t.Run("pi requires github gh-proxy mode", func(t *testing.T) {
+ err := compiler.validatePiEngineRequirements(NewTools(map[string]any{
+ "github": true,
+ }), NewPiEngine())
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "tools.github.mode: gh-proxy")
+ })
+
+ t.Run("pi requires cli-proxy", func(t *testing.T) {
+ err := compiler.validatePiEngineRequirements(NewTools(map[string]any{
+ "github": map[string]any{"mode": "gh-proxy"},
+ }), NewPiEngine())
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "tools.cli-proxy: true")
+ })
+
+ t.Run("valid pi tool config passes", func(t *testing.T) {
+ err := compiler.validatePiEngineRequirements(NewTools(map[string]any{
+ "github": map[string]any{"mode": "gh-proxy"},
+ "cli-proxy": true,
+ }), NewPiEngine())
+ assert.NoError(t, err)
+ })
+}
diff --git a/pkg/workflow/compiler_orchestrator_tools.go b/pkg/workflow/compiler_orchestrator_tools.go
index 0fd0f112dd6..379ebb74e05 100644
--- a/pkg/workflow/compiler_orchestrator_tools.go
+++ b/pkg/workflow/compiler_orchestrator_tools.go
@@ -229,6 +229,10 @@ func (c *Compiler) processToolsAndMarkdown(result *parser.FrontmatterResult, cle
return nil, err
}
+ if err := c.validatePiEngineRequirements(NewTools(tools), agenticEngine); err != nil {
+ return nil, err
+ }
+
// Validate web-search support for the current engine (warning only)
c.validateWebSearchSupport(tools, agenticEngine)
diff --git a/pkg/workflow/data/engines/crush.md b/pkg/workflow/data/engines/crush.md
index b095d4d7457..5cb862697a7 100644
--- a/pkg/workflow/data/engines/crush.md
+++ b/pkg/workflow/data/engines/crush.md
@@ -5,7 +5,7 @@ engine:
description: Crush CLI with headless mode and multi-provider LLM support
runtime-id: crush
provider:
- name: crush
+ name: github
auth:
- role: api-key
secret: COPILOT_GITHUB_TOKEN
diff --git a/pkg/workflow/data/engines/opencode.md b/pkg/workflow/data/engines/opencode.md
index d5a2aad0f9a..ae5a81d38ae 100644
--- a/pkg/workflow/data/engines/opencode.md
+++ b/pkg/workflow/data/engines/opencode.md
@@ -5,7 +5,7 @@ engine:
description: OpenCode CLI with headless mode and multi-provider LLM support
runtime-id: opencode
provider:
- name: opencode
+ name: github
auth:
- role: api-key
secret: COPILOT_GITHUB_TOKEN
diff --git a/pkg/workflow/data/engines/pi.md b/pkg/workflow/data/engines/pi.md
index 2267243be0f..b6fa7940ac4 100644
--- a/pkg/workflow/data/engines/pi.md
+++ b/pkg/workflow/data/engines/pi.md
@@ -5,7 +5,7 @@ engine:
description: Pi AI coding agent (experimental)
runtime-id: pi
provider:
- name: pi
+ name: github
auth:
- role: api-key
secret: COPILOT_GITHUB_TOKEN
diff --git a/pkg/workflow/engine_auth_test.go b/pkg/workflow/engine_auth_test.go
index 30770dabc7d..32d0a4c253c 100644
--- a/pkg/workflow/engine_auth_test.go
+++ b/pkg/workflow/engine_auth_test.go
@@ -405,6 +405,9 @@ func TestBuiltInEngineAuthUnchanged(t *testing.T) {
{"codex", "CODEX_API_KEY"},
{"copilot", "COPILOT_GITHUB_TOKEN"},
{"gemini", "GEMINI_API_KEY"},
+ {"opencode", "COPILOT_GITHUB_TOKEN"},
+ {"crush", "COPILOT_GITHUB_TOKEN"},
+ {"pi", "COPILOT_GITHUB_TOKEN"},
}
for _, tt := range tests {
diff --git a/pkg/workflow/engine_definition_test.go b/pkg/workflow/engine_definition_test.go
index 72f2781b3b7..01876564302 100644
--- a/pkg/workflow/engine_definition_test.go
+++ b/pkg/workflow/engine_definition_test.go
@@ -25,8 +25,9 @@ func TestNewEngineCatalog_BuiltIns(t *testing.T) {
{"codex", "Codex", "openai"},
{"copilot", "GitHub Copilot CLI", "github"},
{"gemini", "Google Gemini CLI", "google"},
- {"opencode", "OpenCode", "opencode"},
- {"crush", "Crush", "crush"},
+ {"opencode", "OpenCode", "github"},
+ {"crush", "Crush", "github"},
+ {"pi", "Pi", "github"},
}
for _, tt := range tests {
diff --git a/pkg/workflow/pi_engine.go b/pkg/workflow/pi_engine.go
index bfe7ede01ef..4168ecc54c4 100644
--- a/pkg/workflow/pi_engine.go
+++ b/pkg/workflow/pi_engine.go
@@ -48,8 +48,9 @@ func NewPiEngine() *PiEngine {
}
}
-// GetModelEnvVarName returns the native environment variable name that the Pi CLI uses
-// for model selection. Setting PI_MODEL is equivalent to passing --model to the CLI.
+// GetModelEnvVarName returns the legacy Pi model env-var name exposed by gh-aw.
+// gh-aw passes the model to the Pi CLI via --model and separately exports the
+// original workflow model for extensions.
func (e *PiEngine) GetModelEnvVarName() string {
return constants.PiCLIModelEnvVar
}
@@ -275,7 +276,7 @@ func (e *PiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string)
piArgs = append(piArgs, workflowData.EngineConfig.Args...)
}
- // Pi v0.72+ does not support a PI_MODEL env var; the model must be passed as
+ // Pi v0.72+ does not support a PI_MODEL env var for CLI model selection; the model must be passed as
// the --model CLI flag. When the firewall is enabled we route LLM traffic
// through the AWF gateway sidecar by generating a temporary models.json that
// registers a custom "aw-gateway" provider pointing at the gateway port. When
@@ -379,6 +380,9 @@ touch %s
"GITHUB_WORKSPACE": "${{ github.workspace }}",
"GITHUB_STEP_SUMMARY": AgentStepSummaryPath,
}
+ if modelConfigured {
+ env["GH_AW_PI_MODEL"] = workflowData.EngineConfig.Model
+ }
// Inject provider-specific credentials from the backend profile.
maps.Copy(env, profile.env)
diff --git a/pkg/workflow/pi_engine_test.go b/pkg/workflow/pi_engine_test.go
index 3eb4caacccf..892d122d790 100644
--- a/pkg/workflow/pi_engine_test.go
+++ b/pkg/workflow/pi_engine_test.go
@@ -200,7 +200,8 @@ func TestPiEngine_GetExecutionSteps_WithModel(t *testing.T) {
assert.Contains(t, stepText, "--model", "Step should pass --model flag to Pi CLI")
assert.Contains(t, stepText, "github-copilot", "Non-firewall copilot model should use github-copilot/ provider prefix")
assert.Contains(t, stepText, "claude-sonnet-4", "Step should include the model ID portion")
- assert.NotContains(t, stepText, "PI_MODEL", "Step should not set the unsupported PI_MODEL env var")
+ assert.Contains(t, stepText, "GH_AW_PI_MODEL", "Step should expose the original workflow model to Pi extensions")
+ assert.NotContains(t, stepText, "\n PI_MODEL:", "Step should not set PI_MODEL in the environment when the CLI model is passed via --model")
}
func TestPiEngine_GetExecutionSteps_ProviderPrefixCopilot(t *testing.T) {