Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ LLM_KEY_OPENAI=sk-proj-1234567890
LLM_KEY_DEEPSEEK=sk-1234567890
LLM_KEY_XAI=xai-1234567890
LLM_KEY_PERPLEXITY=pplx-1234567890
LLM_KEY_GEMINI=AI1234567890
LLM_KEY_GEMINI=AI1234567890
LLM_KEY_OPENROUTER=sk-or-v1-1234567890
96 changes: 86 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
name: "Tests"

on: [ pull_request ]
on: [pull_request]
jobs:
lint:
name: Tests
unit:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 2

- run: git checkout HEAD^2

- name: Build and start services
run: |
docker compose up -d
sleep 10

- name: Run Unit Tests
run: |
docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml \
tests/Agents/AgentTest.php \
tests/Agents/SchemaTest.php \
tests/Agents/Messages \
tests/Agents/Roles \
tests/Agents/Schema

conversation:
name: "${{ matrix.provider }} Tests"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- provider: OpenAI
test: tests/Agents/Conversation/ConversationOpenAITest.php
env_key: LLM_KEY_OPENAI
- provider: Anthropic
test: tests/Agents/Conversation/ConversationAnthropicTest.php
env_key: LLM_KEY_ANTHROPIC
- provider: Deepseek
test: tests/Agents/Conversation/ConversationDeepseekTest.php
env_key: LLM_KEY_DEEPSEEK
- provider: XAI
test: tests/Agents/Conversation/ConversationXAITest.php
env_key: LLM_KEY_XAI
- provider: Perplexity
test: tests/Agents/Conversation/ConversationPerplexityTest.php
env_key: LLM_KEY_PERPLEXITY
- provider: Gemini
test: tests/Agents/Conversation/ConversationGeminiTest.php
env_key: LLM_KEY_GEMINI
- provider: OpenRouter
test: tests/Agents/Conversation/ConversationOpenRouterTest.php
env_key: LLM_KEY_OPENROUTER
steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand All @@ -14,18 +63,45 @@ jobs:

- run: git checkout HEAD^2

- name: Build
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"

- name: Install dependencies
run: composer install --no-interaction --prefer-dist

- name: Run ${{ matrix.provider }} Tests
env:
LLM_KEY_ANTHROPIC: ${{ secrets.LLM_KEY_ANTHROPIC }}
LLM_KEY_OPENAI: ${{ secrets.LLM_KEY_OPENAI }}
LLM_KEY_DEEPSEEK: ${{ secrets.LLM_KEY_DEEPSEEK }}
LLM_KEY_XAI: ${{ secrets.LLM_KEY_XAI }}
LLM_KEY_PERPLEXITY: ${{ secrets.LLM_KEY_PERPLEXITY }}
LLM_KEY_GEMINI: ${{ secrets.LLM_KEY_GEMINI }}
run: |
docker compose build
docker compose up -d
sleep 10
LLM_KEY_OPENROUTER: ${{ secrets.LLM_KEY_OPENROUTER }}
run: vendor/bin/phpunit --configuration phpunit.xml ${{ matrix.test }}

- name: Run Tests
run: docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml tests
diffcheck:
name: DiffCheck Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 2

- run: git checkout HEAD^2

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"

- name: Install dependencies
run: composer install --no-interaction --prefer-dist

- name: Run DiffCheck Tests
env:
LLM_KEY_OPENAI: ${{ secrets.LLM_KEY_OPENAI }}
run: vendor/bin/phpunit --configuration phpunit.xml tests/Agents/DiffCheck
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP ve

## Features

- **Multiple AI Providers** - Support for OpenAI, Anthropic, Deepseek, Perplexity, and XAI APIs
- **Multiple AI Providers** - Support for OpenAI, Anthropic, Deepseek, Perplexity, XAI, Gemini, and OpenRouter APIs
- **Flexible Message Types** - Support for text and structured content in messages
- **Conversation Management** - Easy-to-use conversation handling between agents and users
- **Model Selection** - Choose from various AI models (GPT-4, Claude 3, Deepseek Chat, Sonar, Grok, etc.)
Expand Down Expand Up @@ -155,6 +155,28 @@ Available XAI Models:
- `MODEL_GROK_3_MINI`: Mini version of Grok model
- `MODEL_GROK_2_IMAGE`: Latest Grok model with image support

#### OpenRouter

```php
use Utopia\Agents\Adapters\OpenRouter;
use Utopia\Agents\Adapters\OpenRouter\Models as OpenRouterModels;

$openrouter = new OpenRouter(
apiKey: 'your-api-key',
model: OpenRouterModels::MODEL_OPENAI_GPT_4O,
maxTokens: 2048,
temperature: 0.7,
httpReferer: 'https://your-app.example',
xTitle: 'Your App Name'
);
```

- Named constants are provided for popular models from major providers (OpenAI, Anthropic, Google, Meta, DeepSeek, Mistral, xAI)
- `Models::MODELS` contains the full model catalog; the adapter defaults to `openai/gpt-4o`
- Arbitrary model IDs like `'openai/gpt-5-nano'` or `'anthropic/claude-sonnet-4'` are also accepted directly
- `httpReferer` and `xTitle` are optional and enable OpenRouter app attribution headers
- To re-sync constants from the live OpenRouter API, run `php scripts/sync-openrouter-models.php`

### Managing Conversations

```php
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
- LLM_KEY_XAI=${LLM_KEY_XAI:-}
- LLM_KEY_PERPLEXITY=${LLM_KEY_PERPLEXITY:-}
- LLM_KEY_GEMINI=${LLM_KEY_GEMINI:-}
- LLM_KEY_OPENROUTER=${LLM_KEY_OPENROUTER:-}
depends_on:
- ollama
networks:
Expand Down
5 changes: 4 additions & 1 deletion pint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"rules": {
"php_unit_method_casing": false,
"new_with_parentheses": false
}
},
"notPath": [
"src/Agents/Adapters/OpenRouter/Models.php"
]
}
188 changes: 188 additions & 0 deletions scripts/sync-openrouter-models.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env php
<?php

/**
* Fetches the OpenRouter model catalog and generates Models.php
*
* Usage: php scripts/sync-openrouter-models.php [output-path]
*
* Only models from curated providers get named constants.
* The full catalog is available via the MODELS array.
*/
$endpoint = getenv('OPENROUTER_MODELS_ENDPOINT') ?: 'https://openrouter.ai/api/v1/models';
$defaultOutput = __DIR__.'/../src/Agents/Adapters/OpenRouter/Models.php';
$outputPath = $argv[1] ?? $defaultOutput;

// Providers whose models get named class constants
$curatedProviders = [
'anthropic',
'openai',
'google',
'meta-llama',
'deepseek',
'mistralai',
'x-ai',
];

// Skip model IDs matching these patterns (old/niche variants)
$skipPatterns = [
'/:extended$/', // extended-context variants
'/:free$/', // free-tier duplicates
'/:beta$/', // beta tags
'/-\d{4}-\d{2}-\d{2}/', // date-pinned snapshots (e.g. gpt-4o-2024-08-06)
'/-\d{4}$/', // short date pins (e.g. gpt-4-0314)
'/-\d{4}-preview/', // date preview variants (e.g. gpt-4-1106-preview)
'/gpt-3\.5/', // legacy GPT-3.5 models
'/gpt-4-turbo/', // legacy GPT-4 turbo
'/-preview$/', // generic preview suffixes
];

$headers = ['Accept: application/json'];

$apiKey = getenv('OPENROUTER_API_KEY') ?: getenv('LLM_KEY_OPENROUTER');
if ($apiKey) {
$headers[] = "Authorization: Bearer {$apiKey}";
}

$context = stream_context_create([
'http' => [
'header' => implode("\r\n", $headers),
'timeout' => 30,
],
]);

$response = file_get_contents($endpoint, false, $context);
if ($response === false) {
fwrite(STDERR, "Failed to fetch OpenRouter models from {$endpoint}\n");
exit(1);
}

$payload = json_decode($response, true);
if (! is_array($payload) || ! isset($payload['data']) || ! is_array($payload['data'])) {
fwrite(STDERR, "OpenRouter models response did not include a data array\n");
exit(1);
}

$models = array_filter($payload['data'], fn ($m) => is_array($m) && isset($m['id']) && is_string($m['id']) && $m['id'] !== '');
$models = array_values($models);
usort($models, fn ($a, $b) => strcmp($a['id'], $b['id']));

if (count($models) === 0) {
fwrite(STDERR, "OpenRouter models response was empty\n");
exit(1);
}

$modelIds = array_map(fn ($m) => $m['id'], $models);

/**
* Convert a model ID to a PHP constant name (MODEL_PROVIDER_NAME).
*/
function toConstantName(string $id): string
{
$name = strtoupper($id);
$name = preg_replace('/[^A-Z0-9]+/', '_', $name);
$name = preg_replace('/_+/', '_', $name);
$name = trim($name, '_');

if ($name === '') {
return 'MODEL_UNKNOWN';
}

return "MODEL_{$name}";
}

// Build curated constants (named) and the full ID list
$curatedConstants = []; // name => id
$usedNames = [];

foreach ($modelIds as $id) {
$provider = explode('/', $id, 2)[0];

if (! in_array($provider, $curatedProviders, true)) {
continue;
}

// Skip date-pinned snapshots, free/beta/extended variants
$dominated = false;
foreach ($skipPatterns as $pattern) {
if (preg_match($pattern, $id)) {
$dominated = true;
break;
}
}
if ($dominated) {
continue;
}

$name = toConstantName($id);

if (isset($usedNames[$name])) {
$name .= '_'.strtoupper(substr(sha1($id), 0, 8));
}

$usedNames[$name] = true;
$curatedConstants[$name] = $id;
}

// Generate PHP
$now = gmdate('Y-m-d\TH:i:s\Z');
$totalCount = count($modelIds);
$curatedCount = count($curatedConstants);

$lines = [];
$lines[] = '<?php';
$lines[] = '';
$lines[] = 'namespace Utopia\Agents\Adapters\OpenRouter;';
$lines[] = '';
$lines[] = '/**';
$lines[] = ' * Generated by scripts/sync-openrouter-models.php — do not edit by hand.';
$lines[] = " * Source: {$endpoint}";
$lines[] = " * Synced at: {$now}";
$lines[] = " * Named constants: {$curatedCount} (curated providers)";
$lines[] = " * Total models: {$totalCount}";
$lines[] = ' */';
$lines[] = 'final class Models';
$lines[] = '{';

// Named constants grouped by provider
$currentProvider = '';
foreach ($curatedConstants as $name => $id) {
$provider = explode('/', $id, 2)[0];
if ($provider !== $currentProvider) {
if ($currentProvider !== '') {
$lines[] = '';
}
$lines[] = " // {$provider}";
$currentProvider = $provider;
}
$safeId = str_replace("'", "\\'", $id);
$lines[] = " public const {$name} = '{$safeId}';";
}

$lines[] = '';
// No DEFAULT_MODEL — the default is set in OpenRouter::__construct()

// Full MODELS array as plain strings
$lines[] = '';
$lines[] = ' /**';
$lines[] = ' * Full model catalog. Use model IDs directly or via named constants above.';
$lines[] = ' *';
$lines[] = ' * @var list<string>';
$lines[] = ' */';
$lines[] = ' public const MODELS = [';
foreach ($modelIds as $id) {
$safeId = str_replace("'", "\\'", $id);
$lines[] = " '{$safeId}',";
}
$lines[] = ' ];';
$lines[] = '}';
$lines[] = '';

$dir = dirname($outputPath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}

file_put_contents($outputPath, implode("\n", $lines));

echo "Wrote {$curatedCount} named constants + {$totalCount} model IDs to {$outputPath}\n";
Loading
Loading