Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,37 @@ private static function getProviderData(string $orgCode): array
]),
'remark' => '',
],
// MiniMax - LLM
[
'name' => 'MiniMax',
'provider_code' => ProviderCode::MiniMax->value,
'sort_order' => 992,
'description' => 'MiniMax 是一家领先的 AI 大模型公司,提供高性能的 MiniMax-M2.7 和 MiniMax-M2.5 系列模型,支持百万级超长上下文、工具调用和深度思考能力,通过 OpenAI 兼容接口提供服务。',
'icon' => 'MAGIC/713471849556451329/default/default.png',
'provider_type' => 0,
'category' => 'llm',
'status' => 1,
'is_models_enable' => 0,
'created_at' => $now,
'updated_at' => $now,
'deleted_at' => null,
'translate' => json_encode([
'name' => [
'en_US' => 'MiniMax',
'zh_CN' => 'MiniMax',
],
'description' => [
'en_US' => 'MiniMax is a leading AI large model company, providing high-performance MiniMax-M2.7 and MiniMax-M2.5 series models with million-token context, tool calling and deep thinking capabilities, served via an OpenAI-compatible API.',
'zh_CN' => 'MiniMax 是一家领先的 AI 大模型公司,提供高性能的 MiniMax-M2.7 和 MiniMax-M2.5 系列模型,支持百万级超长上下文、工具调用和深度思考能力,通过 OpenAI 兼容接口提供服务。',
],
]),
'remark' => '',
],
// DeepSeek - LLM
[
'name' => 'DeepSeek',
'provider_code' => ProviderCode::DeepSeek->value,
'sort_order' => 992,
'sort_order' => 991,
'description' => 'DeepSeek 是一家专注于 AI 大模型的公司,提供高性能的 AI 语言模型服务。',
'icon' => 'MAGIC/713471849556451329/default/default.png',
'provider_type' => 0,
Expand All @@ -336,7 +362,7 @@ private static function getProviderData(string $orgCode): array
[
'name' => '自定义提供商',
'provider_code' => ProviderCode::OpenAI->value,
'sort_order' => 991,
'sort_order' => 990,
'description' => '请使用接口与 OpenAI API 相同形式的服务商',
'icon' => 'MAGIC/713471849556451329/default/默认图标.png',
'provider_type' => 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ enum ProviderCode: string
case DashScope = 'DashScope';
case OpenRouter = 'OpenRouter';
case SuChuang = 'SuChuang';
case MiniMax = 'MiniMax';

public function getImplementation(): string
{
Expand All @@ -48,6 +49,7 @@ public function getImplementation(): string
self::DeepSeek => DeepSeekModel::class,
self::DashScope => DashScopeModel::class,
self::OpenRouter => OpenAIModel::class,
self::MiniMax => OpenAIModel::class,
default => OpenAIModel::class,
};
}
Expand Down Expand Up @@ -121,6 +123,7 @@ public function getSortOrder(): int
self::OpenRouter => 6,
self::Volcengine, self::VolcengineArk => 7,
self::DeepSeek => 8,
self::MiniMax => 9,
default => 999, // 其他服务商排在最后
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ enum ProviderTemplateId: string
case VolcengineArkVlm = '21';
case Gemini = '22';

// MiniMax 相关
case MiniMaxLlm = '23';

/**
* 根据ProviderCode和Category获取对应的模板ID.
*/
Expand Down Expand Up @@ -85,6 +88,7 @@ public static function fromProviderCodeAndCategory(ProviderCode $providerCode, C
[ProviderCode::Google, Category::VLM] => self::GoogleVlm,
[ProviderCode::VolcengineArk, Category::VLM] => self::VolcengineArkVlm,
[ProviderCode::Gemini, Category::LLM] => self::Gemini,
[ProviderCode::MiniMax, Category::LLM] => self::MiniMaxLlm,
default => null,
};
}
Expand Down Expand Up @@ -120,6 +124,7 @@ public function toProviderCodeAndCategory(): array
self::GoogleVlm => ['providerCode' => ProviderCode::Google, 'category' => Category::VLM],
self::VolcengineArkVlm => ['providerCode' => ProviderCode::VolcengineArk, 'category' => Category::VLM],
self::Gemini => ['providerCode' => ProviderCode::Gemini, 'category' => Category::LLM],
self::MiniMaxLlm => ['providerCode' => ProviderCode::MiniMax, 'category' => Category::LLM],
};
}

Expand All @@ -143,6 +148,7 @@ public function getDescription(): string
ProviderCode::Google => 'Google',
ProviderCode::VolcengineArk => '火山引擎-方舟',
ProviderCode::Gemini => 'Google Gemini',
ProviderCode::MiniMax => 'MiniMax',
default => '未知服务商',
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);
/**
* Copyright (c) The Magic , Distributed under the software license
*/

namespace App\Domain\Provider\Service\ConnectivityTest\LLM;

use App\Domain\Provider\DTO\Item\ProviderConfigItem;
use App\Domain\Provider\Service\ConnectivityTest\ConnectResponse;
use App\Domain\Provider\Service\ConnectivityTest\IProvider;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Hyperf\Codec\Json;

use function Hyperf\Translation\__;

/**
* MiniMax LLM connectivity test provider.
*
* Unlike the DeepSeek provider which only lists models, this provider
* validates connectivity by sending a lightweight chat completion request
* with the specified model version (similar to LLMVolcengineProvider).
* MiniMax requires temperature in (0.0, 1.0], so we clamp it explicitly.
*/
class LLMMiniMaxProvider implements IProvider
{
protected string $apiBase = 'https://api.minimax.io/v1';

public function connectivityTestByModel(ProviderConfigItem $serviceProviderConfig, string $modelVersion): ConnectResponse
{
$connectResponse = new ConnectResponse();
$connectResponse->setStatus(true);
$apiKey = $serviceProviderConfig->getApiKey();
if (empty($apiKey)) {
$connectResponse->setStatus(false);
$connectResponse->setMessage(__('service_provider.api_key_empty'));
return $connectResponse;
}
try {
$this->testChatCompletion($apiKey, $modelVersion);
} catch (Exception $e) {
$connectResponse->setStatus(false);
if ($e instanceof ClientException) {
$connectResponse->setMessage(Json::decode($e->getResponse()->getBody()->getContents()));
} else {
$connectResponse->setMessage($e->getMessage());
}
}

return $connectResponse;
}

/**
* Test connectivity by sending a minimal chat completion request.
*
* This validates both the API key and that the specific model version
* is accessible, rather than just listing available models.
* MiniMax requires temperature strictly in (0.0, 1.0].
*/
protected function testChatCompletion(string $apiKey, string $modelVersion): array
{
$client = new Client();
$payload = [
'model' => $modelVersion,
'max_tokens' => 1,
'temperature' => 0.01,
'messages' => [
[
'role' => 'user',
'content' => 'Hi',
],
],
];

$response = $client->request('POST', $this->apiBase . '/chat/completions', [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
],
'json' => $payload,
]);

return Json::decode($response->getBody()->getContents());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

declare(strict_types=1);
/**
* Copyright (c) The Magic , Distributed under the software license
*/

namespace HyperfTest\Cases\Unit\Provider;

use App\Domain\Provider\DTO\Item\ProviderConfigItem;
use App\Domain\Provider\Service\ConnectivityTest\LLM\LLMMiniMaxProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;

/**
* @internal
*/
class LLMMiniMaxProviderTest extends TestCase
{
public function testApiBaseIsCorrect(): void
{
$provider = new LLMMiniMaxProvider();
$reflection = new \ReflectionProperty($provider, 'apiBase');
$reflection->setAccessible(true);
$this->assertSame('https://api.minimax.io/v1', $reflection->getValue($provider));
}

public function testConnectivityTestFailsWithEmptyApiKey(): void
{
$provider = new LLMMiniMaxProvider();
$config = new ProviderConfigItem([]);

$response = $provider->connectivityTestByModel($config, 'MiniMax-M2.7');

$this->assertFalse($response->getStatus());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests call nonexistent getStatus() instead of isStatus()

Medium Severity

The tests call $response->getStatus() on a ConnectResponse object, but ConnectResponse only defines isStatus() for the boolean $status property. There is no getStatus() method and no __call magic method anywhere in the class hierarchy (BaseObjectAbstractObjectAbstractEntityConnectResponse), so every test assertion using getStatus() will fail at runtime with an undefined method error.

Additional Locations (2)
Fix in Cursor Fix in Web

}

public function testConnectivityTestSucceedsWithValidApiKey(): void
{
// Create a mock HTTP handler that returns a successful response
$mock = new MockHandler([
new Response(200, [], json_encode([
'object' => 'list',
'data' => [
['id' => 'MiniMax-M2.7', 'object' => 'model'],
['id' => 'MiniMax-M2.5', 'object' => 'model'],
],
])),
]);

$handlerStack = HandlerStack::create($mock);
$mockClient = new Client(['handler' => $handlerStack]);

// Use a subclass to inject the mock client
$provider = new class($mockClient) extends LLMMiniMaxProvider {
private Client $mockClient;

public function __construct(Client $mockClient)
{
$this->mockClient = $mockClient;
}

protected function fetchModels(string $apiKey): array
{
$response = $this->mockClient->request('GET', $this->apiBase . '/models', [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
],
]);

return json_decode($response->getBody()->getContents(), true);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests mock nonexistent method, bypassing HTTP mocks entirely

High Severity

The test subclasses override a fetchModels method that does not exist on LLMMiniMaxProvider. The actual provider calls testChatCompletion, so the mock HTTP client is never used. All three tests (testConnectivityTestSucceedsWithValidApiKey, testConnectivityTestFailsWithInvalidApiKey, testConnectivityTestFailsOnNetworkError) will make real HTTP requests to api.minimax.io instead of using the mocked responses, making them flaky and not actually testing the intended behavior. The override needs to target testChatCompletion instead of fetchModels.

Additional Locations (2)
Fix in Cursor Fix in Web

};

$config = new ProviderConfigItem(['api_key' => 'test-valid-key']);

$response = $provider->connectivityTestByModel($config, 'MiniMax-M2.7');

$this->assertTrue($response->getStatus());
}

public function testConnectivityTestFailsWithInvalidApiKey(): void
{
// Create a mock HTTP handler that returns an authentication error
$mock = new MockHandler([
new \GuzzleHttp\Exception\ClientException(
'Client error',
new \GuzzleHttp\Psr7\Request('GET', 'https://api.minimax.io/v1/models'),
new Response(401, [], json_encode([
'error' => [
'message' => 'Invalid API key',
'type' => 'authentication_error',
],
]))
),
]);

$handlerStack = HandlerStack::create($mock);
$mockClient = new Client(['handler' => $handlerStack]);

$provider = new class($mockClient) extends LLMMiniMaxProvider {
private Client $mockClient;

public function __construct(Client $mockClient)
{
$this->mockClient = $mockClient;
}

protected function fetchModels(string $apiKey): array
{
$response = $this->mockClient->request('GET', $this->apiBase . '/models', [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
],
]);

return json_decode($response->getBody()->getContents(), true);
}
};

$config = new ProviderConfigItem(['api_key' => 'invalid-key']);

$response = $provider->connectivityTestByModel($config, 'MiniMax-M2.7');

$this->assertFalse($response->getStatus());
}

public function testConnectivityTestFailsOnNetworkError(): void
{
// Create a mock HTTP handler that throws a network exception
$mock = new MockHandler([
new \GuzzleHttp\Exception\ConnectException(
'Connection refused',
new \GuzzleHttp\Psr7\Request('GET', 'https://api.minimax.io/v1/models')
),
]);

$handlerStack = HandlerStack::create($mock);
$mockClient = new Client(['handler' => $handlerStack]);

$provider = new class($mockClient) extends LLMMiniMaxProvider {
private Client $mockClient;

public function __construct(Client $mockClient)
{
$this->mockClient = $mockClient;
}

protected function fetchModels(string $apiKey): array
{
$response = $this->mockClient->request('GET', $this->apiBase . '/models', [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
],
]);

return json_decode($response->getBody()->getContents(), true);
}
};

$config = new ProviderConfigItem(['api_key' => 'test-key']);

$response = $provider->connectivityTestByModel($config, 'MiniMax-M2.7');

$this->assertFalse($response->getStatus());
$this->assertNotEmpty($response->getMessage());
}
}
Loading