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 →