|
| 1 | +#!/usr/bin/env php |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Fetches the OpenRouter model catalog and generates Models.php |
| 6 | + * |
| 7 | + * Usage: php scripts/sync-openrouter-models.php [output-path] |
| 8 | + * |
| 9 | + * Only models from curated providers get named constants. |
| 10 | + * The full catalog is available via the MODELS array. |
| 11 | + */ |
| 12 | +$endpoint = getenv('OPENROUTER_MODELS_ENDPOINT') ?: 'https://openrouter.ai/api/v1/models'; |
| 13 | +$defaultOutput = __DIR__.'/../src/Agents/Adapters/OpenRouter/Models.php'; |
| 14 | +$outputPath = $argv[1] ?? $defaultOutput; |
| 15 | + |
| 16 | +// Providers whose models get named class constants |
| 17 | +$curatedProviders = [ |
| 18 | + 'anthropic', |
| 19 | + 'openai', |
| 20 | + 'google', |
| 21 | + 'meta-llama', |
| 22 | + 'deepseek', |
| 23 | + 'mistralai', |
| 24 | + 'x-ai', |
| 25 | +]; |
| 26 | + |
| 27 | +// Skip model IDs matching these patterns (old/niche variants) |
| 28 | +$skipPatterns = [ |
| 29 | + '/:extended$/', // extended-context variants |
| 30 | + '/:free$/', // free-tier duplicates |
| 31 | + '/:beta$/', // beta tags |
| 32 | + '/-\d{4}-\d{2}-\d{2}/', // date-pinned snapshots (e.g. gpt-4o-2024-08-06) |
| 33 | + '/-\d{4}$/', // short date pins (e.g. gpt-4-0314) |
| 34 | + '/-\d{4}-preview/', // date preview variants (e.g. gpt-4-1106-preview) |
| 35 | + '/gpt-3\.5/', // legacy GPT-3.5 models |
| 36 | + '/gpt-4-turbo/', // legacy GPT-4 turbo |
| 37 | + '/-preview$/', // generic preview suffixes |
| 38 | +]; |
| 39 | + |
| 40 | +$headers = ['Accept: application/json']; |
| 41 | + |
| 42 | +$apiKey = getenv('OPENROUTER_API_KEY') ?: getenv('LLM_KEY_OPENROUTER'); |
| 43 | +if ($apiKey) { |
| 44 | + $headers[] = "Authorization: Bearer {$apiKey}"; |
| 45 | +} |
| 46 | + |
| 47 | +$context = stream_context_create([ |
| 48 | + 'http' => [ |
| 49 | + 'header' => implode("\r\n", $headers), |
| 50 | + 'timeout' => 30, |
| 51 | + ], |
| 52 | +]); |
| 53 | + |
| 54 | +$response = file_get_contents($endpoint, false, $context); |
| 55 | +if ($response === false) { |
| 56 | + fwrite(STDERR, "Failed to fetch OpenRouter models from {$endpoint}\n"); |
| 57 | + exit(1); |
| 58 | +} |
| 59 | + |
| 60 | +$payload = json_decode($response, true); |
| 61 | +if (! is_array($payload) || ! isset($payload['data']) || ! is_array($payload['data'])) { |
| 62 | + fwrite(STDERR, "OpenRouter models response did not include a data array\n"); |
| 63 | + exit(1); |
| 64 | +} |
| 65 | + |
| 66 | +$models = array_filter($payload['data'], fn ($m) => is_array($m) && isset($m['id']) && is_string($m['id']) && $m['id'] !== ''); |
| 67 | +$models = array_values($models); |
| 68 | +usort($models, fn ($a, $b) => strcmp($a['id'], $b['id'])); |
| 69 | + |
| 70 | +if (count($models) === 0) { |
| 71 | + fwrite(STDERR, "OpenRouter models response was empty\n"); |
| 72 | + exit(1); |
| 73 | +} |
| 74 | + |
| 75 | +$modelIds = array_map(fn ($m) => $m['id'], $models); |
| 76 | + |
| 77 | +/** |
| 78 | + * Convert a model ID to a PHP constant name (MODEL_PROVIDER_NAME). |
| 79 | + */ |
| 80 | +function toConstantName(string $id): string |
| 81 | +{ |
| 82 | + $name = strtoupper($id); |
| 83 | + $name = preg_replace('/[^A-Z0-9]+/', '_', $name); |
| 84 | + $name = preg_replace('/_+/', '_', $name); |
| 85 | + $name = trim($name, '_'); |
| 86 | + |
| 87 | + if ($name === '') { |
| 88 | + return 'MODEL_UNKNOWN'; |
| 89 | + } |
| 90 | + |
| 91 | + return "MODEL_{$name}"; |
| 92 | +} |
| 93 | + |
| 94 | +// Build curated constants (named) and the full ID list |
| 95 | +$curatedConstants = []; // name => id |
| 96 | +$usedNames = []; |
| 97 | + |
| 98 | +foreach ($modelIds as $id) { |
| 99 | + $provider = explode('/', $id, 2)[0]; |
| 100 | + |
| 101 | + if (! in_array($provider, $curatedProviders, true)) { |
| 102 | + continue; |
| 103 | + } |
| 104 | + |
| 105 | + // Skip date-pinned snapshots, free/beta/extended variants |
| 106 | + $dominated = false; |
| 107 | + foreach ($skipPatterns as $pattern) { |
| 108 | + if (preg_match($pattern, $id)) { |
| 109 | + $dominated = true; |
| 110 | + break; |
| 111 | + } |
| 112 | + } |
| 113 | + if ($dominated) { |
| 114 | + continue; |
| 115 | + } |
| 116 | + |
| 117 | + $name = toConstantName($id); |
| 118 | + |
| 119 | + if (isset($usedNames[$name])) { |
| 120 | + $name .= '_'.strtoupper(substr(sha1($id), 0, 8)); |
| 121 | + } |
| 122 | + |
| 123 | + $usedNames[$name] = true; |
| 124 | + $curatedConstants[$name] = $id; |
| 125 | +} |
| 126 | + |
| 127 | +// Generate PHP |
| 128 | +$now = gmdate('Y-m-d\TH:i:s\Z'); |
| 129 | +$totalCount = count($modelIds); |
| 130 | +$curatedCount = count($curatedConstants); |
| 131 | + |
| 132 | +$lines = []; |
| 133 | +$lines[] = '<?php'; |
| 134 | +$lines[] = ''; |
| 135 | +$lines[] = 'namespace Utopia\Agents\Adapters\OpenRouter;'; |
| 136 | +$lines[] = ''; |
| 137 | +$lines[] = '/**'; |
| 138 | +$lines[] = ' * Generated by scripts/sync-openrouter-models.php — do not edit by hand.'; |
| 139 | +$lines[] = " * Source: {$endpoint}"; |
| 140 | +$lines[] = " * Synced at: {$now}"; |
| 141 | +$lines[] = " * Named constants: {$curatedCount} (curated providers)"; |
| 142 | +$lines[] = " * Total models: {$totalCount}"; |
| 143 | +$lines[] = ' */'; |
| 144 | +$lines[] = 'final class Models'; |
| 145 | +$lines[] = '{'; |
| 146 | + |
| 147 | +// Named constants grouped by provider |
| 148 | +$currentProvider = ''; |
| 149 | +foreach ($curatedConstants as $name => $id) { |
| 150 | + $provider = explode('/', $id, 2)[0]; |
| 151 | + if ($provider !== $currentProvider) { |
| 152 | + if ($currentProvider !== '') { |
| 153 | + $lines[] = ''; |
| 154 | + } |
| 155 | + $lines[] = " // {$provider}"; |
| 156 | + $currentProvider = $provider; |
| 157 | + } |
| 158 | + $safeId = str_replace("'", "\\'", $id); |
| 159 | + $lines[] = " public const {$name} = '{$safeId}';"; |
| 160 | +} |
| 161 | + |
| 162 | +$lines[] = ''; |
| 163 | +// No DEFAULT_MODEL — the default is set in OpenRouter::__construct() |
| 164 | + |
| 165 | +// Full MODELS array as plain strings |
| 166 | +$lines[] = ''; |
| 167 | +$lines[] = ' /**'; |
| 168 | +$lines[] = ' * Full model catalog. Use model IDs directly or via named constants above.'; |
| 169 | +$lines[] = ' *'; |
| 170 | +$lines[] = ' * @var list<string>'; |
| 171 | +$lines[] = ' */'; |
| 172 | +$lines[] = ' public const MODELS = ['; |
| 173 | +foreach ($modelIds as $id) { |
| 174 | + $safeId = str_replace("'", "\\'", $id); |
| 175 | + $lines[] = " '{$safeId}',"; |
| 176 | +} |
| 177 | +$lines[] = ' ];'; |
| 178 | +$lines[] = '}'; |
| 179 | +$lines[] = ''; |
| 180 | + |
| 181 | +$dir = dirname($outputPath); |
| 182 | +if (! is_dir($dir)) { |
| 183 | + mkdir($dir, 0755, true); |
| 184 | +} |
| 185 | + |
| 186 | +file_put_contents($outputPath, implode("\n", $lines)); |
| 187 | + |
| 188 | +echo "Wrote {$curatedCount} named constants + {$totalCount} model IDs to {$outputPath}\n"; |
0 commit comments