diff --git a/.changeset/patch-awf-default-ai-credits-pricing-version-guard.md b/.changeset/patch-awf-default-ai-credits-pricing-version-guard.md new file mode 100644 index 00000000000..657c4b7f37c --- /dev/null +++ b/.changeset/patch-awf-default-ai-credits-pricing-version-guard.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Reject `models.default-ai-credits-pricing` when a workflow pins an AWF version older than v0.27.43, because those versions drop `apiProxy.defaultAiCreditsPricing` during config resolution before it reaches the API proxy. diff --git a/pkg/constants/version_constants.go b/pkg/constants/version_constants.go index 09b824d8813..325f0515c17 100644 --- a/pkg/constants/version_constants.go +++ b/pkg/constants/version_constants.go @@ -116,6 +116,11 @@ const AWFContainerRuntimeMinVersion Version = "v0.27.30" // Workflows pinning an older AWF version must use the old --security-mode compat behavior. const AWFLegacySecurityMinVersion Version = "v0.27.32" +// AWFDefaultAiCreditsPricingMinVersion is the minimum AWF version where +// apiProxy.defaultAiCreditsPricing survives config resolution and reaches the +// api-proxy container as AWF_DEFAULT_AI_CREDITS_PRICING. +const AWFDefaultAiCreditsPricingMinVersion Version = "v0.27.43" + // AWFAPIProxyProvidersMinVersion is the minimum AWF version that supports // apiProxy.providers in awf-config.json. // Workflows pinning an older AWF version must not emit this field because older diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index 796d2644312..3e4b05809ae 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -1093,6 +1093,12 @@ func awfSupportsLegacySecurity(firewallConfig *FirewallConfig) bool { return awfVersionAtLeast(firewallConfig, constants.AWFLegacySecurityMinVersion) } +// awfSupportsDefaultAiCreditsPricing returns true when apiProxy.defaultAiCreditsPricing +// survives AWF config resolution and reaches the api-proxy container. +func awfSupportsDefaultAiCreditsPricing(firewallConfig *FirewallConfig) bool { + return awfVersionAtLeast(firewallConfig, constants.AWFDefaultAiCreditsPricingMinVersion) +} + // awfSupportsAPIProxyProviders returns true when the effective AWF version supports // apiProxy.providers in awf-config.json. func awfSupportsAPIProxyProviders(firewallConfig *FirewallConfig) bool { diff --git a/pkg/workflow/model_costs_pricing_validation.go b/pkg/workflow/model_costs_pricing_validation.go index 6a21b8987ce..80fab9f2077 100644 --- a/pkg/workflow/model_costs_pricing_validation.go +++ b/pkg/workflow/model_costs_pricing_validation.go @@ -1,11 +1,19 @@ package workflow -import "fmt" +import ( + "fmt" + + "github.com/github/gh-aw/pkg/constants" +) // validateDefaultAiCreditsPricing returns an error when the workflow's -// models.default-ai-credits-pricing frontmatter is present and any price field -// has a non-positive value. Absent pricing (nil) is allowed; the check is only -// enforced once a value is explicitly configured. +// models.default-ai-credits-pricing frontmatter is present and either: +// - the effective AWF version is older than AWFDefaultAiCreditsPricingMinVersion +// (the field is silently dropped during config resolution in older versions), or +// - any price field has a non-positive value. +// +// Absent pricing (nil) is allowed; both checks are only enforced once a value +// is explicitly configured. // // The AWF api-proxy rejects zero rates as "not configured", so requiring // positive values here prevents silent runtime failures for self-hosted models. @@ -14,6 +22,16 @@ func validateDefaultAiCreditsPricing(workflowData *WorkflowData) error { if p == nil { return nil } + firewallConfig := getFirewallConfig(workflowData) + if !awfSupportsDefaultAiCreditsPricing(firewallConfig) { + awfTag := getAWFImageTag(firewallConfig) + return NewValidationError( + "models.default-ai-credits-pricing", + awfTag, + fmt.Sprintf("requires AWF %s or newer; pinned version %q drops apiProxy.defaultAiCreditsPricing during config resolution", constants.AWFDefaultAiCreditsPricingMinVersion, awfTag), + fmt.Sprintf("Set network.firewall.version or sandbox.agent.version to %s or newer:\n\nnetwork:\n firewall:\n version: %s", constants.AWFDefaultAiCreditsPricingMinVersion, constants.AWFDefaultAiCreditsPricingMinVersion), + ) + } if p.Input <= 0 { return fmt.Errorf("models.default-ai-credits-pricing: input must be a positive value (got %g); use a small positive rate such as 0.000001 for effectively-free self-hosted models", p.Input) } diff --git a/pkg/workflow/model_costs_pricing_validation_test.go b/pkg/workflow/model_costs_pricing_validation_test.go index 8cee1759303..665e6944d7b 100644 --- a/pkg/workflow/model_costs_pricing_validation_test.go +++ b/pkg/workflow/model_costs_pricing_validation_test.go @@ -5,6 +5,7 @@ package workflow import ( "testing" + "github.com/github/gh-aw/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,6 +26,57 @@ func TestValidateDefaultAiCreditsPricing(t *testing.T) { require.NoError(t, err) }) + t.Run("pinned AWF version with config resolution bug is rejected", func(t *testing.T) { + err := validateDefaultAiCreditsPricing(&WorkflowData{ + DefaultAiCreditsPricing: &AiCreditsPricingConfig{ + Input: 3.0, + Output: 15.0, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + Version: "v0.27.42", + }, + }, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), string(constants.AWFDefaultAiCreditsPricingMinVersion)) + assert.Contains(t, err.Error(), "drops apiProxy.defaultAiCreditsPricing") + }) + + t.Run("sandbox agent AWF version override with config resolution bug is rejected", func(t *testing.T) { + err := validateDefaultAiCreditsPricing(&WorkflowData{ + DefaultAiCreditsPricing: &AiCreditsPricingConfig{ + Input: 3.0, + Output: 15.0, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Version: "v0.27.42", + }, + }, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), string(constants.AWFDefaultAiCreditsPricingMinVersion)) + assert.Contains(t, err.Error(), "0.27.42") + }) + + t.Run("minimum AWF version is valid", func(t *testing.T) { + err := validateDefaultAiCreditsPricing(&WorkflowData{ + DefaultAiCreditsPricing: &AiCreditsPricingConfig{ + Input: 3.0, + Output: 15.0, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + Version: string(constants.AWFDefaultAiCreditsPricingMinVersion), + }, + }, + }) + require.NoError(t, err) + }) + t.Run("small positive values are valid for free models", func(t *testing.T) { err := validateDefaultAiCreditsPricing(&WorkflowData{ DefaultAiCreditsPricing: &AiCreditsPricingConfig{