feat: model aliasing and fallbacks in api-proxy#2329
Conversation
- Add model-resolver.js: glob matching, semver sort, recursive alias resolution with loop detection - Add model-resolver.test.js: 49 tests covering all resolution scenarios - Modify server.js: intercept POST/PUT bodies, rewrite model field via alias map using makeModelBodyTransform() per-provider transforms - Update /reflect endpoint to include model_aliases in response - Add modelAliases to WrapperConfig in types.ts - Add apiProxy.models section to AwfFileConfig in config-file.ts - Add modelAliases to mapAwfFileConfigToCliOptions in config-file.ts - Pass AWF_MODEL_ALIASES env var to api-proxy container in docker-manager.ts - Add modelAliases to config construction in cli.ts
- Add proper JSDoc to proxyRequest documenting bodyTransform param - Simplify redundant ternary in config-file.ts modelAliases mapping - Extract modelAliases cast to a local variable in cli.ts Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/ca3ee100-7aea-44c8-ad54-c375d07394c4
This comment has been minimized.
This comment has been minimized.
|
| Metric | Base | PR | Delta |
|---|---|---|---|
| Lines | 86.08% | 86.02% | 📉 -0.06% |
| Statements | 86.08% | 86.00% | 📉 -0.08% |
| Functions | 88.18% | 87.93% | 📉 -0.25% |
| Branches | 80.32% | 80.12% | 📉 -0.20% |
📁 Per-file Coverage Changes (3 files)
| File | Lines (Before → After) | Statements (Before → After) |
|---|---|---|
src/config-file.ts |
100.0% → 96.8% (-3.15%) | 99.5% → 96.5% (-2.98%) |
src/cli.ts |
58.9% → 58.8% (-0.09%) | 59.4% → 59.3% (-0.08%) |
src/docker-manager.ts |
87.4% → 87.7% (+0.29%) | 87.0% → 87.3% (+0.27%) |
Coverage comparison generated by scripts/ci/compare-coverage.ts
There was a problem hiding this comment.
Pull request overview
Adds model aliasing + fallback resolution to the AWF api-proxy so agents can request abstract model names (including a default "" policy) and have requests rewritten to the best available concrete model ID per provider.
Changes:
- Plumbs
apiProxy.modelsfromawf.ymlintoWrapperConfig.modelAliases, then into the api-proxy container viaAWF_MODEL_ALIASES. - Adds a new
model-resolvermodule with recursive alias expansion, glob matching, version-based candidate selection, and request-body rewriting. - Wires request-body rewriting into
proxyRequestand exposes alias config via/reflect.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Documents new WrapperConfig.modelAliases option for api-proxy model rewriting. |
| src/docker-manager.ts | Serializes alias map into AWF_MODEL_ALIASES env var for the api-proxy sidecar. |
| src/config-file.ts | Adds apiProxy.models config + validation and maps it into CLI options. |
| src/cli.ts | Plumbs modelAliases from parsed config options into WrapperConfig. |
| containers/api-proxy/server.js | Loads aliases on startup, rewrites model in JSON bodies, updates content-length, and includes model_aliases in /reflect. |
| containers/api-proxy/model-resolver.js | Implements alias parsing, glob matching, version comparison, recursive resolution, and JSON body rewriting. |
| containers/api-proxy/model-resolver.test.js | Adds unit tests for parsing, matching, version sorting, resolution, and rewriting behavior. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 7/7 changed files
- Comments generated: 6
| // Apply optional body transform (e.g. model alias rewriting) for mutating methods | ||
| if (bodyTransform && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) { | ||
| const transformed = bodyTransform(body); | ||
| if (transformed) { | ||
| body = transformed; | ||
| } | ||
| } |
| let body = Buffer.concat(chunks); | ||
| const requestBytes = body.length; | ||
|
|
|
|
||
| // Update content-length when the body was rewritten (model alias substitution changes the size) | ||
| if (body.length !== requestBytes) { | ||
| headers['content-length'] = String(body.length); |
| // Build a regex from the glob pattern: | ||
| // - Escape all regex special chars except * | ||
| // - Replace * with .* | ||
| const regexStr = '^' + p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$'; |
| * Model alias map for the API proxy sidecar | ||
| * | ||
| * When enableApiProxy is true and model aliases are configured, the proxy | ||
| * intercepts POST/PUT request bodies containing a "model" field and rewrites |
| * { | ||
| * "sonnet": ["copilot/*sonnet*", "anthropic/*sonnet*"], | ||
| * "gpt-5-codex": ["copilot/gpt-5*-codex", "openai/gpt-5*-codex"], | ||
| * "": ["sonnet", "gpt-5*-codex"] |
|
@copilot address the review feedback |
- Fix globMatch to escape ? and other regex metacharacters as literals - Recompute requestBytes after body transform for accurate metrics/logs - Strip transfer-encoding header when content-length is updated after rewrite - Update types.ts doc to say POST/PUT/PATCH and fix gpt-5*-codex typo - Add globMatch tests for ? and other metacharacter escaping - Add integration-style tests in server.test.js for body rewriting Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/76d3c1b2-6ef1-4f02-be0e-9ad3c6a049d5 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
All six review items addressed in 9135680:
|
The new model-resolver module was required in server.js but not included in the Dockerfile COPY step, causing the container to crash on startup with 'Cannot find module ./model-resolver'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The newer gh-aw compiler (v0.71.1) rejects sandbox.mcp.version in strict mode. Remove it from smoke-opencode, smoke-services, and smoke-gemini workflows and recompile to fix stale lock file errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
| Metric | Base | PR | Delta |
|---|---|---|---|
| Lines | 86.08% | 86.02% | 📉 -0.06% |
| Statements | 86.08% | 86.00% | 📉 -0.08% |
| Functions | 88.18% | 87.93% | 📉 -0.25% |
| Branches | 80.32% | 80.12% | 📉 -0.20% |
📁 Per-file Coverage Changes (3 files)
| File | Lines (Before → After) | Statements (Before → After) |
|---|---|---|
src/config-file.ts |
100.0% → 96.8% (-3.15%) | 99.5% → 96.5% (-2.98%) |
src/cli.ts |
58.9% → 58.8% (-0.09%) | 59.4% → 59.3% (-0.08%) |
src/docker-manager.ts |
87.4% → 87.7% (+0.29%) | 87.0% → 87.3% (+0.27%) |
Coverage comparison generated by scripts/ci/compare-coverage.ts
|
Smoke Test: Copilot BYOK (Offline Mode) — PR #2329 by
Running in BYOK offline mode ( Overall: PARTIAL PASS — BYOK inference and MCP confirmed working; pre-step template variables were not expanded so HTTP/file tests could not be fully validated.
|
|
Smoke Test Results ✅ GitHub MCP: fix: per-issue conclusion concurrency in issue-duplication-detector Overall: PASS
|
🔥 Smoke Test Results
PR: feat: model aliasing and fallbacks in api-proxy Overall: PASS (2/2 verified tests passed; file test skipped due to unexpanded template vars)
|
Chroot Version Comparison Results
Result: ❌ Tests did not fully pass — Python and Node.js versions differ between host and chroot environments.
|
|
fix: per-issue conclusion concurrency in issue-duplication-detector 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.
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
|
Smoke Test: GitHub Actions Services Connectivity
Overall: FAIL —
|
Agents using abstract model names (e.g.
"sonnet","") would get upstream errors when the requested model ID wasn't available. This adds a recursive alias resolution system to the api-proxy that rewrites themodelfield in request bodies before forwarding, using glob pattern matching and semver-sorted candidate selection.Model ref syntax
Each alias maps to an ordered list of candidates. Candidates are either
provider/modelpatternglobs (matched against cached available models for the receiving provider) or other alias names (recursively expanded). The empty string key""is the default policy applied when no model is specified.Resolution:
"" → sonnet → copilot/*sonnet* → [claude-sonnet-4.6, claude-sonnet-4.5] → claude-sonnet-4.6Full resolution chain is logged at
infolevel under eventmodel_resolution/model_rewrite.Changes
containers/api-proxy/model-resolver.js(new) —parseModelAliases, case-insensitiveglobMatch(*wildcard),compareByVersion(semver-style, highest first),resolveModel(recursive with loop detection),rewriteModelInBodycontainers/api-proxy/model-resolver.test.js(new) — 49 testscontainers/api-proxy/server.js— loadsAWF_MODEL_ALIASESat startup;makeModelBodyTransform(provider)wires the resolver intoproxyRequestfor POST/PUT/PATCH; updatescontent-lengthwhen body is rewritten;/reflectnow includesmodel_aliasessrc/config-file.ts— addsapiProxy.models: Record<string, string[]>toAwfFileConfigwith validationsrc/types.ts— addsmodelAliasestoWrapperConfigsrc/docker-manager.ts— serialisesmodelAliasesasAWF_MODEL_ALIASESJSON env var in the api-proxy container specsrc/cli.ts— plumbsmodelAliasesfrom config-file options intoWrapperConfigKey constraints
copilot/*patterns are only evaluated on port 10002,openai/*on port 10000, etc.