feat: max-model-multiplier-cap guardrail#4215
Conversation
Add a new guard that rejects API requests whose model's resolved cost multiplier exceeds the operator-configured cap (AWF_MAX_MODEL_MULTIPLIER). Changes: - containers/api-proxy/guards/max-model-multiplier-guard.js: new guard module - containers/api-proxy/proxy-request.js: extractModelFromBody(), guard integration - src/types/rate-limit-options.ts: maxModelMultiplier?: number field - src/config-file.ts: config-file mapping for apiProxy.maxModelMultiplier - src/services/api-proxy-service.ts: AWF_MAX_MODEL_MULTIPLIER env var passthrough - src/commands/build-config.ts: BuildConfigInputs.maxModelMultiplier? - src/commands/validators/log-and-limits.ts: parsing/validation - src/commands/validators/config-assembly.ts: passthrough to buildConfig - docs/awf-config.schema.json + src/awf-config-schema.json: schema field - Tests: guard unit tests, server integration tests, TS config/service tests
|
| Metric | Base | PR | Delta |
|---|---|---|---|
| Lines | 96.42% | 96.42% | ➡️ +0.00% |
| Statements | 96.28% | 96.28% | ➡️ +0.00% |
| Functions | 98.27% | 98.27% | ➡️ +0.00% |
| Branches | 90.53% | 90.45% | 📉 -0.08% |
📁 Per-file Coverage Changes (2 files)
| File | Lines (Before → After) | Statements (Before → After) |
|---|---|---|
src/commands/validators/log-and-limits.ts |
91.5% → 88.5% (-3.02%) | 91.5% → 88.5% (-3.02%) |
src/config-writer.ts |
89.3% → 90.9% (+1.65%) | 89.3% → 90.9% (+1.65%) |
Coverage comparison generated by scripts/ci/compare-coverage.ts
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR adds a “max model multiplier” spend guardrail to the API proxy: if a request’s resolved model multiplier exceeds a configured cap, the proxy rejects it with 400 model_multiplier_cap_exceeded before contacting upstream. It wires the cap from AWF config → validation/build-config → AWF_MAX_MODEL_MULTIPLIER env var, and integrates the new guard into proxy-request with targeted unit/integration tests.
Changes:
- Introduces
maxModelMultiplierconfiguration (schema + config-file mapping + validator + build-config) and injects it into the api-proxy container asAWF_MAX_MODEL_MULTIPLIER. - Implements
max-model-multiplier-guardand plugs it intocontainers/api-proxy/proxy-request.js(including request-body model extraction). - Adds test coverage for env-var wiring and for the new guard behavior in both guard-level and proxyRequest-level tests.
Show a summary per file
| File | Description |
|---|---|
| src/types/rate-limit-options.ts | Adds maxModelMultiplier?: number to rate-limit options and documents the new guardrail behavior. |
| src/services/api-proxy-service.ts | Injects AWF_MAX_MODEL_MULTIPLIER into the api-proxy container environment when configured. |
| src/services/api-proxy-service-rate-limit.test.ts | Verifies AWF_MAX_MODEL_MULTIPLIER is set/unset correctly in generated docker-compose env. |
| src/config-file.ts | Adds apiProxy.maxModelMultiplier to config-file type + maps it into CLI/options assembly. |
| src/config-file-validation.test.ts | Adds config-schema validation tests for apiProxy.maxModelMultiplier. |
| src/commands/validators/log-and-limits.ts | Adds validation/parsing for maxModelMultiplier in the options pipeline (but currently conflicts with an existing CLI option name). |
| src/commands/validators/config-assembly.ts | Plumbs maxModelMultiplier from log/limits validation into assembled config. |
| src/commands/build-config.ts | Adds maxModelMultiplier to WrapperConfig assembly. |
| src/awf-config-schema.json | Adds apiProxy.maxModelMultiplier schema field (plus large formatting churn). |
| docs/awf-config.schema.json | Mirrors schema update for docs (plus large formatting churn). |
| containers/api-proxy/server.token-guards.test.js | Adds proxyRequest-level tests for the new max-model-multiplier guard behavior. |
| containers/api-proxy/proxy-request.js | Extracts model from the (post-transform) request body and applies the new guard before upstream request. |
| containers/api-proxy/guards/max-model-multiplier-guard.test.js | Adds unit tests for multiplier resolution, cache behavior, and error payload structure. |
| containers/api-proxy/guards/max-model-multiplier-guard.js | Implements config parsing/caching, multiplier resolution (exact/longest-prefix/default), and error payload builder. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 14/14 changed files
- Comments generated: 2
| const maxModelMultiplierOption = (options as Record<string, unknown>).maxModelMultiplier as | ||
| | string | ||
| | number | ||
| | undefined; | ||
| const maxModelMultiplier = |
| /** | ||
| * Returns a block state object when the given model's resolved multiplier | ||
| * exceeds the configured cap (AWF_MAX_MODEL_MULTIPLIER), or null when no cap | ||
| * is configured or the model is below the cap. | ||
| * | ||
| * @param {string|null} model - The model name from the request body (may be null) | ||
| * @returns {{ model: string, multiplier: number, maxModelMultiplier: number, | ||
| * maxExceeded: boolean } | null} | ||
| */ |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@copilot address review feedback and make sure that any new configuration params are thoroughly described in teh specs and schema. non-security sensitive config params should be passed through stdin config and security sensitive through environment variables |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Addressed in commit Naming conflict (reviewer feedback): Renamed the multiplier cap option from JSDoc mismatch (reviewer feedback): Spec & schema coverage: |
|
| Metric | Base | PR | Delta |
|---|---|---|---|
| Lines | 96.42% | 96.42% | ➡️ +0.00% |
| Statements | 96.28% | 96.28% | ➡️ +0.00% |
| Functions | 98.27% | 98.27% | ➡️ +0.00% |
| Branches | 90.53% | 90.45% | 📉 -0.08% |
📁 Per-file Coverage Changes (2 files)
| File | Lines (Before → After) | Statements (Before → After) |
|---|---|---|
src/commands/validators/log-and-limits.ts |
91.5% → 88.5% (-3.02%) | 91.5% → 88.5% (-3.02%) |
src/config-writer.ts |
89.3% → 90.9% (+1.65%) | 89.3% → 90.9% (+1.65%) |
Coverage comparison generated by scripts/ci/compare-coverage.ts
|
Smoke Test: Copilot BYOK (Offline) Mode
Running in BYOK offline mode ( PR by
|
✅ Smoke Test: Claude Engine
Total: PASS
|
🔬 Smoke Test Results
PR: feat: max-model-multiplier-cap guardrail Overall: PARTIAL PASS — MCP ✅, pre-step data unavailable for HTTP/file tests.
|
|
refactor(api-proxy): extract duplicate collectLogOutput() into shared test helper ✅ Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "registry.npmjs.org"See Network Configuration for more information.
|
Smoke Test: API Proxy OpenTelemetry Tracing
All 5 scenarios passed. OTEL tracing integration is working correctly.
|
Chroot Version Comparison
Overall: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot.
|
|
Smoke test results: FAIL Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "localhost"See Network Configuration for more information.
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
|
Smoke Test: GitHub Actions Services Connectivity
Overall: FAIL —
|
Adds a hard cap on model cost multipliers so unexpected pricing or model-routing changes can't silently blow up spend. Requests whose resolved model multiplier exceeds the configured cap are rejected
400 model_multiplier_cap_exceededbefore reaching upstream.Guard module (
containers/api-proxy/guards/max-model-multiplier-guard.js)effective-token-guardlogic)null(no-op when unconfigured or multiplier is within the cap)Proxy integration (
containers/api-proxy/proxy-request.js)extractModelFromBody()to pull the model field from the post-transform request bodyConfig pipeline
apiProxy.maxModelMultiplierCap(number) in AWF config file → validated inlog-and-limits.ts→ passed throughbuild-config→AWF_MAX_MODEL_MULTIPLIERenv var injected into the api-proxy containermaxEffectiveTokensandmaxRuns--max-model-multiplier-cap <number>CLI flag added; kept separate from the existing--max-model-multiplier <model:multiplier,...>per-model map flag to avoid key/type collisionSchema
maxModelMultiplierCap: { type: "number", exclusiveMinimum: 0 }added to bothdocs/awf-config.schema.jsonandsrc/awf-config-schema.jsonSpec & docs
docs/awf-config-spec.md§12 "Model Multiplier Cap" — enforcement rules, multiplier resolution algorithm, and error shapedocs/api-proxy-sidecar.md— configuration examples, enforcement description, and detection code snippetExample config