Files
gbrain/test/anthropic-model-ids.test.ts
MasaandGitHub d9ac24744c fix(pricing): register claude-opus-5 in the Anthropic recipe allowlist and canonical pricing table (#3398)
Anthropic released Claude Opus 5, at the same $5/$25 pricing tier as
Opus 4.8. Neither the chat recipe allowlist nor CANONICAL_PRICING knew
about it, so operators could not opt into it via models.tier.deep /
models.default without gbrain rejecting the id.

- src/core/ai/recipes/anthropic.ts: add claude-opus-5 to the models list.
- src/core/model-pricing.ts: add anthropic:claude-opus-5 { input: 5.00,
  output: 25.00 } (plus cache rates, matching Opus 4.8's ratios).
- src/core/takes-quality-eval/pricing.ts: add it to SUPPORTED_MODELS so
  eval takes-quality run --budget-usd doesn't reject it during preflight.
- Refreshed the stale pricing-verification date and the Opus list in
  docs/architecture/KEY_FILES.md.
- Tests: pinned-value regression in test/model-pricing.test.ts, recipe
  membership in test/anthropic-model-ids.test.ts, budget-pricing coverage
  in test/eval-takes-quality-pricing.test.ts.

Scope: registration only. TIER_DEFAULTS / DEFAULT_ALIASES /
DEFAULT_CHAT_MODEL are untouched — default-routing bumps are the
separate, already-open #2858; this just makes the id valid/priced for
operators who opt in explicitly.
2026-07-27 13:28:37 -07:00

64 lines
2.9 KiB
TypeScript

import { describe, it, expect } from 'bun:test';
import { anthropic } from '../src/core/ai/recipes/anthropic.ts';
describe('Anthropic recipe model IDs', () => {
it('chat models use canonical Anthropic API IDs (no phantom dates)', () => {
const chatModels = anthropic.touchpoints?.chat?.models ?? [];
// claude-sonnet-4-6 is the correct API ID per Anthropic docs.
// The dated form claude-sonnet-4-6-20250929 returns 404 on the API.
expect(chatModels).toContain('claude-sonnet-4-6');
expect(chatModels).not.toContain('claude-sonnet-4-6-20250929');
});
it('expansion models use canonical IDs', () => {
const expansionModels = anthropic.touchpoints?.expansion?.models ?? [];
expect(expansionModels).toContain('claude-sonnet-4-6');
expect(expansionModels).not.toContain('claude-sonnet-4-6-20250929');
});
it('does not forward-alias dateless 4.6+ models (they ARE the canonical ID)', () => {
// Starting with Claude 4.6, Anthropic API IDs are dateless and pinned.
// No forward alias needed — the dateless form IS the model ID.
expect(anthropic.aliases?.['claude-sonnet-4-6']).toBeUndefined();
expect(anthropic.aliases?.['claude-opus-4-7']).toBeUndefined();
});
it('pre-4.6 models still have date-based aliases', () => {
// Haiku 4.5 predates the dateless convention, keeps its alias
expect(anthropic.aliases?.['claude-haiku-4-5']).toBe('claude-haiku-4-5-20251001');
});
it('reverse-alias rescues stale broken Sonnet 4.6 ID', () => {
// v0.31.6 shipped 'claude-sonnet-4-6-20250929' as a hardcoded default.
// Users with stale config (models.dream.synthesize, facts.extraction_model)
// must keep working — the reverse alias rewrites broken → canonical.
expect(anthropic.aliases?.['claude-sonnet-4-6-20250929']).toBe('claude-sonnet-4-6');
});
it('current-generation models are listed for chat (Fable 5 / Opus 5 / Opus 4.8 / Sonnet 5)', () => {
// Regression guard for the tier-config incident: a brain with
// `models.tier.deep = anthropic:claude-opus-4-8` had think/auto_think
// silently degrade because the recipe list stopped at Opus 4.7.
const chatModels = anthropic.touchpoints?.chat?.models ?? [];
expect(chatModels).toContain('claude-fable-5');
expect(chatModels).toContain('claude-opus-5');
expect(chatModels).toContain('claude-opus-4-8');
expect(chatModels).toContain('claude-sonnet-5');
});
it('Sonnet 5 is listed for expansion', () => {
expect(anthropic.touchpoints?.expansion?.models ?? []).toContain('claude-sonnet-5');
});
it('all listed models follow naming conventions', () => {
const allModels = [
...(anthropic.touchpoints?.chat?.models ?? []),
...(anthropic.touchpoints?.expansion?.models ?? []),
];
for (const m of allModels) {
// No model should contain a date that doesn't exist on the Anthropic API
expect(m).not.toMatch(/claude-sonnet-4-6-\d{8}/);
}
});
});