Skip to content

Agent SDK: export typed, orderable model constants with enumerated capabilities #276

@deathbots

Description

@deathbots

Agent SDK: export typed, orderable model constants with enumerated capabilities

Summary

The Agent SDK accepts model identifiers as bare string values ('opus', 'sonnet', 'haiku'). There are no exported constants, enums, or structured model metadata. Consumers who dispatch different SDK agents for different tasks (e.g., opus for adversarial review, sonnet for code fixes) must define their own string constants and manually track which models support which features.

Request: Export a structured, typed model registry from @anthropic-ai/claude-agent-sdk that enables:

  1. Referencing models by constant instead of bare string
  2. Ordering/sorting models by capability tier and cost
  3. Querying models by enumerated feature flags with exhaustive pattern matching
  4. Selecting models programmatically (e.g., "second-most-capable model that supports adaptive thinking")

Motivation

We build an autopilot system that dispatches 11+ Agent SDK agents per PR review cycle. Each agent role has different requirements — adversarial assessors need the strongest reasoning model, code fixers need a fast capable model, explore agents need the cheapest option. Today we maintain a hand-rolled model-tiers.ts with bare strings:

// We shouldn't have to maintain this
export const MODEL_OPUS = 'opus' as const;
export const MODEL_SONNET = 'sonnet' as const;
export const MODEL_HAIKU = 'haiku' as const;

This is connascence of meaning — multiple components agree on what magic strings mean, with no type system enforcement. If the SDK adds a new model family or renames an alias, consumers discover the breakage at runtime.

Proposed API

1. Exported model constants

// Importable from '@anthropic-ai/claude-agent-sdk'
export const Models = {
  OPUS: { id: 'opus', ... },
  SONNET: { id: 'sonnet', ... },
  HAIKU: { id: 'haiku', ... },
} as const;

2. Orderable capability tier (numeric enum)

TypeScript has no Comparable interface, but numeric values are naturally orderable with <, >, and Array.prototype.sort():

/** Higher number = more capable / more expensive. */
export enum ModelTier {
  /** Lightweight, fast, cheapest. */
  Economy = 0,
  /** Balanced capability and cost. */
  Standard = 1,
  /** Maximum reasoning capability. */
  Frontier = 2,
}

This enables:

// "Give me the second-most-capable model"
const sorted = allModels.sort((a, b) => b.tier - a.tier);
const secondBest = sorted[1];

// "Cheapest model that supports adaptive thinking"
const cheapest = allModels
  .filter(m => m.capabilities.has(ModelCapability.AdaptiveThinking))
  .sort((a, b) => a.tier - b.tier)[0];

3. Enumerated capabilities (const enum for exhaustive matching)

Boolean feature flags (supportsEffort?: boolean) prevent exhaustive pattern matching — the compiler can't verify you handled all cases. Enumerated values enable satisfies Record<ModelCapability, ...> for compile-time completeness:

/** Every capability a model may or may not support. */
export enum ModelCapability {
  EffortLevels = 'effort_levels',
  AdaptiveThinking = 'adaptive_thinking',
  FastMode = 'fast_mode',
  AutoMode = 'auto_mode',
  ExtendedContext = 'extended_context',
  ToolUse = 'tool_use',
  // ... as new capabilities are added, consuming code with
  // `satisfies Record<ModelCapability, ...>` gets a compile error
}

export interface ModelDescriptor {
  /** The alias string accepted by query() and createSession() */
  readonly id: string;
  /** Human-readable name */
  readonly displayName: string;
  /** Numeric tier for ordering (higher = more capable/expensive) */
  readonly tier: ModelTier;
  /** Set of capabilities — use .has() for queries, iterate for exhaustive checks */
  readonly capabilities: ReadonlySet<ModelCapability>;
  /** Available effort levels, if EffortLevels capability is present */
  readonly effortLevels?: readonly EffortLevel[];
}

4. Registry with query helpers

export const ModelRegistry = {
  all(): readonly ModelDescriptor[];
  byId(id: string): ModelDescriptor | undefined;
  byTier(tier: ModelTier): readonly ModelDescriptor[];
  withCapability(cap: ModelCapability): readonly ModelDescriptor[];
  /** Models sorted by tier descending (most capable first). */
  ranked(): readonly ModelDescriptor[];
} as const;

Why enumerated capabilities matter for exhaustive matching

With boolean flags, adding a new capability is invisible to consumers:

// BAD: boolean flags — new capability added silently, no compile error
if (model.supportsEffort) { ... }
if (model.supportsFastMode) { ... }
// supportsNewThing added in SDK v3 — consumer code compiles fine but ignores it

With enumerated values and satisfies, the compiler forces consumers to handle every capability:

// GOOD: exhaustive mapping — adding a new ModelCapability breaks compilation
const CAPABILITY_LABELS = {
  [ModelCapability.EffortLevels]: 'Effort',
  [ModelCapability.AdaptiveThinking]: 'Thinking',
  [ModelCapability.FastMode]: 'Fast',
  [ModelCapability.AutoMode]: 'Auto',
} satisfies Record<ModelCapability, string>;
// ^ If SDK adds ModelCapability.NewThing, this fails to compile

Current workaround

We define our own constants and accept the maintenance burden:

// model-tiers.ts — our hand-rolled version of what the SDK should provide
export const MODEL_OPUS = 'opus' as const;
export const MODEL_SONNET = 'sonnet' as const;
export const MODEL_HAIKU = 'haiku' as const;
export type ModelAlias = typeof MODEL_OPUS | typeof MODEL_SONNET | typeof MODEL_HAIKU;

export const AGENT_MODELS = {
  coordinator: MODEL_OPUS,
  prover: MODEL_OPUS,
  // ...
} as const satisfies Record<string, ModelAlias>;

This works but is fragile — if the SDK adds a new model family or changes aliases, we find out at runtime.

What exists today

The SDK already has ModelInfo with value, displayName, supportsEffort, supportedEffortLevels, supportsAdaptiveThinking, supportsFastMode, supportsAutoMode. And supportedModels(): Promise<ModelInfo[]> exists on the streaming-input interface. The pieces are there — they just need to be:

  1. Exported as constants (not just runtime-queryable)
  2. Typed with enums instead of optional booleans
  3. Given a numeric tier for natural ordering

Environment

  • Claude Code: 2.1.97
  • Agent SDK: @anthropic-ai/claude-agent-sdk (latest)
  • Use case: Multi-agent PR autopilot with role-based model selection

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions