-
Notifications
You must be signed in to change notification settings - Fork 472
Propagate models.providers pricing into AWF apiProxy (including threat-detection runs)
#48107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
20f0dd6
0d8a72b
de6116e
a25ee88
a45e534
70de462
1296ec0
4ba7ec5
1fc2916
e76f1e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # ADR-48107: Propagate `models.providers` Pricing into AWF `apiProxy` Config | ||
|
|
||
| **Date**: 2026-07-26 | ||
| **Status**: Draft | ||
| **Deciders**: Unknown | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| The AWF runtime enforces AI-credits consumption through an API proxy guardrail that resolves per-model pricing at request time. Pricing data for built-in models is embedded in the proxy's static catalog. When users configure custom or BYOK models (e.g., via `models.providers` overrides), those pricing entries are registered only in the agent-side catalog; the API proxy evaluates a separately generated `awf-config.json` that contains no provider pricing, causing every custom-model request to fail at turn 1 with `unknown_model_ai_credits`. The same data flow gap exists for threat-detection runs, which construct their own `WorkflowData` from the primary run's data without propagating `ModelCosts`. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will embed `models.providers` pricing overlays from `WorkflowData.ModelCosts` into the generated `awf-config.json` under the `apiProxy.providers` key, and propagate `ModelCosts` into the `WorkflowData` constructed for threat-detection runs. This makes the API proxy's pricing resolver self-contained within the AWF config it receives, removing the dependency on a separate catalog lookup at proxy evaluation time. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Shared Pricing Service / Catalog Query at Proxy Evaluation Time | ||
|
|
||
| The API proxy could query a shared in-process or network pricing service at request evaluation time rather than reading pricing from the config. This would decouple pricing data from the serialized config artifact. | ||
|
|
||
| Not chosen because it would require a new inter-component dependency (the proxy calling back into the agent process or an external service), adding latency on every guarded request and significant complexity to both the proxy and the agent runtime. The config-embedding approach requires no new interfaces. | ||
|
|
||
| #### Alternative 2: Default Fallback Cost for Unknown Models | ||
|
|
||
| The API proxy could apply a configurable default cost for any model not found in its pricing table, rather than failing with `unknown_model_ai_credits`. | ||
|
|
||
| Not chosen because a silent fallback would allow under-accounting of AI-credit consumption for BYOK models — a correctness and auditability concern. Users configure explicit pricing precisely to get accurate accounting. Propagating actual pricing preserves correctness without requiring any policy decisions about default values. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Custom and BYOK models resolve pricing correctly at the API proxy layer, eliminating `unknown_model_ai_credits` failures for all runs using `models.providers` overrides. | ||
| - Threat-detection runs gain pricing parity with primary agent runs, ensuring that detection phases do not fail or under-account when a custom model is configured. | ||
| - The AWF config is self-contained: all information the proxy needs to evaluate a request is present in the config artifact, simplifying debugging and offline replay. | ||
| - A focused test suite regression-guards both paths (main `BuildAWFConfigJSON` and detection step generation). | ||
|
|
||
| #### Negative | ||
| - The `apiProxy.providers` field is typed as `map[string]any`, offering no compile-time validation of pricing structure; malformed cost entries (e.g., non-numeric strings) would be forwarded to the proxy silently. | ||
| - Each `awf-config.json` artifact now embeds a copy of the provider pricing catalog, increasing config size in proportion to the number of custom models configured. | ||
|
|
||
| #### Neutral | ||
| - The AWF config JSON schema (`awf-config.schema.json`) is extended to allow `apiProxy.providers` with `additionalProperties: true`, preventing schema-validation rejection of the new field while intentionally not constraining the nested pricing shape. | ||
| - Existing runs with no `models.providers` configuration are unaffected: `extractModelCostProviders` returns `nil` when `ModelCosts` is empty or has no `providers` key. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,7 @@ import ( | |
| _ "embed" | ||
| "encoding/json" | ||
| "fmt" | ||
| "maps" | ||
| "slices" | ||
| "strconv" | ||
| "strings" | ||
|
|
@@ -259,6 +260,12 @@ type AWFAPIProxyConfig struct { | |
| // The "gemini" target is also used for Antigravity engine routing. | ||
| Targets map[string]*AWFAPITargetConfig `json:"targets,omitempty"` | ||
|
|
||
| // Providers holds per-provider model pricing overlays used by the API proxy | ||
| // AI-credits guardrails for models not present in the built-in pricing table. | ||
| // Structure matches models.json provider format: | ||
| // providers.<provider>.models.<model>.cost.{input,output,cache_read,cache_write,reasoning} | ||
| Providers map[string]any `json:"providers,omitempty"` | ||
|
|
||
| // Models contains model alias and fallback policy definitions. | ||
| // Keys are alias names (empty string "" = default policy); values are ordered | ||
| // lists of vendor/modelid patterns or other alias names to try in sequence. | ||
|
|
@@ -602,6 +609,15 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { | |
| awfConfigLog.Printf("API proxy: %d custom targets configured", len(targets)) | ||
| } | ||
|
|
||
| if providers := extractModelCostProviders(config.WorkflowData); len(providers) > 0 { | ||
| if awfSupportsAPIProxyProviders(firewallConfig) { | ||
| apiProxy.Providers = providers | ||
| awfConfigLog.Printf("API proxy: %d model-cost provider override(s) configured", len(providers)) | ||
| } else { | ||
| awfConfigLog.Printf("Skipping apiProxy.providers: AWF version %q requires at least %s", getAWFImageTag(firewallConfig), constants.AWFAPIProxyProvidersMinVersion) | ||
| } | ||
| } | ||
|
|
||
| // ── Models section (nested under apiProxy per AWF config schema) ────────── | ||
| if config.WorkflowData != nil && len(config.WorkflowData.ModelMappings) > 0 { | ||
| apiProxy.Models = config.WorkflowData.ModelMappings | ||
|
|
@@ -866,6 +882,23 @@ func extractDefaultAiCreditsPricing(workflowData *WorkflowData) *AiCreditsPricin | |
| } | ||
| } | ||
|
|
||
| func extractModelCostProviders(workflowData *WorkflowData) map[string]any { | ||
| if workflowData == nil || len(workflowData.ModelCosts) == 0 { | ||
| return nil | ||
| } | ||
| providers, ok := workflowData.ModelCosts["providers"].(map[string]any) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] The type assertion 💡 Suggested hardeningAdd a log line in the providers, ok := workflowData.ModelCosts["providers"].(map[string]any)
if !ok {
awfConfigLog.Printf("API proxy: models.providers has unexpected type %T; skipping provider overlay",
workflowData.ModelCosts["providers"])
return nil
}This surfaces future type mismatches at runtime instead of masking them. @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| if !ok { | ||
| awfConfigLog.Printf("API proxy: models.providers has unexpected type %T; skipping provider overlay", workflowData.ModelCosts["providers"]) | ||
| return nil | ||
| } | ||
| if len(providers) == 0 { | ||
| return nil | ||
| } | ||
| clone := make(map[string]any, len(providers)) | ||
| maps.Copy(clone, providers) | ||
| return clone | ||
| } | ||
|
|
||
| // getRunnerTopology extracts the runner topology string from WorkflowData. | ||
| // Returns an empty string when no topology is configured. | ||
| func getRunnerTopology(workflowData *WorkflowData) string { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/diagnosing-bugs]
Providersis typedmap[string]any, which gives no compile-time safety and makes it easy for callers to pass structurally wrong data that only fails at AWF runtime. Consider defining a typed struct (e.g.AWFProviderConfig) mirroring the models.json provider shape.💡 Why this matters
Other fields in this struct use concrete types (
AWFAPITargetConfig,AiCreditsPricingConfig, etc.). Usingmap[string]anyhere is inconsistent and makes the shape opaque to future readers. A typed struct:AWFAPITargetConfighandles targetsIf the shape needs to remain flexible, at minimum add a doc comment listing the expected keys.
@copilot please address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept
Providersasmap[string]anyintentionally in1fc2916because this field forwards the upstream AWF provider catalog shape as-is and that shape is intentionally open-ended across providers/models. I added stricter behavioral guardrails instead (version gating, type-check logging, and extraction hardening) so unsupported or malformed input fails safely.