Skip to content

Model Picker — PHP source

Filter every major model by context window, price, modality, and tier — sort by cost, context, or tokens-per-dollar to find the right model for the task.

This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
/**
 * Model Picker — filter + rank the AI model catalog.
 *
 * Language: PHP (8.1+, standard library only — mbstring for lowercase/substr
 *           search, which is effectively universal in modern PHP)
 * Source:   CosmoDev polyglot showcase port of the Model Picker tool, ported
 *           from src/lib/modelPicker.ts (the canonical TypeScript
 *           implementation).
 * Live:     https://dev.cosmolabs.org/tools/model-picker
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Design goals:
 *   - Pure + deterministic; never throws.
 *   - Functionally equivalent to the TS reference: same inputs -> same outputs.
 *   - Self-contained: stdlib only (no Composer packages).
 *
 * Port notes: the TS lib imports `listModels` from src/lib/ai/models.ts and
 * lets `models` default to the bundled pricing snapshot
 * (src/data/ai-models.json). A dependency-free port cannot load that file, so
 * the filter step is inlined here and `models` is an explicit parameter (PHP
 * permits a required parameter after an optional one; callers always pass it).
 * `usort` is stable since PHP 8.0, matching Array.prototype.sort's stability
 * guarantee in the TS original — rows comparing equal keep their input order.
 *
 * Every sort places null/missing values last and breaks ties on id ascending,
 * so output order is fully deterministic for a given model list.
 */

declare(strict_types=1);

/**
 * One catalog entry. Property names mirror the TS AiModel interface's JSON
 * field names; PHP nullable types play the role of TS `number|string|null`.
 */
final class AiModel
{
    public function __construct(
        public readonly string $id,
        public readonly string $name,
        public readonly string $vendor,
        public readonly string $family,
        public readonly string $tier,
        public readonly int $contextWindow,
        public readonly int $maxOutput,
        public readonly array $modalities,
        // USD per 1M tokens; null for open/unpriced models.
        public readonly ?float $inputPerM = null,
        public readonly ?float $outputPerM = null,
        public readonly ?float $cacheReadPerM = null,
        public readonly ?float $cacheWritePerM = null,
        public readonly ?string $knowledgeCutoff = null,
        // ISO date string; null when unknown.
        public readonly ?string $released = null,
        public readonly bool $openWeights = false,
        public readonly bool $reasoning = false,
        public readonly bool $toolCall = false,
    ) {
    }
}

/**
 * Mirrors the TS ModelFilter interface: every field is optional, and null
 * means "do not apply this constraint" (TS undefined).
 */
final class ModelFilter
{
    public function __construct(
        public readonly ?string $vendor = null,
        public readonly ?string $tier = null,
        // Minimum usable context window in tokens.
        public readonly ?int $minContext = null,
        // Maximum input price (USD per 1M tokens); null-priced models are skipped.
        public readonly ?float $maxInputPerM = null,
        // Required input modality, e.g. 'image'.
        public readonly ?string $modality = null,
        // Case-insensitive substring match on id, name, and vendor.
        public readonly ?string $search = null,
    ) {
    }
}

/** A curated entry point: a filter + the sort that makes that filter useful. */
final class Preset
{
    public function __construct(
        public readonly ModelFilter $filter,
        public readonly string $sort,
    ) {
    }
}

/**
 * The curated presets, mirroring the TS PRESETS record. A function (not a
 * const) because PHP constants cannot hold objects.
 */
function presets(): array
{
    return [
        'long-context' => new Preset(new ModelFilter(minContext: 500_000), 'context'),
        'cheap-bulk' => new Preset(new ModelFilter(maxInputPerM: 1.0), 'price'),
        'flagship' => new Preset(new ModelFilter(tier: 'flagship'), 'tokensPerDollar'),
    ];
}

/** id ascending — the shared stable tie-break for every sort. */
function byId(AiModel $a, AiModel $b): int
{
    return $a->id <=> $b->id;
}

function cmpPrice(AiModel $a, AiModel $b): int
{
    if ($a->inputPerM === null && $b->inputPerM === null) {
        return byId($a, $b);
    }
    if ($a->inputPerM === null) {
        return 1;
    }
    if ($b->inputPerM === null) {
        return -1;
    }
    return ($a->inputPerM <=> $b->inputPerM) ?: byId($a, $b);
}

function cmpContext(AiModel $a, AiModel $b): int
{
    return ($b->contextWindow <=> $a->contextWindow) ?: byId($a, $b);
}

function cmpTokensPerDollar(AiModel $a, AiModel $b): int
{
    $ta = $a->outputPerM === null ? null : 1_000_000 / $a->outputPerM;
    $tb = $b->outputPerM === null ? null : 1_000_000 / $b->outputPerM;
    if ($ta === null && $tb === null) {
        return byId($a, $b);
    }
    if ($ta === null) {
        return 1;
    }
    if ($tb === null) {
        return -1;
    }
    return ($tb <=> $ta) ?: byId($a, $b);
}

function cmpReleased(AiModel $a, AiModel $b): int
{
    if ($a->released === null && $b->released === null) {
        return byId($a, $b);
    }
    if ($a->released === null) {
        return 1;
    }
    if ($b->released === null) {
        return -1;
    }
    return ($b->released <=> $a->released) ?: byId($a, $b);
}

/** Sort comparator per key: value order first, null last, id asc on ties. */
function comparators(): array
{
    return [
        'price' => cmpPrice(...),
        'context' => cmpContext(...),
        'tokensPerDollar' => cmpTokensPerDollar(...),
        'released' => cmpReleased(...),
    ];
}

/**
 * Filter the model list. Inline port of listModels() from src/lib/ai/models.ts:
 * null prices never satisfy maxInputPerM, and search lowercases both sides
 * before the substring check.
 */
function listModels(ModelFilter $filter, array $models): array
{
    $q = $filter->search === null ? null : mb_strtolower($filter->search);
    return array_values(array_filter($models, function (AiModel $m) use ($filter, $q): bool {
        if ($filter->vendor !== null && $m->vendor !== $filter->vendor) {
            return false;
        }
        if ($filter->tier !== null && $m->tier !== $filter->tier) {
            return false;
        }
        if ($filter->minContext !== null && $m->contextWindow < $filter->minContext) {
            return false;
        }
        if (
            $filter->maxInputPerM !== null &&
            ($m->inputPerM === null || $m->inputPerM > $filter->maxInputPerM)
        ) {
            return false;
        }
        if ($filter->modality !== null && !in_array($filter->modality, $m->modalities, true)) {
            return false;
        }
        if ($q !== null && !mb_contains(mb_strtolower("{$m->id} {$m->name} {$m->vendor}"), $q)) {
            return false;
        }
        return true;
    }));
}

/** Case-insensitive substring check mirroring PHP 8+ str_contains(). */
function mb_contains(string $haystack, string $needle): bool
{
    return $needle === '' || mb_stripos($haystack, $needle) !== false;
}

/** Filter the model list, then sort it. See comparators() for the exact order.
 *  `$models` defaults to an empty list (the TS original defaults it to the
 *  bundled pricing snapshot, which a dependency-free port cannot load). */
function pickModels(ModelFilter $filter, string $sort = 'price', array $models = []): array
{
    $list = listModels($filter, $models);
    usort($list, comparators()[$sort]);
    return $list;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →