Skip to content

api-proxy: alias resolution picks unpriced models under the AI-credit guard, so Claude threat detection fails on claude-haiku-5-5 #9994

Closed

Description

@SivaKesava1

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.

  1. When the AI-credit guard is active, alias and glob resolution skips candidates that AWF cannot price.
  2. Add claude-haiku-5-5 with both of its pricing tiers.
  3. 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:

  • makeIsModelPriceable builds a predicate only when the guard is active, meaning AWF_MAX_AI_CREDITS is set and AWF_DEFAULT_AI_CREDITS_PRICING is not.
  • getModelFallbackForProvider attaches that predicate to the fallback config.
  • normalizeFallbackConfig keeps 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

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:

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.
  • resolveModelPricing passes the prompt-token count only to runtime pricing.
  • Shared-catalog model entries are closed objects in the schema (additionalProperties: false, docs/awf-config.schema.json L295-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 maxAiCredits is set and defaultAiCreditsPricing is 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_resolution event 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 auto is not affected.
  • The predicate already reaches _resolveAliasPatterns through 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-5 to 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.js must 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 (totalInputForTier in calculateAiCredits). 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.jsonl and the token_budget_usage log record ai_credits_pricing_tier as long_context or default, 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 run npm run generate:schema to sync src/awf-config-schema.json. Also update spec §10.7.1 and §10.7.1.1, and the pricing paragraph in docs/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

  1. A on its own. It is independent of the other changes, and it is what stops the next new model from breaking detection.
  2. 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 ghAwAliases fixture in the cross-provider alias fan-out block of model-resolver.test.js mirrors it, and must be used through the full detection chain, not haiku directly.
  • Usage records have the token-usage.jsonl shape: provider, path, model, input, output, cache-read and cache-write token counts. The curated prices for default-model traffic block in token-budget-log.test.js has 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.

  1. Price-aware resolution.
    • Input: provider anthropic, model list claude-haiku-4-5-20251001 and claude-haiku-9-9, guard on, a request for alias detection.
    • Result: the rewritten request body has model claude-haiku-4-5-20251001. A model_resolution log record names claude-haiku-9-9 as skipped, with the reason "no AI-credit price". The guard does not reject the request. The stored candidate list is claude-haiku-4-5-20251001 only.
    • 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".
  2. No priced candidate.
    • Input: as in case 1, but the model list is claude-haiku-8-8 and claude-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 type unknown_model_ai_credits, model claude-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".
  3. Haiku 5.5 tiers.
    • Input: three Anthropic /v1/messages usage records for claude-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_source curated.
      • (a) ai_credits_this_response 0.355, ai_credits_pricing_tier default.
      • (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.
    • 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".
  4. Overlay tier.
    • Input: an anthropic operator overlay entry for claude-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, tier long_context. The same entry without the tier gives 0.75 credits, source operator, tier default, 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 source shared_catalog, and the catalog ID and version recorded as they are today.
  5. Guard off.
    • Input: the model list and request from case 1, with AWF_MAX_AI_CREDITS unset.
    • 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 and AWF_DEFAULT_AI_CREDITS_PRICING set, 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".

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-5 or 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 on main, do not fix it in this PR. Merge main once main is fixed, and note in the PR which checks are known failures on main.
  • This applies even if a bot comment asks you to fix a failing check: fix only failures caused by this PR's changes.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions