mh-code / ai
A lightweight OpenAI-compatible AI client for PHP. Supports streaming and direct responses, works with any provider that implements the OpenAI API (OpenAI, DeepSeek, Moonshot, Qwen, Zhipu, Ollama, etc.).
Requires
- php: >=8.0
- ext-curl: *
- ext-json: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-21 05:43:57 UTC
README
一个轻量级、无第三方依赖的 PHP OpenAI 兼容客户端。支持流式输出与直接返回两种调用方式,可用于所有实现了 OpenAI HTTP API 的 AI 服务:
- OpenAI / Azure OpenAI
- DeepSeek(深度求索)
- Moonshot / Kimi(月之暗面)
- 通义千问 Qwen(DashScope 兼容模式)
- 智谱 GLM(BigModel)
- Groq
- Ollama / LM Studio 等本地服务
- 以及其他任何兼容 OpenAI 接口的服务
环境要求
- PHP >= 8.0
- 扩展:
ext-curl、ext-json
流式输出已知问题
在 Swoole 协程 + cURL 环境下,流式输出是否正常与底层 libc 有关。
目前已测试:
| 环境 | PHP | Swoole | libc | 流式输出 |
|---|---|---|---|---|
| 原生 PHP + Swoole 协程 | 8.4.21 | 6.0.2 | glibc | ❌ 不支持 |
Swoole CLI (sw6) | 8.4.x | 6.2.0 | musl | ✅ 正常 |
Swoole CLI (sw5) | 8.1.x | 5.1.5 | musl | ✅ 正常 |
结论:
Swoole Coroutine + cURL 的流式输出依赖底层 libc 环境。
目前测试结果表明:
musl:流式输出正常glibc:流式输出异常
因此,在 Hyperf / Swoole 协程环境中使用无法正常流式输出,请先检查当前 PHP 使用的是 musl 还是 glibc。
查看 libc
php -i | grep -iE 'libc|musl|glibc'
## 安装
composer require mh-code/ai
## 快速开始
配置在**初始化时传入**,不会从任何动态/全局配置中读取。
<?php
require DIR . '/vendor/autoload.php';
use MhCode\Ai\Client; use MhCode\Ai\Config;
$client = new Client(new Config(
apiKey: 'sk-xxxxxxxx', // 无需 key 的本地服务可留空
baseUrl: 'https://api.deepseek.com/v1', // 替换为目标服务地址
model: 'deepseek-chat', // 默认模型
));
## 直接获取返回值
// 返回完整响应数组 $response = $client->chat([
['role' => 'user', 'content' => '介绍一下你自己'],
]);
// 直接返回生成的文本 $text = $client->chatText([
['role' => 'user', 'content' => '写一首关于春天的诗'],
]); echo $text;
## 流式输出
`chatStream()` 将每个 SSE 数据块解码后回调,可逐字输出;回调返回 `false` 可提前中止。
$client->chatStream(
[['role' => 'user', 'content' => '讲一个笑话']],
function (array $chunk) {
$delta = $chunk['choices'][0]['delta']['content'] ?? '';
if ($delta !== '') {
echo $delta;
flush();
}
},
);
`chatStreamText()` 边流式边累积文本,结束时返回完整内容,可选回调接收每个增量。
$text = $client->chatStreamText(
[['role' => 'user', 'content' => '写一篇短文']],
options: ['temperature' => 0.8],
onPartial: fn (string $delta) => print($delta),
);
## 区分深度思考与正文
深度思考模型(如 DeepSeek 的 `deepseek-reasoner`)在流式返回中带有两种内容:
| 字段 | 含义 |
| --- | --- |
| `delta.reasoning_content` | 模型的深度思考(推理)过程 |
| `delta.content` | 最终回答正文 |
`chatStream()` 回调收到的是完整的原始数据块,可分别读取两个字段并做区分展示:
$thinking = ''; // 深度思考内容 $answer = ''; // 最终正文 $phase = 'thinking'; // 当前阶段:thinking(思考中)/ answer(回答中)
$client->chatStream(
[['role' => 'user', 'content' => '你好,请给我春江花月夜的原文']],
function (array $chunk) use (&$thinking, &$answer, &$phase) {
$choice = $chunk['choices'][0] ?? [];
$delta = $choice['delta'] ?? [];
// 深度思考内容
$reasoning = $delta['reasoning_content'] ?? '';
if ($reasoning !== '') {
$thinking .= $reasoning;
echo '[思考] ' . $reasoning;
flush();
return true;
}
// 正文内容
$content = $delta['content'] ?? '';
if ($content !== '') {
if ($phase === 'thinking') {
$phase = 'answer';
echo PHP_EOL . '-------- 思考结束,正式回答 --------' . PHP_EOL;
}
$answer .= $content;
echo $content;
flush();
return true;
}
// 结束标记
if (($choice['finish_reason'] ?? null) !== null) {
return false; // 提前中止流
}
return true;
},
);
完整可运行示例见 [examples/text.php](examples/text.php)。
## 配置项
`Config` 构造函数全部参数(均可在初始化时指定):
| 参数 | 类型 | 必填/选填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `apiKey` | `string` | 必填 | 无 | API Key;本地服务可传空字符串 |
| `model` | `string` | 必填 | 无 | 默认模型,单个请求可覆盖 |
| `baseUrl` | `string` | 必填 | 无 | OpenAI 兼容接口地址 |
| `timeout` | `int` | 选填 | `60` | 请求总超时(秒) |
| `connectTimeout` | `?int` | 选填 | `null` | 连接超时(秒),null 使用 libcurl 默认 |
| `verifySsl` | `bool` | 选填 | `true` | 是否校验 SSL 证书 |
| `organization` | `?string` | 选填 | `null` | OpenAI Organization(可选) |
| `headers` | `array` | 选填 | `[]` | 额外请求头;`Authorization` 可覆盖默认 Bearer |
| `defaultOptions` | `array` | 选填 | `[]` | 默认合并到 chat/embeddings 请求体的参数(优先级最低,可被单次 `$options` 覆盖),如 `['temperature' => 0.7]` |
## 更多接口
// 每个请求可覆盖模型与传入任意参数 $client->chat($messages, [
'model' => 'gpt-4o',
'temperature' => 0.5,
'max_tokens' => 2048,
'tools' => [...], // function calling
]);
// 向量化 $client->embeddings('你好,世界'); $client->embeddings(['文本1', '文本2'], ['model' => 'text-embedding-3-small']);
// 模型列表 $client->models();
// 通用请求:images / audio / moderation 等任意 OpenAI 兼容接口 $client->request('POST', '/images/generations', [
'prompt' => '一只戴帽子的猫',
'n' => 1,
]);
## 错误处理
接口返回非 2xx 状态时抛出 `MhCode\Ai\Exception\ApiException`:
use MhCode\Ai\Exception\ApiException;
try {
$client->chatText([['role' => 'user', 'content' => 'hi']]);
} catch (ApiException $e) {
echo $e->getStatusCode(); // 429
echo $e->getMessage(); // 错误信息
var_dump($e->getErrorBody()); // 完整响应体
}
## 支持的服务示例
只需更换 `baseUrl`(部分服务路径需带 `/v1`):
| 服务 | baseUrl |
| --- | --- |
| OpenAI | `https://api.openai.com/v1` |
| DeepSeek | `https://api.deepseek.com/v1` |
| Moonshot / Kimi | `https://api.moonshot.cn/v1` |
| 通义千问(兼容模式) | `https://dashscope.aliyuncs.com/compatible-mode/v1` |
| 智谱 GLM | `https://open.bigmodel.cn/api/paas/v4` |
| Groq | `https://api.groq.com/openai/v1` |
| Ollama(本地) | `http://localhost:11434/v1` |
| LM Studio(本地) | `http://localhost:1234/v1` |
## 目录结构
src/ ├── Client.php # 主客户端 ├── Config.php # 初始化配置 └── Exception/
└── ApiException.php # 接口异常
## License
[MIT](LICENSE)