mirror of
https://github.com/symfony/ai.git
synced 2026-03-23 23:42:18 +01:00
[Platform][amazee.ai] Add bridge
This commit is contained in:
committed by
Christopher Hertel
parent
e924ac199d
commit
514c354990
@@ -203,3 +203,7 @@ OPENSEARCH_ENDPOINT=http://127.0.0.1:9200
|
||||
|
||||
# For using OVH
|
||||
OVH_AI_SECRET_KEY=
|
||||
|
||||
# amazee.ai
|
||||
AMAZEEAI_LLM_KEY=
|
||||
AMAZEEAI_LLM_API_URL=
|
||||
|
||||
26
examples/amazeeai/chat.php
Normal file
26
examples/amazeeai/chat.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\PlatformFactory;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(env('AMAZEEAI_LLM_API_URL'), env('AMAZEEAI_LLM_KEY'), http_client());
|
||||
|
||||
$messages = new MessageBag(
|
||||
Message::forSystem('You are a pirate and you write funny.'),
|
||||
Message::ofUser('What is the Symfony framework?'),
|
||||
);
|
||||
$result = $platform->invoke('claude-3-5-sonnet', $messages);
|
||||
|
||||
echo $result->asText().\PHP_EOL;
|
||||
24
examples/amazeeai/embeddings.php
Normal file
24
examples/amazeeai/embeddings.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\PlatformFactory;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(env('AMAZEEAI_LLM_API_URL'), env('AMAZEEAI_LLM_KEY'), http_client());
|
||||
|
||||
$result = $platform->invoke('titan-embed-text-v2:0', <<<TEXT
|
||||
Once upon a time, there was a country called Japan. It was a beautiful country with a lot of mountains and rivers.
|
||||
The people of Japan were very kind and hardworking. They loved their country very much and took care of it. The
|
||||
country was very peaceful and prosperous. The people lived happily ever after.
|
||||
TEXT);
|
||||
|
||||
print_vectors($result);
|
||||
28
examples/amazeeai/stream.php
Normal file
28
examples/amazeeai/stream.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\PlatformFactory;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(env('AMAZEEAI_LLM_API_URL'), env('AMAZEEAI_LLM_KEY'), http_client());
|
||||
|
||||
$messages = new MessageBag(Message::ofUser('List the first 50 prime number?'));
|
||||
$result = $platform->invoke('claude-3-5-haiku', $messages, [
|
||||
'stream' => true,
|
||||
]);
|
||||
|
||||
foreach ($result->asStream() as $word) {
|
||||
echo $word;
|
||||
}
|
||||
echo \PHP_EOL;
|
||||
@@ -41,6 +41,7 @@
|
||||
},
|
||||
"ai-ai-ml-api-platform": "src/platform/src/Bridge/AiMlApi",
|
||||
"ai-albert-platform": "src/platform/src/Bridge/Albert",
|
||||
"ai-amazeeai-platform": "src/platform/src/Bridge/AmazeeAi",
|
||||
"ai-anthropic-platform": "src/platform/src/Bridge/Anthropic",
|
||||
"ai-azure-platform": "src/platform/src/Bridge/Azure",
|
||||
"ai-bedrock-platform": "src/platform/src/Bridge/Bedrock",
|
||||
|
||||
@@ -28,6 +28,7 @@ return static function (DefinitionConfigurator $configurator): void {
|
||||
->arrayNode('platform')
|
||||
->children()
|
||||
->append($import('platform/albert'))
|
||||
->append($import('platform/amazeeai'))
|
||||
->append($import('platform/anthropic'))
|
||||
->append($import('platform/azure'))
|
||||
->append($import('platform/bedrock'))
|
||||
|
||||
24
src/ai-bundle/config/platform/amazeeai.php
Normal file
24
src/ai-bundle/config/platform/amazeeai.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Config\Definition\Configurator;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
|
||||
return (new ArrayNodeDefinition('amazeeai'))
|
||||
->children()
|
||||
->stringNode('base_url')->isRequired()->end()
|
||||
->stringNode('api_key')->isRequired()->end()
|
||||
->stringNode('http_client')
|
||||
->defaultValue('http_client')
|
||||
->info('Service ID of the HTTP client to use')
|
||||
->end()
|
||||
->end();
|
||||
@@ -26,6 +26,7 @@ use Symfony\AI\Chat\Command\SetupStoreCommand as SetupMessageStoreCommand;
|
||||
use Symfony\AI\Chat\MessageNormalizer;
|
||||
use Symfony\AI\Platform\Bridge\AiMlApi\ModelCatalog as AiMlApiModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\Albert\ModelCatalog as AlbertModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\ModelApiCatalog as AmazeeAiModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
|
||||
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\Azure\OpenAi\ModelCatalog as AzureOpenAiModelCatalog;
|
||||
@@ -96,6 +97,7 @@ return static function (ContainerConfigurator $container): void {
|
||||
// model catalog
|
||||
->set('ai.platform.model_catalog.aimlapi', AiMlApiModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.albert', AlbertModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.amazeeai', AmazeeAiModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.anthropic', AnthropicModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.azure.openai', AzureOpenAiModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.bedrock', BedrockModelCatalog::class)
|
||||
|
||||
@@ -21,6 +21,7 @@ To use a specific AI platform, install the corresponding bridge package:
|
||||
|---------------------|-------------------------------------------|
|
||||
| AI.ML API | `symfony/ai-ai-ml-api-platform` |
|
||||
| Albert | `symfony/ai-albert-platform` |
|
||||
| amazee.ai | `symfony/ai-amazee-ai-platform` |
|
||||
| Anthropic | `symfony/ai-anthropic-platform` |
|
||||
| Azure OpenAI | `symfony/ai-azure-platform` |
|
||||
| AWS Bedrock | `symfony/ai-bedrock-platform` |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"ai",
|
||||
"aimlapi",
|
||||
"albert",
|
||||
"amazeeai",
|
||||
"anthropic",
|
||||
"azure",
|
||||
"bedrock",
|
||||
|
||||
3
src/platform/src/Bridge/AmazeeAi/.gitattributes
vendored
Normal file
3
src/platform/src/Bridge/AmazeeAi/.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.git* export-ignore
|
||||
8
src/platform/src/Bridge/AmazeeAi/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
8
src/platform/src/Bridge/AmazeeAi/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
Please do not submit any Pull Requests here. They will be closed.
|
||||
---
|
||||
|
||||
Please submit your PR here instead:
|
||||
https://github.com/symfony/ai
|
||||
|
||||
This repository is what we call a "subtree split": a read-only subset of that main repository.
|
||||
We're looking forward to your PR there!
|
||||
20
src/platform/src/Bridge/AmazeeAi/.github/workflows/close-pull-request.yml
vendored
Normal file
20
src/platform/src/Bridge/AmazeeAi/.github/workflows/close-pull-request.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Close Pull Request
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: superbrothers/close-pull-request@v3
|
||||
with:
|
||||
comment: |
|
||||
Thanks for your Pull Request! We love contributions.
|
||||
|
||||
However, you should instead open your PR on the main repository:
|
||||
https://github.com/symfony/ai
|
||||
|
||||
This repository is what we call a "subtree split": a read-only subset of that main repository.
|
||||
We're looking forward to your PR there!
|
||||
4
src/platform/src/Bridge/AmazeeAi/.gitignore
vendored
Normal file
4
src/platform/src/Bridge/AmazeeAi/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
.phpunit.result.cache
|
||||
7
src/platform/src/Bridge/AmazeeAi/CHANGELOG.md
Normal file
7
src/platform/src/Bridge/AmazeeAi/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.4
|
||||
---
|
||||
|
||||
* Add the bridge
|
||||
202
src/platform/src/Bridge/AmazeeAi/CompletionsResultConverter.php
Normal file
202
src/platform/src/Bridge/AmazeeAi/CompletionsResultConverter.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\AmazeeAi;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Generic\CompletionsModel;
|
||||
use Symfony\AI\Platform\Exception\AuthenticationException;
|
||||
use Symfony\AI\Platform\Exception\BadRequestException;
|
||||
use Symfony\AI\Platform\Exception\ContentFilterException;
|
||||
use Symfony\AI\Platform\Exception\RateLimitExceededException;
|
||||
use Symfony\AI\Platform\Exception\RuntimeException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\Result\ChoiceResult;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
use Symfony\AI\Platform\Result\RawResultInterface;
|
||||
use Symfony\AI\Platform\Result\ResultInterface;
|
||||
use Symfony\AI\Platform\Result\StreamResult;
|
||||
use Symfony\AI\Platform\Result\TextResult;
|
||||
use Symfony\AI\Platform\Result\ToolCall;
|
||||
use Symfony\AI\Platform\Result\ToolCallResult;
|
||||
use Symfony\AI\Platform\ResultConverterInterface;
|
||||
use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Completions ResultConverter for amazee.ai's LiteLLM proxy.
|
||||
*
|
||||
* LiteLLM may return finish_reason "tool_calls" for structured output
|
||||
* responses but place the content in message.content instead of
|
||||
* message.tool_calls. This converter handles that quirk by checking
|
||||
* for tool_calls first and falling back to message.content as TextResult.
|
||||
*/
|
||||
class CompletionsResultConverter implements ResultConverterInterface
|
||||
{
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return $model instanceof CompletionsModel;
|
||||
}
|
||||
|
||||
public function convert(RawResultInterface|RawHttpResult $result, array $options = []): ResultInterface
|
||||
{
|
||||
$response = $result->getObject();
|
||||
\assert($response instanceof ResponseInterface);
|
||||
|
||||
if (401 === $response->getStatusCode()) {
|
||||
$errorMessage = json_decode($response->getContent(false), true)['error']['message'];
|
||||
throw new AuthenticationException($errorMessage);
|
||||
}
|
||||
|
||||
if (400 === $response->getStatusCode()) {
|
||||
$errorMessage = json_decode($response->getContent(false), true)['error']['message'] ?? 'Bad Request';
|
||||
throw new BadRequestException($errorMessage);
|
||||
}
|
||||
|
||||
if (429 === $response->getStatusCode()) {
|
||||
throw new RateLimitExceededException();
|
||||
}
|
||||
|
||||
if ($options['stream'] ?? false) {
|
||||
return new StreamResult($this->convertStream($result));
|
||||
}
|
||||
|
||||
$data = $result->getData();
|
||||
|
||||
if (isset($data['error']['code']) && 'content_filter' === $data['error']['code']) {
|
||||
throw new ContentFilterException($data['error']['message']);
|
||||
}
|
||||
|
||||
if (isset($data['error'])) {
|
||||
throw new RuntimeException(\sprintf('Error "%s"-%s (%s): "%s".', $data['error']['code'] ?? '-', $data['error']['type'] ?? '-', $data['error']['param'] ?? '-', $data['error']['message'] ?? '-'));
|
||||
}
|
||||
|
||||
if (!isset($data['choices'])) {
|
||||
throw new RuntimeException('Response does not contain choices.');
|
||||
}
|
||||
|
||||
$choices = array_map($this->convertChoice(...), $data['choices']);
|
||||
|
||||
return 1 === \count($choices) ? $choices[0] : new ChoiceResult($choices);
|
||||
}
|
||||
|
||||
public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private function convertStream(RawResultInterface|RawHttpResult $result): \Generator
|
||||
{
|
||||
$toolCalls = [];
|
||||
foreach ($result->getDataStream() as $data) {
|
||||
if ($this->streamIsToolCall($data)) {
|
||||
$toolCalls = $this->convertStreamToToolCalls($toolCalls, $data);
|
||||
}
|
||||
|
||||
if ([] !== $toolCalls && $this->isToolCallsStreamFinished($data)) {
|
||||
yield new ToolCallResult(...array_map($this->convertToolCall(...), $toolCalls));
|
||||
}
|
||||
|
||||
if (!isset($data['choices'][0]['delta']['content'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $data['choices'][0]['delta']['content'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $toolCalls
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function convertStreamToToolCalls(array $toolCalls, array $data): array
|
||||
{
|
||||
if (!isset($data['choices'][0]['delta']['tool_calls'])) {
|
||||
return $toolCalls;
|
||||
}
|
||||
|
||||
foreach ($data['choices'][0]['delta']['tool_calls'] as $i => $toolCall) {
|
||||
if (isset($toolCall['id'])) {
|
||||
$toolCalls[$i] = [
|
||||
'id' => $toolCall['id'],
|
||||
'function' => $toolCall['function'],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$toolCalls[$i]['function']['arguments'] .= $toolCall['function']['arguments'];
|
||||
}
|
||||
|
||||
return $toolCalls;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function streamIsToolCall(array $data): bool
|
||||
{
|
||||
return isset($data['choices'][0]['delta']['tool_calls']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function isToolCallsStreamFinished(array $data): bool
|
||||
{
|
||||
return isset($data['choices'][0]['finish_reason']) && 'tool_calls' === $data['choices'][0]['finish_reason'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a choice, handling LiteLLM's quirk where finish_reason is
|
||||
* "tool_calls" but the actual content is in message.content (structured output)
|
||||
* instead of message.tool_calls.
|
||||
*
|
||||
* @param array<string, mixed> $choice
|
||||
*/
|
||||
private function convertChoice(array $choice): ToolCallResult|TextResult
|
||||
{
|
||||
if ('tool_calls' === $choice['finish_reason']) {
|
||||
if (isset($choice['message']['tool_calls'])) {
|
||||
return new ToolCallResult(...array_map([$this, 'convertToolCall'], $choice['message']['tool_calls']));
|
||||
}
|
||||
|
||||
// LiteLLM structured output: finish_reason is "tool_calls" but
|
||||
// content is in message.content instead of message.tool_calls
|
||||
if (isset($choice['message']['content'])) {
|
||||
return new TextResult($choice['message']['content']);
|
||||
}
|
||||
}
|
||||
|
||||
if (\in_array($choice['finish_reason'], ['stop', 'length'], true)) {
|
||||
return new TextResult($choice['message']['content']);
|
||||
}
|
||||
|
||||
throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finish_reason']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* id: string,
|
||||
* type: 'function',
|
||||
* function: array{
|
||||
* name: string,
|
||||
* arguments: string
|
||||
* }
|
||||
* } $toolCall
|
||||
*/
|
||||
private function convertToolCall(array $toolCall): ToolCall
|
||||
{
|
||||
$arguments = json_decode($toolCall['function']['arguments'], true, flags: \JSON_THROW_ON_ERROR);
|
||||
|
||||
return new ToolCall($toolCall['id'], $toolCall['function']['name'], $arguments);
|
||||
}
|
||||
}
|
||||
19
src/platform/src/Bridge/AmazeeAi/LICENSE
Normal file
19
src/platform/src/Bridge/AmazeeAi/LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2026-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
142
src/platform/src/Bridge/AmazeeAi/ModelApiCatalog.php
Normal file
142
src/platform/src/Bridge/AmazeeAi/ModelApiCatalog.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\AmazeeAi;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Generic\CompletionsModel;
|
||||
use Symfony\AI\Platform\Bridge\Generic\EmbeddingsModel;
|
||||
use Symfony\AI\Platform\Capability;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelCatalog\AbstractModelCatalog;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* Model catalog that discovers available models from the amazee.ai
|
||||
* LiteLLM /model/info endpoint.
|
||||
*
|
||||
* Maps each model to CompletionsModel or EmbeddingsModel based on the
|
||||
* mode field, so the Generic platform's ModelClients can route requests
|
||||
* correctly.
|
||||
*/
|
||||
final class ModelApiCatalog extends AbstractModelCatalog
|
||||
{
|
||||
private bool $modelsAreLoaded = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly string $baseUrl,
|
||||
#[\SensitiveParameter] private readonly ?string $apiKey = null,
|
||||
) {
|
||||
$this->models = [];
|
||||
}
|
||||
|
||||
public function getModel(string $modelName): Model
|
||||
{
|
||||
$this->preloadRemoteModels();
|
||||
|
||||
return parent::getModel($modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{class: class-string, capabilities: list<Capability>}>
|
||||
*/
|
||||
public function getModels(): array
|
||||
{
|
||||
$this->preloadRemoteModels();
|
||||
|
||||
return parent::getModels();
|
||||
}
|
||||
|
||||
private function preloadRemoteModels(): void
|
||||
{
|
||||
if ($this->modelsAreLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->modelsAreLoaded = true;
|
||||
$this->models = [...$this->models, ...$this->fetchRemoteModels()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{class: class-string<Model>, capabilities: list<Capability>}>
|
||||
*/
|
||||
private function fetchRemoteModels(): iterable
|
||||
{
|
||||
$response = $this->httpClient->request('GET', $this->baseUrl.'/model/info', [
|
||||
'headers' => array_filter([
|
||||
'Authorization' => $this->apiKey ? 'Bearer '.$this->apiKey : null,
|
||||
]),
|
||||
]);
|
||||
|
||||
foreach ($response->toArray()['data'] ?? [] as $modelInfo) {
|
||||
$name = $modelInfo['model_name'] ?? null;
|
||||
if (null === $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$info = $modelInfo['model_info'] ?? [];
|
||||
$mode = $info['mode'] ?? null;
|
||||
|
||||
if ('embedding' === $mode) {
|
||||
yield $name => [
|
||||
'class' => EmbeddingsModel::class,
|
||||
'capabilities' => $this->buildEmbeddingCapabilities($info),
|
||||
];
|
||||
} else {
|
||||
yield $name => [
|
||||
'class' => CompletionsModel::class,
|
||||
'capabilities' => $this->buildCompletionsCapabilities($info),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $info
|
||||
*
|
||||
* @return list<Capability>
|
||||
*/
|
||||
private function buildEmbeddingCapabilities(array $info): array
|
||||
{
|
||||
$capabilities = [Capability::EMBEDDINGS, Capability::INPUT_TEXT];
|
||||
|
||||
if ($info['supports_multiple_inputs'] ?? true) {
|
||||
$capabilities[] = Capability::INPUT_MULTIPLE;
|
||||
}
|
||||
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $info
|
||||
*
|
||||
* @return list<Capability>
|
||||
*/
|
||||
private function buildCompletionsCapabilities(array $info): array
|
||||
{
|
||||
$capabilities = [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING];
|
||||
|
||||
if ($info['supports_image_input'] ?? false) {
|
||||
$capabilities[] = Capability::INPUT_IMAGE;
|
||||
}
|
||||
if ($info['supports_audio_input'] ?? false) {
|
||||
$capabilities[] = Capability::INPUT_AUDIO;
|
||||
}
|
||||
if ($info['supports_tool_calling'] ?? $info['supports_function_calling'] ?? false) {
|
||||
$capabilities[] = Capability::TOOL_CALLING;
|
||||
}
|
||||
if ($info['supports_response_schema'] ?? false) {
|
||||
$capabilities[] = Capability::OUTPUT_STRUCTURED;
|
||||
}
|
||||
|
||||
return $capabilities;
|
||||
}
|
||||
}
|
||||
50
src/platform/src/Bridge/AmazeeAi/PlatformFactory.php
Normal file
50
src/platform/src/Bridge/AmazeeAi/PlatformFactory.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\AmazeeAi;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\AI\Platform\Bridge\Generic\Completions\ModelClient;
|
||||
use Symfony\AI\Platform\Bridge\Generic\Embeddings;
|
||||
use Symfony\AI\Platform\Contract;
|
||||
use Symfony\AI\Platform\ModelCatalog\FallbackModelCatalog;
|
||||
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
|
||||
use Symfony\AI\Platform\Platform;
|
||||
use Symfony\Component\HttpClient\EventSourceHttpClient;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class PlatformFactory
|
||||
{
|
||||
public static function create(
|
||||
string $baseUrl,
|
||||
#[\SensitiveParameter] ?string $apiKey = null,
|
||||
?HttpClientInterface $httpClient = null,
|
||||
ModelCatalogInterface $modelCatalog = new FallbackModelCatalog(),
|
||||
?Contract $contract = null,
|
||||
?EventDispatcherInterface $eventDispatcher = null,
|
||||
): Platform {
|
||||
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
|
||||
|
||||
return new Platform(
|
||||
[
|
||||
new ModelClient($httpClient, $baseUrl, $apiKey),
|
||||
new Embeddings\ModelClient($httpClient, $baseUrl, $apiKey),
|
||||
],
|
||||
[
|
||||
new CompletionsResultConverter(),
|
||||
new Embeddings\ResultConverter(),
|
||||
],
|
||||
$modelCatalog,
|
||||
$contract,
|
||||
$eventDispatcher,
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/platform/src/Bridge/AmazeeAi/README.md
Normal file
12
src/platform/src/Bridge/AmazeeAi/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
amazee.ai Platform
|
||||
===================
|
||||
|
||||
amazee.ai platform bridge for Symfony AI.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/ai/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/ai/pulls)
|
||||
in the [main Symfony AI repository](https://github.com/symfony/ai)
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\AmazeeAi\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\CompletionsResultConverter;
|
||||
use Symfony\AI\Platform\Bridge\Generic\CompletionsModel;
|
||||
use Symfony\AI\Platform\Bridge\Generic\EmbeddingsModel;
|
||||
use Symfony\AI\Platform\Capability;
|
||||
use Symfony\AI\Platform\Exception\RuntimeException;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
use Symfony\AI\Platform\Result\TextResult;
|
||||
use Symfony\AI\Platform\Result\ToolCallResult;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\JsonMockResponse;
|
||||
|
||||
final class CompletionsResultConverterTest extends TestCase
|
||||
{
|
||||
public function testSupportsCompletionsModel()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
|
||||
$this->assertTrue($converter->supports(new CompletionsModel('test', [Capability::INPUT_MESSAGES])));
|
||||
}
|
||||
|
||||
public function testDoesNotSupportEmbeddingsModel()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
|
||||
$this->assertFalse($converter->supports(new EmbeddingsModel('test', [Capability::EMBEDDINGS])));
|
||||
}
|
||||
|
||||
public function testConvertStopFinishReason()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
$result = new RawHttpResult($this->createResponse([
|
||||
'choices' => [
|
||||
[
|
||||
'finish_reason' => 'stop',
|
||||
'message' => ['content' => 'Hello world'],
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
||||
$converted = $converter->convert($result);
|
||||
|
||||
$this->assertInstanceOf(TextResult::class, $converted);
|
||||
$this->assertSame('Hello world', $converted->getContent());
|
||||
}
|
||||
|
||||
public function testConvertToolCallsFinishReasonWithToolCalls()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
$result = new RawHttpResult($this->createResponse([
|
||||
'choices' => [
|
||||
[
|
||||
'finish_reason' => 'tool_calls',
|
||||
'message' => [
|
||||
'tool_calls' => [
|
||||
[
|
||||
'id' => 'call_123',
|
||||
'type' => 'function',
|
||||
'function' => [
|
||||
'name' => 'get_weather',
|
||||
'arguments' => '{"city":"Paris"}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
||||
$converted = $converter->convert($result);
|
||||
|
||||
$this->assertInstanceOf(ToolCallResult::class, $converted);
|
||||
}
|
||||
|
||||
public function testConvertToolCallsFinishReasonWithContentFallback()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
$result = new RawHttpResult($this->createResponse([
|
||||
'choices' => [
|
||||
[
|
||||
'finish_reason' => 'tool_calls',
|
||||
'message' => [
|
||||
'content' => '{"recipe":"Pasta Carbonara","ingredients":["pasta","eggs","bacon"]}',
|
||||
],
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
||||
$converted = $converter->convert($result);
|
||||
|
||||
$this->assertInstanceOf(TextResult::class, $converted);
|
||||
$this->assertSame(
|
||||
'{"recipe":"Pasta Carbonara","ingredients":["pasta","eggs","bacon"]}',
|
||||
$converted->getContent(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testConvertToolCallsFinishReasonWithoutContentThrows()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
$result = new RawHttpResult($this->createResponse([
|
||||
'choices' => [
|
||||
[
|
||||
'finish_reason' => 'tool_calls',
|
||||
'message' => [],
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Unsupported finish reason "tool_calls"');
|
||||
$converter->convert($result);
|
||||
}
|
||||
|
||||
public function testGetTokenUsageExtractorReturnsNull()
|
||||
{
|
||||
$converter = new CompletionsResultConverter();
|
||||
|
||||
$this->assertNull($converter->getTokenUsageExtractor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function createResponse(array $data): \Symfony\Contracts\HttpClient\ResponseInterface
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($data),
|
||||
]);
|
||||
|
||||
return $httpClient->request('POST', 'https://litellm.example.com/v1/chat/completions');
|
||||
}
|
||||
}
|
||||
179
src/platform/src/Bridge/AmazeeAi/Tests/ModelApiCatalogTest.php
Normal file
179
src/platform/src/Bridge/AmazeeAi/Tests/ModelApiCatalogTest.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\AmazeeAi\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\ModelApiCatalog;
|
||||
use Symfony\AI\Platform\Bridge\Generic\CompletionsModel;
|
||||
use Symfony\AI\Platform\Bridge\Generic\EmbeddingsModel;
|
||||
use Symfony\AI\Platform\Capability;
|
||||
use Symfony\AI\Platform\Exception\ModelNotFoundException;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\JsonMockResponse;
|
||||
|
||||
final class ModelApiCatalogTest extends TestCase
|
||||
{
|
||||
public function testLazyLoadModels()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$models = $catalog->getModels();
|
||||
|
||||
$this->assertArrayHasKey('claude-3-5-sonnet', $models);
|
||||
$this->assertArrayHasKey('titan-embed-text-v2:0', $models);
|
||||
}
|
||||
|
||||
public function testCompletionsModel()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$model = $catalog->getModel('claude-3-5-sonnet');
|
||||
|
||||
$this->assertInstanceOf(CompletionsModel::class, $model);
|
||||
}
|
||||
|
||||
public function testEmbeddingsModel()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$model = $catalog->getModel('titan-embed-text-v2:0');
|
||||
|
||||
$this->assertInstanceOf(EmbeddingsModel::class, $model);
|
||||
}
|
||||
|
||||
public function testCompletionsCapabilities()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$models = $catalog->getModels();
|
||||
$capabilities = $models['claude-3-5-sonnet']['capabilities'];
|
||||
|
||||
$this->assertContains(Capability::INPUT_MESSAGES, $capabilities);
|
||||
$this->assertContains(Capability::OUTPUT_TEXT, $capabilities);
|
||||
$this->assertContains(Capability::OUTPUT_STREAMING, $capabilities);
|
||||
$this->assertContains(Capability::INPUT_IMAGE, $capabilities);
|
||||
$this->assertContains(Capability::TOOL_CALLING, $capabilities);
|
||||
$this->assertContains(Capability::OUTPUT_STRUCTURED, $capabilities);
|
||||
}
|
||||
|
||||
public function testEmbeddingCapabilities()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$models = $catalog->getModels();
|
||||
$capabilities = $models['titan-embed-text-v2:0']['capabilities'];
|
||||
|
||||
$this->assertContains(Capability::EMBEDDINGS, $capabilities);
|
||||
$this->assertContains(Capability::INPUT_TEXT, $capabilities);
|
||||
$this->assertContains(Capability::INPUT_MULTIPLE, $capabilities);
|
||||
}
|
||||
|
||||
public function testModelNotFound()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$this->expectException(ModelNotFoundException::class);
|
||||
$catalog->getModel('non-existent-model');
|
||||
}
|
||||
|
||||
public function testModelsAreLoadedOnlyOnce()
|
||||
{
|
||||
$callCount = 0;
|
||||
$httpClient = new MockHttpClient(function () use (&$callCount) {
|
||||
++$callCount;
|
||||
|
||||
return new JsonMockResponse($this->getModelInfoResponse());
|
||||
});
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com', 'test-key');
|
||||
|
||||
$catalog->getModels();
|
||||
$catalog->getModels();
|
||||
$catalog->getModel('claude-3-5-sonnet');
|
||||
|
||||
$this->assertSame(1, $callCount);
|
||||
}
|
||||
|
||||
public function testWithoutApiKey()
|
||||
{
|
||||
$httpClient = new MockHttpClient([
|
||||
new JsonMockResponse($this->getModelInfoResponse()),
|
||||
]);
|
||||
|
||||
$catalog = new ModelApiCatalog($httpClient, 'https://litellm.example.com');
|
||||
|
||||
$models = $catalog->getModels();
|
||||
|
||||
$this->assertNotEmpty($models);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getModelInfoResponse(): array
|
||||
{
|
||||
return [
|
||||
'data' => [
|
||||
[
|
||||
'model_name' => 'claude-3-5-sonnet',
|
||||
'model_info' => [
|
||||
'mode' => 'chat',
|
||||
'supports_image_input' => true,
|
||||
'supports_audio_input' => false,
|
||||
'supports_tool_calling' => true,
|
||||
'supports_response_schema' => true,
|
||||
],
|
||||
],
|
||||
[
|
||||
'model_name' => 'claude-3-5-haiku',
|
||||
'model_info' => [
|
||||
'mode' => 'chat',
|
||||
'supports_image_input' => false,
|
||||
'supports_tool_calling' => true,
|
||||
'supports_response_schema' => false,
|
||||
],
|
||||
],
|
||||
[
|
||||
'model_name' => 'titan-embed-text-v2:0',
|
||||
'model_info' => [
|
||||
'mode' => 'embedding',
|
||||
'supports_multiple_inputs' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\AmazeeAi\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Platform\Bridge\AmazeeAi\PlatformFactory;
|
||||
use Symfony\AI\Platform\Platform;
|
||||
use Symfony\Component\HttpClient\EventSourceHttpClient;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
|
||||
final class PlatformFactoryTest extends TestCase
|
||||
{
|
||||
public function testCreateWithDefaults()
|
||||
{
|
||||
$platform = PlatformFactory::create(
|
||||
'https://litellm.example.com',
|
||||
'test-api-key',
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(Platform::class, $platform);
|
||||
}
|
||||
|
||||
public function testCreateWithCustomHttpClient()
|
||||
{
|
||||
$httpClient = new MockHttpClient();
|
||||
|
||||
$platform = PlatformFactory::create(
|
||||
'https://litellm.example.com',
|
||||
'test-api-key',
|
||||
$httpClient,
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(Platform::class, $platform);
|
||||
}
|
||||
|
||||
public function testCreateWithEventSourceHttpClient()
|
||||
{
|
||||
$httpClient = new EventSourceHttpClient(new MockHttpClient());
|
||||
|
||||
$platform = PlatformFactory::create(
|
||||
'https://litellm.example.com',
|
||||
'test-api-key',
|
||||
$httpClient,
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(Platform::class, $platform);
|
||||
}
|
||||
}
|
||||
56
src/platform/src/Bridge/AmazeeAi/composer.json
Normal file
56
src/platform/src/Bridge/AmazeeAi/composer.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "symfony/ai-amazee-ai-platform",
|
||||
"description": "amazee.ai LiteLLM platform bridge for Symfony AI",
|
||||
"license": "MIT",
|
||||
"type": "symfony-ai-platform",
|
||||
"keywords": [
|
||||
"ai",
|
||||
"bridge",
|
||||
"amazeeai",
|
||||
"litellm",
|
||||
"platform"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christophe Jossart",
|
||||
"email": "christophe@colorfield.dev"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/ai-generic-platform": "^0.4",
|
||||
"symfony/ai-platform": "^0.4",
|
||||
"symfony/http-client": "^7.3|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^11.5.53"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\AI\\Platform\\Bridge\\AmazeeAi\\": ""
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Symfony\\AI\\PHPStan\\": "../../../../../.phpstan/",
|
||||
"Symfony\\AI\\Platform\\Bridge\\AmazeeAi\\Tests\\": "Tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/ai",
|
||||
"url": "https://github.com/symfony/ai"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/platform/src/Bridge/AmazeeAi/phpstan.dist.neon
Normal file
29
src/platform/src/Bridge/AmazeeAi/phpstan.dist.neon
Normal file
@@ -0,0 +1,29 @@
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-phpunit/extension.neon
|
||||
- ../../../../../.phpstan/extension.neon
|
||||
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- .
|
||||
- Tests/
|
||||
excludePaths:
|
||||
- vendor/
|
||||
treatPhpDocTypesAsCertain: false
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#"
|
||||
reportUnmatched: false
|
||||
-
|
||||
message: '#^Call to( static)? method PHPUnit\\Framework\\Assert::.* will always evaluate to true\.$#'
|
||||
reportUnmatched: false
|
||||
-
|
||||
identifier: 'symfonyAi.forbidNativeException'
|
||||
path: Tests/*
|
||||
reportUnmatched: false
|
||||
|
||||
services:
|
||||
- # Conditionally enabled by bleeding edge in phpstan/phpstan-phpunit 2.x
|
||||
class: PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension
|
||||
tags:
|
||||
- phpstan.ignoreErrorExtension
|
||||
33
src/platform/src/Bridge/AmazeeAi/phpunit.xml.dist
Normal file
33
src/platform/src/Bridge/AmazeeAi/phpunit.xml.dist
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnDeprecation="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
executionOrder="random"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony AI amazee.ai Platform Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory>./</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory>./Resources</directory>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</source>
|
||||
</phpunit>
|
||||
Reference in New Issue
Block a user