resolveModelMaxOutputTokens function

int? resolveModelMaxOutputTokens(
  1. String modelId, {
  2. required String api,
})

The ceiling-table step of max-output-token resolution for modelId: per-model config override (caller-side) > this table > provider default (caller-side). Null is the table MISS — non-claude ids (glm, kimi, ...) ride the caller's provider default unchanged, and so does any claude id whose api is outside the table's family (anthropicMessagesApi, issue #302: Claude-specific ceilings never leak onto other provider families).

Ported from kimi-code (issue #273). Resolution over the normalized id (lowercased, _-):

  • no claude substring → null;
  • family (opus|sonnet|haiku) + version, from two regexes: (a) version AFTER the family: (?:^|-)(opus|sonnet|haiku)(?:-?(\d{1,2})(?:[.-](\d{1,2}))?(?=[-.]|$))?claude-opus-4-5-20251101 → (4,5), claude-opus-4.5 → (4,5), claude-opus-4 → (4,0); the \d{1,2} + [-.]|$ lookahead rejects date suffixes (claude-3-opus-20240229 never parses 20240229); (b) no version from (a): one BEFORE the family, matched over the text preceding it with (\d{1,2})(?:[.-](\d{1,2}))?(?=-|$)claude-3-opus → (3,0), claude-3-5-sonnet → (3,5);
  • the largest catalogued (major,minor) ≤ the parsed version wins (an exact hit is that maximum, so one scan serves both: opus-4.9 rides 4.8's 128000, sonnet-4.7 falls to 4.6's); nothing catalogued ≤ it, an unparseable version, or no known family → _unknownClaudeOutputCeiling (AC2: totally unknown Claude → conservative fallback).

Implementation

int? resolveModelMaxOutputTokens(String modelId, {required String api}) {
  if (api != anthropicMessagesApi) return null;
  final id = modelId.toLowerCase().replaceAll('_', '-');
  if (!id.contains('claude')) return null;

  final match = RegExp(
    r'(?:^|-)(opus|sonnet|haiku)(?:-?(\d{1,2})(?:[.-](\d{1,2}))?(?=[-.]|$))?',
  ).firstMatch(id);
  if (match == null) return _unknownClaudeOutputCeiling;
  final table = _modelOutputCeilings[match.group(1)!]!;

  int major;
  int minor;
  final after = match.group(2);
  if (after != null) {
    major = int.parse(after);
    minor = match.group(3) == null ? 0 : int.parse(match.group(3)!);
  } else {
    final before = RegExp(
      r'(\d{1,2})(?:[.-](\d{1,2}))?(?=-|$)',
    ).firstMatch(id.substring(0, match.start));
    if (before == null) return _unknownClaudeOutputCeiling;
    major = int.parse(before.group(1)!);
    minor = before.group(2) == null ? 0 : int.parse(before.group(2)!);
  }

  // Largest catalogued version ≤ the parsed one; monotonic
  // `major*100+minor` keys keep the comparison integer-simple.
  final given = major * 100 + minor;
  int? bestCeiling;
  var bestVersion = -1;
  for (final entry in table.entries) {
    final dot = entry.key.indexOf('.');
    final version =
        int.parse(entry.key.substring(0, dot)) * 100 +
        int.parse(entry.key.substring(dot + 1));
    if (version > given) continue;
    if (version > bestVersion) {
      bestVersion = version;
      bestCeiling = entry.value;
    }
  }
  return bestCeiling ?? _unknownClaudeOutputCeiling;
}