Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/constants/version_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 22 additions & 4 deletions pkg/workflow/model_costs_pricing_validation.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -14,6 +22,16 @@ func validateDefaultAiCreditsPricing(workflowData *WorkflowData) error {
if p == nil {
return nil
}
firewallConfig := getFirewallConfig(workflowData)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] getAWFImageTag strips the v prefix (returns 0.27.42 not v0.27.42), so the error message pinned-version quote won't match what users typed in their YAML. Consider using firewallConfig.Version directly in the error string to preserve fidelity.

The second regression test already asserts Contains(err.Error(), "0.27.42") without v, so it passes either way — but users seeing the CLI error will be confused if their YAML says v0.27.42 and the message echoes 0.27.42.

@copilot please address this.

if !awfSupportsDefaultAiCreditsPricing(firewallConfig) {
Comment on lines +25 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocks harmless configs: this check only inspects the AWF version, not whether the firewall/sandbox is actually enabled.

💡 Details

getFirewallConfig can return FirewallConfig{Enabled: false, Version: agentVersion} when the firewall is disabled via sandbox.agent: false (or network.firewall.enabled: false) but a version is still recorded (carried over from sandbox.agent.version). awfSupportsDefaultAiCreditsPricing only calls awfVersionAtLeast, ignoring firewallConfig.Enabled entirely.

Since DefaultAiCreditsPricing is only wired into the AWF api-proxy config inside BuildAWFConfigJSON, which is only invoked when isFirewallEnabled(workflowData) is true, this new compile-time check will reject models.default-ai-credits-pricing for workflows where the firewall/api-proxy is disabled — even though the feature is a no-op there and the config-resolution bug this PR fixes cannot occur.

Suggested fix:

firewallConfig := getFirewallConfig(workflowData)
if isFirewallEnabled(workflowData) && !awfSupportsDefaultAiCreditsPricing(firewallConfig) {
    return fmt.Errorf(...)
}

No test in this PR covers the "firewall disabled + old pinned version" case, so this gap goes unverified.

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)
}
Expand Down
52 changes: 52 additions & 0 deletions pkg/workflow/model_costs_pricing_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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{
Expand Down
Loading