diff --git a/.github/workflows/smoke-copilot-auto.lock.yml b/.github/workflows/smoke-copilot-auto.lock.yml index 5c316f6e8b8..80f7930c2f1 100644 --- a/.github/workflows/smoke-copilot-auto.lock.yml +++ b/.github/workflows/smoke-copilot-auto.lock.yml @@ -149,7 +149,7 @@ jobs: GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🌸" GH_AW_COMPILED_STRICT: "true" - GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"0","output":"0"}}}}}}' + GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"8.5e-07","output":"1.55e-06"}}}}}}' GH_AW_INFO_FEATURES: '{"gh-aw-detection":false}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index 76390377df0..cda9840a879 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -246,7 +246,7 @@ type ModelEnvVarProvider interface { type InferenceProviderResolver interface { // ResolveLLMProvider returns the effective provider for the workflow // (for example "github", "anthropic", or "openai"). - ResolveLLMProvider(workflowData *WorkflowData) string + ResolveLLMProvider(workflowData *WorkflowData) LLMProvider } // LLMProviderResolver is kept as a backward-compatible alias. diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index 9b89fc7a376..7b13ce4e127 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -50,7 +50,7 @@ func (e *ClaudeEngine) GetModelEnvVarName() string { // ResolveLLMProvider returns the effective provider for Claude inference. // Default is anthropic, overridable via engine.provider (or engine.model-provider). -func (e *ClaudeEngine) ResolveLLMProvider(workflowData *WorkflowData) string { +func (e *ClaudeEngine) ResolveLLMProvider(workflowData *WorkflowData) LLMProvider { return resolveEngineLLMProvider(workflowData, LLMProviderAnthropic) } @@ -423,7 +423,7 @@ func (e *ClaudeEngine) buildClaudeFullCommand(workflowData *WorkflowData, claude func (e *ClaudeEngine) buildClaudeCommandEnv(workflowData *WorkflowData) map[string]string { provider := e.ResolveLLMProvider(workflowData) env := buildClaudeBaseEnvMap(provider, workflowData) - env["GH_AW_LLM_PROVIDER"] = provider + env["GH_AW_LLM_PROVIDER"] = string(provider) if isFirewallEnabled(workflowData) && provider != LLMProviderAnthropic { env["ANTHROPIC_BASE_URL"] = llmProviderGatewayBaseURL(provider) } @@ -465,7 +465,7 @@ func (e *ClaudeEngine) buildClaudeCommandEnv(workflowData *WorkflowData) map[str // buildClaudeBaseEnvMap returns the initial Claude execution environment with static flags // and well-known GitHub Actions context values. -func buildClaudeBaseEnvMap(provider string, workflowData *WorkflowData) map[string]string { +func buildClaudeBaseEnvMap(provider LLMProvider, workflowData *WorkflowData) map[string]string { return map[string]string{ "ANTHROPIC_API_KEY": llmProviderSecretExpression(provider, workflowData), "DISABLE_TELEMETRY": "1", diff --git a/pkg/workflow/claude_engine_test.go b/pkg/workflow/claude_engine_test.go index 314bcfa8660..e2af0eead19 100644 --- a/pkg/workflow/claude_engine_test.go +++ b/pkg/workflow/claude_engine_test.go @@ -179,7 +179,7 @@ func TestClaudeEngineLLMProviderGitHubUsesCopilotCredentials(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", EngineConfig: &EngineConfig{ - LLMProvider: "github", + LLMProvider: LLMProviderGitHub, }, NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{Enabled: true}, diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index f99daff44ce..f369659a6f9 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -78,7 +78,7 @@ func (e *CodexEngine) GetModelEnvVarName() string { // ResolveLLMProvider returns the effective provider for Codex inference. // Default is openai, overridable via engine.provider (or engine.model-provider). -func (e *CodexEngine) ResolveLLMProvider(workflowData *WorkflowData) string { +func (e *CodexEngine) ResolveLLMProvider(workflowData *WorkflowData) LLMProvider { return resolveEngineLLMProvider(workflowData, LLMProviderOpenAI) } diff --git a/pkg/workflow/codex_engine_test.go b/pkg/workflow/codex_engine_test.go index 028704bbf69..83f2af308fa 100644 --- a/pkg/workflow/codex_engine_test.go +++ b/pkg/workflow/codex_engine_test.go @@ -16,7 +16,7 @@ func TestCodexEngine_ResolveLLMProvider_DefaultOpenAI(t *testing.T) { engine := NewCodexEngine() asserted := engine.ResolveLLMProvider(&WorkflowData{EngineConfig: &EngineConfig{ID: "codex"}}) - if asserted != "openai" { + if asserted != LLMProviderOpenAI { t.Fatalf("expected default model-provider to be openai, got %q", asserted) } } diff --git a/pkg/workflow/compiler_model_pricing.go b/pkg/workflow/compiler_model_pricing.go index 7b21bfacc47..d99dad5964b 100644 --- a/pkg/workflow/compiler_model_pricing.go +++ b/pkg/workflow/compiler_model_pricing.go @@ -63,7 +63,7 @@ func resolveEngineProviderForPricing(engineConfig *EngineConfig) string { return "github-copilot" // default provider when no engine is specified } if engineConfig.LLMProvider != "" { - return normalizeProviderForPricing(engineConfig.LLMProvider) + return normalizeProviderForPricing(string(engineConfig.LLMProvider)) } if engineConfig.InlineProviderID != "" { return normalizeProviderForPricing(engineConfig.InlineProviderID) diff --git a/pkg/workflow/compiler_model_pricing_test.go b/pkg/workflow/compiler_model_pricing_test.go index 86955d4dcb9..3754ef07521 100644 --- a/pkg/workflow/compiler_model_pricing_test.go +++ b/pkg/workflow/compiler_model_pricing_test.go @@ -141,8 +141,8 @@ func TestResolveEngineProviderForPricing(t *testing.T) { config *EngineConfig want string }{ - {"LLMProvider wins", &EngineConfig{LLMProvider: "openai", InlineProviderID: "other", ID: "claude"}, "openai"}, - {"LLMProvider alias normalized", &EngineConfig{LLMProvider: "github_models", ID: "claude"}, "github-copilot"}, + {"LLMProvider wins", &EngineConfig{LLMProvider: LLMProviderOpenAI, InlineProviderID: "other", ID: "claude"}, "openai"}, + {"LLMProvider alias normalized", &EngineConfig{LLMProvider: LLMProvider("github_models"), ID: "claude"}, "github-copilot"}, {"InlineProviderID second", &EngineConfig{InlineProviderID: "openai", ID: "claude"}, "openai"}, {"claude engine → anthropic", &EngineConfig{ID: "claude"}, "anthropic"}, {"codex engine → openai", &EngineConfig{ID: "codex"}, "openai"}, diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go index 40d675017ca..a2b45306e7e 100644 --- a/pkg/workflow/copilot_engine.go +++ b/pkg/workflow/copilot_engine.go @@ -66,7 +66,7 @@ func (e *CopilotEngine) GetModelEnvVarName() string { // ResolveLLMProvider returns the effective provider for Copilot inference. // Default is github, overridable via engine.model-provider. -func (e *CopilotEngine) ResolveLLMProvider(workflowData *WorkflowData) string { +func (e *CopilotEngine) ResolveLLMProvider(workflowData *WorkflowData) LLMProvider { return resolveEngineLLMProvider(workflowData, LLMProviderGitHub) } diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index cf5ab0ccb44..4aaab348c09 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -520,7 +520,7 @@ type copilotStepEnvFlags struct { func (e *CopilotEngine) buildCopilotStepEnv( workflowData *WorkflowData, - llmProvider string, + llmProvider LLMProvider, modelEnvVar string, timeoutValue string, flags copilotStepEnvFlags, @@ -537,8 +537,8 @@ func (e *CopilotEngine) buildCopilotStepEnv( return env } -func (e *CopilotEngine) buildCopilotBaseStepEnv(workflowData *WorkflowData, llmProvider, timeoutValue string, isBYOKMode, useCopilotRequests bool) map[string]string { - env := map[string]string{"COPILOT_AGENT_RUNNER_TYPE": "STANDALONE", "GITHUB_STEP_SUMMARY": AgentStepSummaryPath, "GITHUB_HEAD_REF": "${{ github.head_ref }}", "GITHUB_REF_NAME": "${{ github.ref_name }}", "GITHUB_WORKSPACE": "${{ github.workspace }}", "RUNNER_TEMP": "${{ runner.temp }}", "GH_AW_TIMEOUT_MINUTES": timeoutValue, "GITHUB_SERVER_URL": "${{ github.server_url }}", "GITHUB_API_URL": "${{ github.api_url }}", "GH_AW_LLM_PROVIDER": llmProvider} +func (e *CopilotEngine) buildCopilotBaseStepEnv(workflowData *WorkflowData, llmProvider LLMProvider, timeoutValue string, isBYOKMode, useCopilotRequests bool) map[string]string { + env := map[string]string{"COPILOT_AGENT_RUNNER_TYPE": "STANDALONE", "GITHUB_STEP_SUMMARY": AgentStepSummaryPath, "GITHUB_HEAD_REF": "${{ github.head_ref }}", "GITHUB_REF_NAME": "${{ github.ref_name }}", "GITHUB_WORKSPACE": "${{ github.workspace }}", "RUNNER_TEMP": "${{ runner.temp }}", "GH_AW_TIMEOUT_MINUTES": timeoutValue, "GITHUB_SERVER_URL": "${{ github.server_url }}", "GITHUB_API_URL": "${{ github.api_url }}", "GH_AW_LLM_PROVIDER": string(llmProvider)} // Auto-configure Copilot BYOK routing when engine.model-provider selects a non-GitHub provider. // Explicit engine.env values still win later via maps.Copy. if llmProvider != LLMProviderGitHub && isFirewallEnabled(workflowData) { diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 56e8ecbffb4..2ace2457b02 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -3059,7 +3059,7 @@ func TestCopilotEngineLLMProviderAnthropicAutoBYOK(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", EngineConfig: &EngineConfig{ - LLMProvider: "anthropic", + LLMProvider: LLMProviderAnthropic, }, NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{Enabled: true}, diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index ea7b35de549..98db36836c5 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -46,7 +46,7 @@ func toEngineEnvValueString(value any) (string, bool) { type EngineConfig struct { ID string Version string - LLMProvider string // Inference provider override for this engine (engine.provider / engine.model-provider) + LLMProvider LLMProvider // Inference provider override for this engine (engine.provider / engine.model-provider) PermissionMode string MaxTurns string MaxToolDenials string // Maximum repeated tool denials before stopping inference (copilot SDK mode only) @@ -292,7 +292,7 @@ func extractInlineEngineConfig(runtime any, engineObj map[string]any, topLevel e func extractInlineProviderConfig(config *EngineConfig, provider any) string { switch providerTyped := provider.(type) { case string: - config.InlineProviderID = normalizeEngineProvider(providerTyped) + config.InlineProviderID = string(normalizeEngineProvider(providerTyped)) case map[string]any: if id, ok := providerTyped["id"].(string); ok { config.InlineProviderID = id @@ -381,8 +381,8 @@ func applyEngineProviderFields(config *EngineConfig, engineObj map[string]any) { } } -func normalizeEngineProvider(provider string) string { - return strings.ToLower(strings.TrimSpace(provider)) +func normalizeEngineProvider(provider string) LLMProvider { + return LLMProvider(strings.ToLower(strings.TrimSpace(provider))) } func applyEnginePermissionMode(config *EngineConfig, engineObj map[string]any) { diff --git a/pkg/workflow/engine_config_test.go b/pkg/workflow/engine_config_test.go index 1aaafd52402..00f11db396d 100644 --- a/pkg/workflow/engine_config_test.go +++ b/pkg/workflow/engine_config_test.go @@ -253,7 +253,7 @@ func TestExtractEngineConfig(t *testing.T) { }, }, expectedEngineSetting: "claude", - expectedConfig: &EngineConfig{ID: "claude", LLMProvider: "github"}, + expectedConfig: &EngineConfig{ID: "claude", LLMProvider: LLMProviderGitHub}, }, { name: "object format - with provider override", @@ -264,7 +264,7 @@ func TestExtractEngineConfig(t *testing.T) { }, }, expectedEngineSetting: "claude", - expectedConfig: &EngineConfig{ID: "claude", LLMProvider: "openai"}, + expectedConfig: &EngineConfig{ID: "claude", LLMProvider: LLMProviderOpenAI}, }, { name: "object format - provider override wins over model-provider", @@ -276,7 +276,7 @@ func TestExtractEngineConfig(t *testing.T) { }, }, expectedEngineSetting: "claude", - expectedConfig: &EngineConfig{ID: "claude", LLMProvider: "openai"}, + expectedConfig: &EngineConfig{ID: "claude", LLMProvider: LLMProviderOpenAI}, }, { name: "object format - deprecated llm-provider ignored", diff --git a/pkg/workflow/engine_helpers_secrets_test.go b/pkg/workflow/engine_helpers_secrets_test.go index c422d163383..b21a2b05489 100644 --- a/pkg/workflow/engine_helpers_secrets_test.go +++ b/pkg/workflow/engine_helpers_secrets_test.go @@ -192,7 +192,7 @@ func TestGetRequiredSecretNames_Copilot(t *testing.T) { Tools: map[string]any{}, ParsedTools: &ToolsConfig{}, EngineConfig: &EngineConfig{ - LLMProvider: "openai", + LLMProvider: LLMProviderOpenAI, }, } @@ -281,7 +281,7 @@ func TestGetRequiredSecretNames_Claude(t *testing.T) { Tools: map[string]any{}, ParsedTools: &ToolsConfig{}, EngineConfig: &EngineConfig{ - LLMProvider: "github", + LLMProvider: LLMProviderGitHub, }, } diff --git a/pkg/workflow/llm_provider.go b/pkg/workflow/llm_provider.go index 5cb92705276..9ef4c6ca33e 100644 --- a/pkg/workflow/llm_provider.go +++ b/pkg/workflow/llm_provider.go @@ -10,13 +10,21 @@ import ( var llmProviderLog = logger.New("workflow:llm_provider") +// LLMProvider identifies the inference provider used by an engine (e.g. "github", "anthropic", "openai"). +type LLMProvider string + +// String returns the string representation of the provider, satisfying the fmt.Stringer interface. +func (p LLMProvider) String() string { + return string(p) +} + const ( - LLMProviderGitHub = "github" - LLMProviderAnthropic = "anthropic" - LLMProviderOpenAI = "openai" + LLMProviderGitHub LLMProvider = "github" + LLMProviderAnthropic LLMProvider = "anthropic" + LLMProviderOpenAI LLMProvider = "openai" ) -var llmProviderAliases = map[string]string{ +var llmProviderAliases = map[string]LLMProvider{ "copilot": LLMProviderGitHub, "github": LLMProviderGitHub, "github-copilot": LLMProviderGitHub, @@ -26,11 +34,11 @@ var llmProviderAliases = map[string]string{ } type llmProviderProfile struct { - id string + id LLMProvider gatewayPort int } -func normalizeLLMProvider(provider string) string { +func normalizeLLMProvider(provider string) LLMProvider { normalized := strings.TrimSpace(provider) if normalized == "" { return LLMProviderAnthropic @@ -39,22 +47,22 @@ func normalizeLLMProvider(provider string) string { if alias, ok := llmProviderAliases[normalized]; ok { return alias } - return normalized + return LLMProvider(normalized) } -func resolveEngineLLMProvider(workflowData *WorkflowData, defaultProvider string) string { +func resolveEngineLLMProvider(workflowData *WorkflowData, defaultProvider LLMProvider) LLMProvider { if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.LLMProvider == "" { - provider := normalizeLLMProvider(defaultProvider) + provider := normalizeLLMProvider(string(defaultProvider)) llmProviderLog.Printf("Resolved LLM provider from default: %s", provider) return provider } - provider := normalizeLLMProvider(workflowData.EngineConfig.LLMProvider) + provider := normalizeLLMProvider(string(workflowData.EngineConfig.LLMProvider)) llmProviderLog.Printf("Resolved LLM provider from engine config: %s", provider) return provider } -func llmProviderProfileFor(provider string) llmProviderProfile { - switch normalizeLLMProvider(provider) { +func llmProviderProfileFor(provider LLMProvider) llmProviderProfile { + switch provider { case LLMProviderGitHub: return llmProviderProfile{ id: LLMProviderGitHub, @@ -73,8 +81,8 @@ func llmProviderProfileFor(provider string) llmProviderProfile { } } -func llmProviderSecretNames(provider string) []string { - switch normalizeLLMProvider(provider) { +func llmProviderSecretNames(provider LLMProvider) []string { + switch provider { case LLMProviderGitHub: return []string{"COPILOT_GITHUB_TOKEN"} case LLMProviderOpenAI: @@ -84,8 +92,8 @@ func llmProviderSecretNames(provider string) []string { } } -func llmProviderSecretExpression(provider string, workflowData *WorkflowData) string { - switch normalizeLLMProvider(provider) { +func llmProviderSecretExpression(provider LLMProvider, workflowData *WorkflowData) string { + switch provider { case LLMProviderGitHub: if hasCopilotRequestsWritePermission(workflowData) { llmProviderLog.Print("Using github.token for GitHub Copilot (copilot-requests write permission present)") @@ -100,13 +108,13 @@ func llmProviderSecretExpression(provider string, workflowData *WorkflowData) st } } -func llmProviderGatewayBaseURL(provider string) string { +func llmProviderGatewayBaseURL(provider LLMProvider) string { profile := llmProviderProfileFor(provider) return fmt.Sprintf("http://host.docker.internal:%d", profile.gatewayPort) } -func llmProviderDocsURL(provider string) string { - switch normalizeLLMProvider(provider) { +func llmProviderDocsURL(provider LLMProvider) string { + switch provider { case LLMProviderGitHub: return "https://github.github.com/gh-aw/reference/engines/#github-copilot-default" case LLMProviderOpenAI: diff --git a/pkg/workflow/pi_engine.go b/pkg/workflow/pi_engine.go index 00104a2cd7e..2cbbb7e9cc7 100644 --- a/pkg/workflow/pi_engine.go +++ b/pkg/workflow/pi_engine.go @@ -61,7 +61,7 @@ func (e *PiEngine) GetModelEnvVarName() string { // ResolveLLMProvider returns the effective provider for Pi inference. // Default is github, overridable via engine.model-provider. -func (e *PiEngine) ResolveLLMProvider(workflowData *WorkflowData) string { +func (e *PiEngine) ResolveLLMProvider(workflowData *WorkflowData) LLMProvider { return resolveEngineLLMProvider(workflowData, LLMProviderGitHub) } diff --git a/pkg/workflow/pi_engine_test.go b/pkg/workflow/pi_engine_test.go index bad37a5646b..d6bbe7c7b6b 100644 --- a/pkg/workflow/pi_engine_test.go +++ b/pkg/workflow/pi_engine_test.go @@ -29,7 +29,7 @@ func TestPiEngine_GetModelEnvVarName(t *testing.T) { func TestPiEngine_ResolveLLMProvider_DefaultGitHub(t *testing.T) { engine := NewPiEngine() - assert.Equal(t, "github", engine.ResolveLLMProvider(&WorkflowData{EngineConfig: &EngineConfig{ID: "pi"}})) + assert.Equal(t, LLMProviderGitHub, engine.ResolveLLMProvider(&WorkflowData{EngineConfig: &EngineConfig{ID: "pi"}})) } func TestPiEngine_GetRequiredSecretNames(t *testing.T) {