A while ago, I integrated TianliGPT into my articles to auto-generate a “TL;DR” summary block. TianliGPT offers a super-simple drop-in snippet, but it’s focused only on summarization and related-article recommendations; extending it further is pretty limited. So I recently ditched TianliGPT and switched to Moonshot AI for both summarization and extra features.
Defining the requirements
Besides generating a summary, we also want to pose relevant questions (and their answers) based on the article. Clicking a question reveals its answer—something like the image below:

Preview of the article-summary module
To meet that need, the model must return JSON like this for the front-end to consume:
{
"summary": "Article summary content",
"qa": [
{
"question": "Question 1",
"answer": "Answer 1"
},{
"question": "Question 2",
"answer": "Answer 2"
},
...
]
}
So we craft the following prompt:
Design a concise list of questions that dig out professional concepts or under-explained parts from the article. Provide a summary and create 6 precise questions with answers. Reply in this JSON format:
{
"summary": "Article summary content",
"qa": [
{
"question": "Question 1",
"answer": "Answer 1"
},
...
]
}
Put the summary in the summary field, questions in question, and answers in answer.
A prompt is the information or instruction given to the model to guide its response. An effective prompt needs clarity, relevance, brevity, context, and instructiveness.
Since Kimi and Moonshot AI share the same backbone, we can test with Kimi to preview what the Moonshot API will return (and save some money). After chatting with Kimi using the prompt above, the result looks like this:

Model chat result
Talking to the model
With the “what” settled, we tackle the “how”. Moonshot’s docs provide Python and Node.js samples; here we’ll do it in PHP.
The official Chat Completions endpoint is https://api.moonshot.cn/v1/chat/completions. Headers and body look like:
# Headers
{
"Content-Type": "application/json",
"Authorization": "Bearer $apiKey"
}
# Body
{
"model": "moonshot-v1-8k",
"messages": [
{
"role": "system",
"content": "You are Kimi, an AI assistant provided by Moonshot AI. You excel at Chinese and English conversations, offering safe, helpful, and accurate answers while refusing terrorism, racism, pornography, or violence. 'Moonshot AI' is a proper noun and must not be translated."
},
{ "role": "user", "content": "Hi, my name is Lilei. What is 1+1?" }
],
"temperature": 0.3
}
model: pickmoonshot-v1-8k,moonshot-v1-32k, ormoonshot-v1-128k.messages: array of chat turns. Roles aresystem,user, orassistant.temperature: sampling temperature, recommended0.3.
We wrap this in a MoonshotAPI class:
class MoonshotAPI {
private $apiKey;
private $baseUrl;
public function __construct($apiKey) {
$this->apiKey = $apiKey;
$this->baseUrl = "https://api.moonshot.cn/v1/chat/completions";
}
/**
* Send request and return API response
* @param string $model Model name
* @param array $messages Message array
* @param float $temperature Temperature for creativity
* @return mixed API response data
*/
public function sendRequest($model, $messages, $temperature) {
$payload = $this->preparePayload($model, $messages, $temperature);
$response = $this->executeCurlRequest($payload);
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Invalid response format.");
}
return $responseData;
}
/**
* Build request body
* @param string $model
* @param array $messages
* @param float $temperature
* @return array Request payload
*/
private function preparePayload($model, $messages, $temperature) {
return [
'model' => $model,
'messages' => $messages,
'temperature' => $temperature,
'response_format' => ["type" => "json_object"] # Enable JSON Mode
];
}
/**
* Execute cURL request
* @param array $data Request data
* @return string API response
*/
private function executeCurlRequest($data) {
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $this->baseUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey
],
]);
$response = curl_exec($curl);
if ($response === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException($error);
}
curl_close($curl);
return $response;
}
}
[!NOTE] If you simply tell Kimi “please output JSON”, it will—sort of. You’ll usually get extra explanatory text around the JSON. Enabling JSON Mode (
'response_format' => ["type" => "json_object"]) forces a clean, parseable JSON document.
The only tricky parameter is $messages, so we add a helper:
/**
* Build message array
*
* @param string $articleText Article content
* @return array System and user messages
*/
function getMessages($articleText) {
return [
[
"role" => "system",
"content" => <<<EOT
Design a concise list of questions that dig out professional concepts or under-explained parts from the article. Provide a summary and create 6 precise questions with answers. Reply in this JSON format:
{
"summary": "Article content",
"qa": [
{
"question": "Question1",
"answer": "Answer1"
},
...
]
}
Put the summary in summary, questions in question, answers in answer.
EOT
],
[
"role" => "user",
"content" => $articleText
]
];
}
We put our prompt in system and the article in the first user message. The messages array is chronological: system → user → assistant, keeping context intact.
The model returns JSON like this; we care about the choices array:
{
"id": "chatcmpl-xxxxxx",
"object": "chat.completion",
"created": xxxxxxxx,
"model": "moonshot-v1-8k",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Model reply here"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 229,
"completion_tokens": 64,
"total_tokens": 293
}
}
In our flow choices has exactly one element (hence the hard-coded $result['choices'][0] below). finish_reason tells us whether the answer is complete (stop). content holds the actual reply.
Now glue it together:
/**
* Call MoonshotAPI
*
* @param string $articleText Article content
* @param string $model Defaults to "moonshot-v1-8k"
* @return array Status code and data
*/
function Moonshot($articleText, $model = "moonshot-v1-8k") {
$apiKey = 'sk-xxxxxxxx'; # Your key from the console
$moonshotApi = new MoonshotAPI($apiKey);
$messages = getMessages($articleText);
$temperature = 0.3;
try {
$result = $moonshotApi->sendRequest($model, $messages, $temperature);
if (isset($result['error'])) {
throw new RuntimeException("Model error: " . $result['error']['message']);
}
# Check completeness
$responseContent = $result['choices'][0]['message']['content'] ?? null;
if ($responseContent === null || $result['choices'][0]['finish_reason'] !== "stop") {
throw new RuntimeException("Response missing or truncated.");
}
# Validate JSON
$decodedResponse = json_decode($responseContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Invalid response format.");
}
return $result;
} catch (Exception $e) {
return ['stat' => 400, 'message' => $e->getMessage()];
}
}
That’s it—call it and you’ll get the model’s reply ✌️. The front-end can render it however it likes; we’ll skip that part.
header('Content-Type: application/json');
class MoonshotAPI {...}
function getMessages(...) {...}
function Moonshot(...) {...}
# Example
try {
$article = "This is the article content";
$aiResponse = Moonshot($article);
# Echo or further process
echo json_encode($aiResponse, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['stat' => 400, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
Extra: handling very long articles
For some articles you may hit:
{
"error": {
"type": "invalid_request_error",
"message": "Invalid request: Your request exceeded model token limit: 8192"
}
}
The combined prompt + response tokens exceed the model’s limit (default here is moonshot-v1-8k). We need to pick a bigger model. The docs give sample code for choosing the right model; we port it to PHP:
/**
* Estimate token count for messages.
* Uses the estimate-token-count API.
*
* @param string $apiKey
* @param array $inputMessages
* @return int Estimated total tokens
*/
function estimateTokenCount($apiKey, $inputMessages) {
$header = [
'Authorization: Bearer ' . $apiKey,
];
$data = [
'model' => 'moonshot-v1-128k',
'messages' => $inputMessages,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://api.moonshot.cn/v1/tokenizers/estimate-token-count',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey
],
]);
$response = curl_exec($curl);
if ($response === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException($error);
}
curl_close($curl);
$result = json_decode($response, true);
return $result['data']['total_tokens'];
}
/**
* Pick the best model based on estimated tokens.
*
* @param string $apiKey
* @param array $inputMessages
* @param int $defaultMaxTokens Default max tokens for response
* @return string Model name
*/
function selectModel($apiKey, $inputMessages, $defaultMaxTokens = 1024) {
$promptTokenCount = estimateTokenCount($apiKey, $inputMessages);
$totalAllowedTokens = $promptTokenCount + $defaultMaxTokens;
if ($totalAllowedTokens <= 8 * 1024) {
return "moonshot-v1-8k";
} elseif ($totalAllowedTokens <= 32 * 1024) {
return "moonshot-v1-32k";
} elseif ($totalAllowedTokens <= 128 * 1024) {
return "moonshot-v1-128k";
} else {
throw new Exception("Tokens exceed maximum limit.");
}
}
Inside Moonshot, when we get invalid_request_error, we retry with the bigger model:
function Moonshot($articleText, $model = "moonshot-v1-8k") {
...
if (isset($result['error'])) {
if ($result['error']['type'] === "invalid_request_error") {
$model = selectModel($apiKey, $messages);
return Moonshot($articleText, $model);
} else {
throw new RuntimeException("Model error: " . $result['error']['message']);
}
}
...
}