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) {