reason code. Capabilities only. */ private const WORDS = [ 'tts' => 'speech', 'speech' => 'speech', 'voice' => 'speech', 'audio' => 'audio', 'lyria' => 'audio', 'music' => 'audio', 'image' => 'image', 'imagen' => 'image', 'banana' => 'image', 'dall' => 'image', 'veo' => 'video', 'video' => 'video', 'sora' => 'video', 'transcribe' => 'transcription', 'transcription' => 'transcription', 'whisper' => 'transcription', 'embedding' => 'embedding', 'embeddings' => 'embedding', 'embed' => 'embedding', 'moderation' => 'moderation', 'live' => 'realtime', 'realtime' => 'realtime', 'robotics' => 'robotics', 'research' => 'agent', 'agent' => 'agent', 'antigravity' => 'agent', 'computer' => 'agent', 'customtools' => 'agent', ]; /** Below this reported input limit a model cannot reliably hold instructions plus knowledge excerpts. */ private const MIN_CONTEXT = 16384; /** * @param array $model normalised catalog entry (id, name, chat, input_limit, ...) * @return array{level:string, reasons:list} */ public static function classify(array $model, ?string $testedModel = null): array { $id = (string) ($model['id'] ?? ''); if ($testedModel !== null && $testedModel === $id) { return ['level' => self::TESTED, 'reasons' => []]; } $reasons = []; if (($model['chat'] ?? null) === false) { $reasons[] = 'not_chat'; } $words = preg_split('/[^a-z0-9]+/', strtolower($id . ' ' . (string) ($model['name'] ?? ''))) ?: []; foreach ($words as $w) { if (isset(self::WORDS[$w])) { $reasons[] = self::WORDS[$w]; } } $reasons = array_values(array_unique($reasons)); if ($reasons !== []) { return ['level' => self::SPECIALIZED, 'reasons' => $reasons]; } if (($model['chat'] ?? null) === null) { $reasons[] = 'not_reported'; } if (in_array('preview', $words, true) || in_array('experimental', $words, true) || in_array('exp', $words, true)) { $reasons[] = 'preview'; } $input = $model['input_limit'] ?? null; if (is_int($input) && $input < self::MIN_CONTEXT) { $reasons[] = 'small_context'; } return $reasons === [] ? ['level' => self::REPORTED, 'reasons' => []] : ['level' => self::UNCONFIRMED, 'reasons' => $reasons]; } /** * Models in display order, grouped by level (tested, reported, unconfirmed, specialized). Order inside a group is kept as given. Nothing is dropped. * * @param list> $models * @return array>> level => models, each with a 'suitability' entry; empty groups omitted */ public static function group(array $models, ?string $testedModel = null): array { $groups = [self::TESTED => [], self::REPORTED => [], self::UNCONFIRMED => [], self::SPECIALIZED => []]; foreach ($models as $m) { $s = self::classify($m, $testedModel); $m['suitability'] = $s; $groups[$s['level']][] = $m; } return array_filter($groups, static fn(array $g): bool => $g !== []); } }