Repository navigation
api-proxy: alias resolution picks unpriced models under the AI-credit guard, so Claude threat detection fails on claude-haiku-5-5 #9994
Description
Summary
On the Claude engine with the Anthropic provider, gh-aw threat detection requests the model alias detection unless the workflow pins a model. On 7 Oct Anthropic started listing claude-haiku-5-5. From the next run, AWF resolved detection to claude-haiku-5-5 instead of claude-haiku-4-5-20251001. AWF has no price for claude-haiku-5-5, so the AI-credit guard rejected every detection request with HTTP 400 unknown_model_ai_credits. Detection runs with continue-on-error, so those runs finished green with no threat verdict. Neither the lock file nor the CLI pin had changed. This was reported by benissimo in github/gh-aw#66852.
The gh-aw side is github/gh-aw#68818. It makes gh-aw honor the detection model variables, so users can pin a model. #9695 asked for a claude-haiku-5-5 price and was closed as not planned on 8 Oct. The AWF side is still open. A price would fix only this one model: alias resolution still picks the newest listed model whether or not AWF can price it, so the next new model will break detection in the same way.
This issue proposes three changes. The first stops the next new model from breaking detection.
- When the AI-credit guard is active, alias and glob resolution skips candidates that AWF cannot price.
- Add
claude-haiku-5-5with both of its pricing tiers. - Let operator overlay and shared-catalog entries carry a long-prompt tier, as Copilot runtime pricing already does.
Evidence (checked against main at b2fc295)
1. Alias resolution ignores pricing
gh-aw's alias table (pkg/workflow/data/model_aliases.json in github/gh-aw) expands detection → small → mini → haiku → anthropic/*haiku*. In AWF, _resolveAliasPatterns matches each provider glob against the live provider model list (L240-L253) and applies the model-policy filter (L266-L275). It then sorts with compareByVersion and takes the first candidate (L303-L307). compareByVersion compares every digit group in the model ID, so claude-haiku-5-5 (5, 5) sorts above claude-haiku-4-5-20251001 (4, 5, 20251001). Nothing on this path looks at pricing.
Each nested alias passes only its single winner up to its parent (L218-L239). The choice between the two Haikus is made inside haiku. The mini, small and detection levels see only claude-haiku-5-5. A price check that runs only on the top-level result would therefore have nothing to choose from. It has to run at every alias level.
I reproduced this on main. The input was gh-aw's alias table, an Anthropic model list containing both Haikus, and AWF_MAX_AI_CREDITS set. resolveModel returns claude-haiku-5-5. The resolution log shows haiku choosing between two candidates, and each parent level resolving to that one model. isModelPriceable returns true for claude-haiku-4-5-20251001 and false for claude-haiku-5-5.
AWF already has what it needs for a price-aware choice, but uses it only in the middle-power fallback:
makeIsModelPriceablebuilds a predicate only when the guard is active, meaningAWF_MAX_AI_CREDITSis set andAWF_DEFAULT_AI_CREDITS_PRICINGis not.getModelFallbackForProviderattaches that predicate to the fallback config.normalizeFallbackConfigkeeps the predicate whether or not fallback is enabled, so it already reaches_resolveAliasPatterns.- The only user is the soft price filter on the synthesized fallback pool (model-fallback.js L89-L108). Its comment says pattern-matched models are deliberately not filtered.
2. Unpriced models are rejected
When maxAiCredits is set and no default pricing is configured, checkUnknownModelRejection rejects any model whose default tier or highest selectable tier has no price. The guard check list turns that into HTTP 400 with event unknown_model_ai_credits (common-guard-checks.js L167-L176), as specified in docs/awf-config-spec.md §10.7.3. According to github/gh-aw#66852, gh-aw's detection step always sets maxAiCredits.
3. claude-haiku-5-5 has no price anywhere
- The curated table
ai-credits-pricing.jshas onlyclaude-haiku-4-5for Haiku. - The reference file added by Correct curated AI-credit model prices #9982,
ai-credits-pricing-reference.json, has onlyclaude-sonnet-5in its Anthropic section. - The bundled catalog
models.dev.catalog.jsonlists nothing newer thananthropic/claude-haiku-4.5for Haiku. - The model appears only in
model-api-mapping.json, which holds routing metadata, not prices.
A related observation: since #9791 (released in v0.28.51), curated and bundled lookups need an exact canonical model match (spec L1346-L1349). A family entry such as claude-sonnet-5 therefore no longer prices claude-sonnet-5-5. In the same reproduction, claude-sonnet-5-5 and claude-opus-5-5 are also unpriced, and the sonnet and opus aliases resolve to them when they are listed. The workflows in github/gh-aw#66852 priced both models in their overlay, which suggests they are listed. If so, an Anthropic workflow that uses sonnet or opus without an overlay hits the same rejection. Change 1 covers that case too. Adding those prices is not part of this issue.
4. Long-prompt pricing works only for Copilot
Haiku 5.5 bills prompts over 100k tokens (input + cache read + cache write) at 5× the base rates, output included. AWF can select a long-prompt tier only from Copilot's live model catalog:
normalizeTierandnormalizeCopilotPricing(runtime-model-catalog.js L35-L75) readlong_contextand the default tier'smax_prompt_tokensthreshold.resolveRuntimePricingswitches every token category to the long tier when the prompt is larger than that threshold, and reports the tier aslong_context. This is the rule in spec §10.7.1.token-usage.jsonlrecords the tier asai_credits_pricing_tier(token-budget-log.js L53).
The other pricing sources always return the default tier:
- The operator overlay (provider-pricing-overlays.js L69-L95) and the shared pricing catalog (model-pricing-catalog.js L57-L83) always return
tier: 'default'. So do the curated and bundled lookups. resolveModelPricingpasses the prompt-token count only to runtime pricing.- Shared-catalog model entries are closed objects in the schema (
additionalProperties: false,docs/awf-config.schema.jsonL295-L382), so they have nowhere to put a tier.
As a result, a long request on the Anthropic provider is priced at the base rates, and max-ai-credits under-counts it by up to 5×. This also affects the workaround in github/gh-aw#66852, a flat claude-haiku-5-5 overlay entry. I checked this on main with a 150,000-token prompt and that overlay. AWF charges 0.75 AI credits and records the default tier. The correct charge is 3.75.
Prices
Per 1M tokens, from Copilot /models billing.token_prices for claude-haiku-5.5 on 14 Oct. benissimo's table in github/gh-aw#66852 agrees.
| input | cache read | cache write | output | |
|---|---|---|---|---|
| prompt ≤ 100k tokens | $0.10 | $0.01 | $0.125 | $0.50 |
| prompt > 100k tokens | $0.50 | $0.05 | $0.625 | $2.50 |
Copilot rounds the cache-write rates to $0.12 and $0.62. Use Anthropic's published $0.125 and $0.625. Haiku 5.5 has no separate reasoning rate, so where an entry requires a reasoning rate, set it equal to the output rate (spec §10.7.1.1).
Intended behaviour
A. Skip unpriced candidates when the guard is active (the main fix)
- When the AI-credit guard is active, alias and glob resolution skips candidates that have no resolvable price. "Active" means
maxAiCreditsis set anddefaultAiCreditsPricingis not. - "Resolvable price" means the same check the guard uses: both the default tier and the highest tier resolve.
- The filter runs at every alias level, after the model-policy filter and before the version sort, and the highest-versioned priced match wins.
- Each skipped candidate gets a resolution log line naming the model, the alias being resolved, and the reason (no AI-credit price on this provider). The line is emitted as a
model_resolutionevent like the other resolution steps. - If no candidate at a level has a price, that level behaves as it does today: the highest version wins, and the guard rejects the request with
unknown_model_ai_credits. Like the existing fallback filter, this filter is soft. It must never turn a resolvable request into a resolution failure. - The ranked candidate list kept for endpoint-blocked retries (
req.awfModelCandidates) leaves out skipped candidates, so a retry cannot land on an unpriced model. - Nothing changes when the guard is off or default pricing is configured. Nothing changes for an explicit request for a concrete model: a direct request for an unpriced model is still rejected as it is today. Copilot
autois not affected. - The predicate already reaches
_resolveAliasPatternsthrough the fallback config. Reusing it keeps a single definition of "guard active".
B. Add a price for claude-haiku-5-5
- Add
claude-haiku-5-5to the curated table and to the Anthropic section of the reference file, with the base rates, the long-prompt rates and a threshold of 100,000 prompt tokens. - The curated table and the reference file both need to hold the long-prompt tier, so this change depends on the tier shape from C.
ai-credits-pricing-reference.test.jsmust also compare the long-prompt tier, not only the base rates.- Once B lands, detection on the Anthropic provider resolves to
claude-haiku-5-5, because it is then the highest priced Haiku, and it is priced correctly. Change A is what protects against the next unpriced model.
C. Long-prompt tier for operator overlays and the shared catalog
- Operator overlay and shared-catalog entries can carry an optional long-prompt tier. The tier is a prompt-token threshold plus a second full rate set. Each source uses the units of its base rates: per-token models.dev strings for the overlay, USD per 1M for the shared catalog. A shared-catalog tier also includes a reasoning rate.
- Tier selection reuses the rule that Copilot pricing already uses. When the prompt is larger than the threshold, every token category uses the long-prompt rates. At or below the threshold, the base rates apply.
- The prompt size is counted on the same token basis as today's Copilot tier selection (
totalInputForTierincalculateAiCredits). For Anthropic, and for Copilot usage that reports non-cached input, that is input plus cache read plus cache write. For usage whose input count already includes cached tokens, it is the reported input. token-usage.jsonland thetoken_budget_usagelog recordai_credits_pricing_tieraslong_contextordefault, the same values Copilot pricing uses.- Entries without a tier behave exactly as they do today.
- A malformed tier makes the entry invalid, as an invalid base rate does today. It must not silently fall back to the base rates.
- Update the schema in
docs/awf-config.schema.json, then runnpm run generate:schemato syncsrc/awf-config-schema.json. Also update spec §10.7.1 and §10.7.1.1, and the pricing paragraph indocs/api-proxy-sidecar.md(L964-L975).
D. Copilot catalog pricing does not change
Copilot runtime pricing keeps its current tier handling and precedence.
Suggested PRs
- A on its own. It is independent of the other changes, and it is what stops the next new model from breaking detection.
- C, then B, which needs C's tier shape. These can be one PR or two.
Acceptance cases
Fixtures. Build fixtures from real request and record shapes:
- Requests are Anthropic Messages bodies as Claude Code sends them, with the model set to the alias.
- Model lists use the IDs the proxy caches from Anthropic
GET /v1/models. - The alias table is gh-aw's table. The
ghAwAliasesfixture in thecross-provider alias fan-outblock ofmodel-resolver.test.jsmirrors it, and must be used through the fulldetectionchain, nothaikudirectly. - Usage records have the
token-usage.jsonlshape: provider, path, model, input, output, cache-read and cache-write token counts. Thecurated prices for default-model trafficblock intoken-budget-log.test.jshas an example.
Guard state. "Guard on" means AWF_MAX_AI_CREDITS=400, with no AWF_DEFAULT_AI_CREDITS_PRICING, no overlay and no shared catalog.
Unpriced model. Cases 1, 2 and 5 use claude-haiku-9-9 as the unpriced newer Haiku, so the tests stay valid after claude-haiku-5-5 is priced.
- Price-aware resolution.
- Input: provider
anthropic, model listclaude-haiku-4-5-20251001andclaude-haiku-9-9, guard on, a request for aliasdetection. - Result: the rewritten request body has model
claude-haiku-4-5-20251001. Amodel_resolutionlog record namesclaude-haiku-9-9as skipped, with the reason "no AI-credit price". The guard does not reject the request. The stored candidate list isclaude-haiku-4-5-20251001only. - Test:
server.models.test.js, "resolves the detection alias to the highest priced Haiku and logs the skipped unpriced model when the AI-credit guard is active".
- Input: provider
- No priced candidate.
- Input: as in case 1, but the model list is
claude-haiku-8-8andclaude-haiku-9-9, neither of them priced. - Result: the request resolves to
claude-haiku-9-9, as it does today. The guard rejects it with HTTP 400, error typeunknown_model_ai_credits, modelclaude-haiku-9-9, and today's message text. - Test:
server.models.test.js, "keeps the highest-versioned candidate and the unknown_model_ai_credits rejection when no alias candidate is priced".
- Input: as in case 1, but the model list is
- Haiku 5.5 tiers.
- Input: three Anthropic
/v1/messagesusage records forclaude-haiku-5-5:- (a) a 50k-token prompt: 10,000 input, 30,000 cache-read and 10,000 cache-write tokens, 2,000 output tokens;
- (b) a 150k-token prompt: 30,000 input, 100,000 cache-read and 20,000 cache-write tokens, 2,000 output tokens;
- (c) a prompt of exactly 100,000 tokens: 20,000 input, 70,000 cache-read and 10,000 cache-write tokens, 2,000 output tokens.
- Result: all three records have
ai_credits_pricing_sourcecurated.- (a)
ai_credits_this_response0.355,ai_credits_pricing_tierdefault. - (b) 3.75 credits, tier
long_context. At base rates this record would cost 0.75. - (c) 0.495 credits, tier
default, because the switch happens only above the threshold.
- (a)
- Test:
token-budget-log.test.js, "prices claude-haiku-5-5 at base rates up to 100k prompt tokens and at long-prompt rates above, recording the tier".
- Input: three Anthropic
- Overlay tier.
- Input: an
anthropicoperator overlay entry forclaude-haiku-5-5. Its base cost is per-token: 1e-07 input, 1e-08 cache read, 1.25e-07 cache write, 5e-07 output. Its long-prompt tier has a threshold of 100,000 and rates of 5e-07, 5e-08, 6.25e-07 and 2.5e-06. Use record (b) from case 3. - Result: 3.75 credits, source
operator, tierlong_context. The same entry without the tier gives 0.75 credits, sourceoperator, tierdefault, as it does today. - Test:
token-budget-log.test.js, "applies an operator overlay long-prompt tier above its threshold and keeps untiered overlay entries on the default tier". Add a matching shared-catalog test next to it. That test expects sourceshared_catalog, and the catalog ID and version recorded as they are today.
- Input: an
- Guard off.
- Input: the model list and request from case 1, with
AWF_MAX_AI_CREDITSunset. - Result: the request resolves to
claude-haiku-9-9(highest version, no pricing check), and no skipped-candidate log line is written. With the guard on andAWF_DEFAULT_AI_CREDITS_PRICINGset, the result is the same, because every model has a price. - Test:
server.models.test.js, "keeps highest-version alias resolution without a pricing check when the AI-credit guard is off".
- Input: the model list and request from case 1, with
Not in this issue
- gh-aw's handling of the detection model variables (fix: honor runtime detection model overrides gh-aw#68818).
- Prices for
claude-sonnet-5-5,claude-opus-5-5or any other model. - Any change to Copilot runtime catalog pricing.
- Any gh-aw compiler change to emit tiers in
modelPricingCatalog.
Scope
- Keep each PR to its fix. Do not change files or behavior unrelated to it.
- Before fixing a failing check, compare it with the same check on
main. If it also fails onmain, do not fix it in this PR. Mergemainoncemainis fixed, and note in the PR which checks are known failures onmain. - This applies even if a bot comment asks you to fix a failing check: fix only failures caused by this PR's changes.
References
- Claude threat detection follows the newest listed Haiku and fails open when it is unpriced; GH_AW_MODEL_DETECTION_CLAUDE never reaches the detection step gh-aw#66852: original report, with the timeline, logs and workaround.
- fix: honor runtime detection model overrides gh-aw#68818: gh-aw honors runtime detection model overrides.
- api-proxy: add claude-haiku-5-5 to the AI-credits price table #9695: add a
claude-haiku-5-5price (closed as not planned on 8 Oct). - Correct curated AI-credit model prices #9982: corrected curated prices and added
ai-credits-pricing-reference.json. - Add versioned shared model-pricing catalog support #9791: versioned shared pricing catalog; curated lookups now need an exact canonical match.
Activity