config = new miniMax_c(); } public function requireModuleEnabled(): void { if (!$this->config->enabled->isTrue()) { throw new Exception('MiniMax module is not enabled.'); } $apiKey = trim((string)$this->config->api_key->getVariableValue()); if ($apiKey === '') { throw new Exception('MiniMax API key is not configured.'); } } /** * Send a structured JSON text task to MiniMax M3 (Anthropic-messages format). * * Returns the parsed JSON object plus `_minimax_response_model` and `_minimax_usage` * so the autopilot can compare against the resolved model id and track tokens. * * @throws Exception */ public function jsonTask( string $schemaName, string $prompt, array $payload, array $schema, float $temperature = 0.1, ?string $model = null ): array { $this->requireModuleEnabled(); // Anthropic-messages uses a single `messages` array, system prompt is separate, // and structured output goes in `tools` with `input_schema`. $data = [ 'model' => $model ?? $this->model, 'max_tokens' => (int)$this->max_tokens, 'temperature' => $temperature, 'system' => $prompt, 'messages' => [ [ 'role' => 'user', 'content' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ], ], 'tools' => [ [ 'name' => $schemaName, 'description' => 'Return the structured decision for the XL Vask automation planner.', 'input_schema' => $schema, ], ], // Force the model to call the tool — guarantees a structured JSON object back. 'tool_choice' => ['type' => 'tool', 'name' => $schemaName], ]; $response = $this->sendRequest($data); return self::parseJsonTaskResponse($response, $schemaName); } public static function parseJsonTaskResponse(array $response, string $expectedToolName): array { // Anthropic-messages stop_reason: end_turn | tool_use | max_tokens | stop_sequence $stopReason = (string)($response['stop_reason'] ?? ''); if ($stopReason === 'max_tokens') { throw new minimax_request_exception('MiniMax response was truncated by max_tokens.', true); } if (!in_array($stopReason, ['end_turn', 'tool_use'], true)) { throw new minimax_request_exception('MiniMax response did not complete (stop_reason=' . $stopReason . ').', true); } $toolInput = null; $toolName = null; foreach ((array)($response['content'] ?? []) as $block) { if (($block['type'] ?? null) === 'tool_use') { $toolName = (string)($block['name'] ?? ''); $toolInput = (array)($block['input'] ?? []); break; } } if ($toolInput === null) { throw new minimax_request_exception('MiniMax completed without a structured tool_use block.', false); } if ($toolName !== $expectedToolName) { throw new minimax_request_exception( 'MiniMax returned tool "' . $toolName . '", expected "' . $expectedToolName . '".', false ); } $resolvedModel = trim((string)($response['model'] ?? '')); if ($resolvedModel === '') { throw new minimax_request_exception('MiniMax response omitted the resolved model.', false); } $usage = (array)($response['usage'] ?? []); $inputTokens = max(0, (int)($usage['input_tokens'] ?? 0)); $outputTokens = max(0, (int)($usage['output_tokens'] ?? 0)); // The legacy `_openai_*` aliases keep the autopilot's sanitizeOpenAiResult() working // unchanged — it reads those keys regardless of which provider produced the result. return [ ...$toolInput, '_minimax_response_model' => $resolvedModel, '_minimax_usage' => [ 'input_tokens' => $inputTokens, 'output_tokens' => $outputTokens, 'total_tokens' => $inputTokens + $outputTokens, 'service_tier' => '', ], '_openai_response_model' => $resolvedModel, '_openai_usage' => [ 'input_tokens' => $inputTokens, 'output_tokens' => $outputTokens, 'total_tokens' => $inputTokens + $outputTokens, 'service_tier' => '', ], ]; } /** * @throws Exception */ private function sendRequest(array $data): array { $this->requireModuleEnabled(); $curl = curl_init($this->api_url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($curl, CURLOPT_TIMEOUT, 60); // MiniMax uses Anthropic-style auth headers curl_setopt($curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-api-key: ' . $this->config->api_key->getVariableValue(), 'anthropic-version: 2023-06-01', ]); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); $response = curl_exec($curl); if (curl_errno($curl)) { $curlCode = curl_errno($curl); curl_close($curl); throw new minimax_request_exception( 'MiniMax transport failed.', in_array($curlCode, [CURLE_OPERATION_TIMEDOUT, CURLE_COULDNT_CONNECT, CURLE_COULDNT_RESOLVE_HOST], true) ); } $httpStatus = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new minimax_request_exception('MiniMax returned a non-JSON response.', $httpStatus === 429 || $httpStatus >= 500, $httpStatus); } if ($httpStatus < 200 || $httpStatus >= 300 || !is_array($responseData)) { throw new minimax_request_exception('MiniMax request failed with HTTP status ' . $httpStatus . '.', $httpStatus === 429 || $httpStatus >= 500, $httpStatus); } return $responseData; } }