English | 简体中文
Non-blocking OpenAI client for PHP with coroutine support and a built-in connection pool, designed for Workerman / webman.
composer require webman/openaiRequires PHP 8.1+ and a Workerman 5.1+ runtime.
Enable coroutines in webman
Setwebman.eventLoopinconfig/process.phptoWorkerman\Events\Fiber::class.
If theswooleorswowextension is installed, you may useWorkerman\Events\Swoole::classorWorkerman\Events\Swow::classinstead.
<?phprequire_once__DIR__ . '/vendor/autoload.php';
useWebman\Openai\Chat;
useWorkerman\Connection\TcpConnection;
useWorkerman\Protocols\Http\Request;
useWorkerman\Protocols\Http\Response;
useWorkerman\Events\Fiber;
useWorkerman\Worker;
$worker = newWorker('http://0.0.0.0:8686');
$worker->eventLoop = Fiber::class;
$worker->onMessage = function (TcpConnection$connection, Request$request) {
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$result = $chat->completions([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'hello']],
]);
$connection->send(newResponse(200, [
'Content-Type' => 'application/json; charset=utf-8',
], json_encode($result, JSON_UNESCAPED_UNICODE)));
};
Worker::runAll();<?phprequire_once__DIR__ . '/vendor/autoload.php';
useWebman\Openai\Chat;
useWorkerman\Connection\TcpConnection;
useWorkerman\Protocols\Http\Chunk;
useWorkerman\Protocols\Http\Request;
useWorkerman\Protocols\Http\Response;
useWorkerman\Events\Fiber;
useWorkerman\Worker;
$worker = newWorker('http://0.0.0.0:8686');
$worker->eventLoop = Fiber::class;
$worker->onMessage = function (TcpConnection$connection, Request$request) {
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$chunks = $chat->completions([
'model' => 'gpt-4o-mini',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
]);
$connection->send(newResponse(200, [
'Transfer-Encoding' => 'chunked',
'Content-Type' => 'application/x-ndjson; charset=utf-8',
]));
foreach ($chunksas$chunk) {
$connection->send(newChunk(json_encode($chunk, JSON_UNESCAPED_UNICODE) . "\n"));
}
$connection->close(newChunk(''));
};
Worker::runAll();Note
API usage is the same whether you run under Workerman or webman; the examples below use webman.
<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$result = $chat->completions([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'hello']],
]);
returnjson($result);
}
}Under coroutines, streaming returns a generator: send response headers to the client first, then emit each piece with Chunk, and finish with close.
<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
useWorkerman\Protocols\Http\Chunk;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$chunks = $chat->completions([
'model' => 'gpt-4o-mini',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
]);
$connection = $request->connection;
$connection->send(response()->withHeaders([
'Transfer-Encoding' => 'chunked',
'Content-Type' => 'application/x-ndjson; charset=utf-8',
]));
foreach ($chunksas$chunk) {
$connection->send(newChunk(json_encode($chunk, JSON_UNESCAPED_UNICODE) . "\n"));
}
$connection->close(newChunk(''));
// Stream finished manually; no return needed
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$connection = $request->connection;
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$chat->completions(
[
'model' => 'gpt-4o-mini',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
],
[
'stream' => function (array$chunk) use ($connection) {
$connection->send(newChunk(json_encode($chunk, JSON_UNESCAPED_UNICODE) . "\n"));
},
'complete' => function (?array$result, ?OpenAIException$e, ?Response$response) use ($connection) {
$connection->send(newChunk(''));
},
]
);
returnresponse()->withHeaders([
'Transfer-Encoding' => 'chunked',
'Content-Type' => 'application/x-ndjson; charset=utf-8',
]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$connection = $request->connection;
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$chat->completions(
[
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'hello']],
],
[
'complete' => function (?array$result, ?OpenAIException$e, ?Response$response) use ($connection) {
$connection->send(newChunk(json_encode($result, JSON_UNESCAPED_UNICODE)));
$connection->send(newChunk(''));
},
]
);
returnresponse()->withHeaders([
'Transfer-Encoding' => 'chunked',
'Content-Type' => 'application/json; charset=utf-8',
]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
class ChatController
{
privatefunctionrunTool(string$name, array$args): string
{
if ($name === 'get_weather') {
$city = $args['city'] ?? '';
returnjson_encode([
'city' => $city,
'summary' => 'Clear',
'temp_c' => 22,
], JSON_UNESCAPED_UNICODE);
}
returnjson_encode(['error' => 'unknown tool'], JSON_UNESCAPED_UNICODE);
}
publicfunctioncompletions(Request$request)
{
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$tools = [
[
'type' => 'function',
'function' => [
'name' => 'get_weather',
'description' => 'Get the current weather for a given city',
'parameters' => [
'type' => 'object',
'properties' => [
'city' => ['type' => 'string'],
],
'required' => ['city'],
],
],
],
];
$messages = [
['role' => 'user', 'content' => 'What is the weather in Hangzhou? Use the tool first, then answer.'],
];
$first = $chat->completions([
'model' => 'gpt-4o-mini',
'messages' => $messages,
'tools' => $tools,
'tool_choice' => 'auto',
]);
$choice = $first['choices'][0] ?? null;
$assistantMsg = $choice['message'] ?? null;
if (
$assistantMsg
&& ($choice['finish_reason'] ?? '') === 'tool_calls'
&& !empty($assistantMsg['tool_calls'])
) {
$messages[] = $assistantMsg;
foreach ($assistantMsg['tool_calls'] as$tc) {
$fn = $tc['function']['name'] ?? '';
$args = json_decode($tc['function']['arguments'] ?? '{}', true) ?: [];
$messages[] = [
'role' => 'tool',
'tool_call_id' => $tc['id'],
'content' => $this->runTool($fn, $args),
];
}
$second = $chat->completions([
'model' => 'gpt-4o-mini',
'messages' => $messages,
'tools' => $tools,
]);
returnjson([
'first' => $first,
'second' => $second,
]);
}
returnjson($first);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Image;
class ImageController
{
publicfunctiongenerations(Request$request)
{
$image = newImage([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$result = $image->generations([
'model' => 'dall-e-3',
'prompt' => 'a dog',
'n' => 1,
'size' => '1024x1024',
]);
returnjson($result);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Image;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class ImageController
{
publicfunctiongenerations(Request$request)
{
$connection = $request->connection;
$image = newImage([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$image->generations(
[
'model' => 'dall-e-3',
'prompt' => 'a dog',
'n' => 1,
'size' => '1024x1024',
],
[
'complete' => function (?array$result, ?OpenAIException$e, ?Response$response) use ($connection) {
$connection->send(newChunk(json_encode($result, JSON_UNESCAPED_UNICODE)));
$connection->send(newChunk(''));
},
]
);
returnresponse()->withHeaders([
'Content-Type' => 'application/json; charset=utf-8',
'Transfer-Encoding' => 'chunked',
]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Embedding;
class EmbeddingController
{
publicfunctioncreate(Request$request)
{
$embedding = newEmbedding([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$result = $embedding->create([
'model' => 'text-embedding-3-small',
'input' => 'Some words',
'encoding_format' => 'float',
]);
returnjson($result);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Embedding;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class EmbeddingController
{
publicfunctioncreate(Request$request)
{
$connection = $request->connection;
$embedding = newEmbedding([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$embedding->create(
[
'model' => 'text-embedding-3-small',
'input' => 'Some words',
'encoding_format' => 'float',
],
[
'complete' => function (?array$result, ?OpenAIException$e, ?Response$response) use ($connection) {
$connection->send(newChunk(json_encode($result, JSON_UNESCAPED_UNICODE)));
$connection->send(newChunk(''));
},
]
);
returnresponse()->withHeaders([
'Content-Type' => 'application/json; charset=utf-8',
'Transfer-Encoding' => 'chunked',
]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
class AudioController
{
publicfunctionspeech(Request$request)
{
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$binary = $audio->speech([
'model' => 'gpt-4o-mini-tts',
'input' => 'Hello, how can I help you?',
'voice' => 'alloy',
]);
returnresponse($binary)->withHeaders([
'Content-Type' => 'audio/mpeg',
]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
useWorkerman\Protocols\Http\Chunk;
class AudioController
{
publicfunctionspeechStream(Request$request)
{
$connection = $request->connection;
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$chunks = $audio->speech([
'model' => 'gpt-4o-mini-tts',
'input' => 'Hello, how can I help you?',
'voice' => 'alloy',
'stream' => true,
]);
$connection->send(response()->withHeaders([
'Content-Type' => 'audio/mpeg',
'Transfer-Encoding' => 'chunked',
]));
foreach ($chunksas$buffer) {
$connection->send(newChunk((string) $buffer));
}
$connection->close(newChunk(''));
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class AudioController
{
publicfunctionspeech(Request$request)
{
$connection = $request->connection;
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$audio->speech(
[
'model' => 'gpt-4o-mini-tts',
'input' => 'Hello, how can I help you?',
'voice' => 'alloy',
],
[
'stream' => function (string$buffer) use ($connection) {
$connection->send(newChunk($buffer));
},
'complete' => function (?string$result, ?OpenAIException$e, ?Response$response) use ($connection) {
$connection->send(newChunk(''));
},
]
);
returnresponse()->withHeaders([
'Content-Type' => 'audio/mpeg',
'Transfer-Encoding' => 'chunked',
]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
class AudioController
{
publicfunctiontranscribe(Request$request)
{
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$result = $audio->transcriptions([
'model' => 'gpt-4o-mini-transcribe',
'file' => '/path/to/audio.mp3',
// 'response_format' => 'json', // default JSON; use 'text' for plain-text responses, etc.
]);
// Usually ['text' => '...', ...] for JSON; string when the response body is plain textreturnjson($result);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
class AudioController
{
publicfunctiontranscribeStream(Request$request)
{
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$events = $audio->transcriptions([
'model' => 'gpt-4o-mini-transcribe',
'file' => [
'contents' => (string) file_get_contents('/path/to/audio.mp3'),
'filename' => 'clip.mp3',
'mime' => 'audio/mpeg',
],
'stream' => true,
]);
$lines = [];
foreach ($eventsas$ev) {
$lines[] = $ev;
}
returnjson(['events' => $lines]);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
class AudioController
{
publicfunctiontranscribeAsync(Request$request)
{
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$audio->transcriptions(
[
'model' => 'gpt-4o-mini-transcribe',
'file' => '/path/to/audio.mp3',
],
[
'complete' => function (array|string|null$result, ?OpenAIException$e, ?Response$response) {
// On success: $result is array|string; on failure: $e is non-null
},
]
);
returnjson(['ok' => true, 'note' => 'Handle the result inside the complete callback']);
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Audio;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
class AudioController
{
publicfunctiontranscribeStreamAsync(Request$request)
{
$audio = newAudio([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$audio->transcriptions(
[
'model' => 'gpt-4o-mini-transcribe',
'file' => '/path/to/audio.mp3',
'stream' => true,
],
[
'stream' => function (array$event) {
// e.g. transcript.text.delta / transcript.text.done
},
'complete' => function (array|string|null$result, ?OpenAIException$e, ?Response$response) {
// If stream is set above, $result is often null on success.// If only complete + $data['stream'], $result is an aggregated array (includes text, etc.).
},
]
);
returnjson(['ok' => true]);
}
}$chat = newChat([
'api' => 'https://YOUR_RESOURCE.openai.azure.com',
'apikey' => getenv('AZURE_OPENAI_KEY') ?: 'xxx',
'isAzure' => true,
// optional: 'azureApiVersion' => '2023-05-15',
]);
$chunks = $chat->completions([
'model' => 'YOUR_DEPLOYMENT_NAME',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
]);$chat = newChat([
'api' => 'https://YOUR_RESOURCE.openai.azure.com',
'apikey' => getenv('AZURE_OPENAI_KEY') ?: 'xxx',
'isAzure' => true,
]);Documentation: https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope
When Chat is configured with an api URL that includes a path, if the path ends with a trailing slash (/), chat/completions is appended automatically (same pattern as the OpenAI-compatible path). For DashScope compatibility mode, use a base URL ending with /v1/:
$chat = newChat([
'api' => 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
'apikey' => getenv('DASHSCOPE_API_KEY') ?: 'xxx',
]);
$chunks = $chat->completions([
'model' => 'qwen-turbo',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
]);$chat = newChat([
'api' => 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
'apikey' => getenv('DASHSCOPE_API_KEY') ?: 'xxx',
]);The second argument $options on each API method may include:
timeout: timeout in seconds (defaults differ slightly per API; see the Client constructor in the source).headers: extra HTTP headers (merged with the defaults).
[$result, $response] = $chat->completions([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'hello']],
], ['with_response' => true]);
echo$response->getHeaderLine('x-request-id');Note
Other endpoints also supportwith_responseto obtain theResponseobject; usage is the same.
[$chunks, $response] = $chat->completions([
'model' => 'gpt-4o-mini',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
], ['with_response' => true]);
echo$response->getHeaderLine('x-request-id');
foreach ($chunksas$chunk) {
echo$chunk['choices'][0]['delta']['content'] ?? '';
}Error handling is unified across coroutine and async styles via the same Webman\Openai\OpenAIException class.
<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
try {
$result = $chat->completions([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'hello']],
]);
returnjson($result);
} catch (OpenAIException$e) {
returnjson([
'ok' => false,
'message' => $e->getMessage(),
'http_status' => $e->statusCode,
'error_code' => $e->errorCode,
'error_type' => $e->errorType,
'error_param' => $e->errorParam,
'raw' => $e->raw,
], $e->statusCode >= 400 && $e->statusCode < 600 ? $e->statusCode : 500);
}
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$connection = $request->connection;
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
try {
$chunks = $chat->completions([
'model' => 'gpt-4o-mini',
'stream' => true,
'messages' => [['role' => 'user', 'content' => 'hello']],
]);
} catch (OpenAIException$e) {
returnjson([
'ok' => false,
'message' => $e->getMessage(),
'http_status' => $e->statusCode,
], 500);
}
$connection->send(response()->withHeaders([
'Transfer-Encoding' => 'chunked',
'Content-Type' => 'application/x-ndjson; charset=utf-8',
]));
try {
foreach ($chunksas$chunk) {
$connection->send(newChunk(json_encode($chunk, JSON_UNESCAPED_UNICODE) . "\n"));
}
} catch (OpenAIException$e) {
$connection->send(newChunk(json_encode([
'_stream_error' => true,
'message' => $e->getMessage(),
'http_status' => $e->statusCode,
], JSON_UNESCAPED_UNICODE) . "\n"));
}
$connection->close(newChunk(''));
}
}<?phpnamespaceapp\controller;
usesupport\Request;
useWebman\Openai\Chat;
useWebman\Openai\OpenAIException;
useWorkerman\Http\Response;
useWorkerman\Protocols\Http\Chunk;
class ChatController
{
publicfunctioncompletions(Request$request)
{
$connection = $request->connection;
$chat = newChat([
'apikey' => getenv('OPENAI_API_KEY') ?: 'sk-xxx',
'api' => 'https://api.openai.com',
]);
$chat->completions(
[
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'hello']],
],
[
'complete' => function (?array$result, ?OpenAIException$e, ?Response$response) use ($connection) {
if ($e !== null) {
$payload = [
'ok' => false,
'message' => $e->getMessage(),
'http_status' => $e->statusCode,
'error_code' => $e->errorCode,
'error_type' => $e->errorType,
'error_param' => $e->errorParam,
];
} else {
$payload = ['ok' => true, 'data' => $result];
}
$connection->send(newChunk(json_encode($payload, JSON_UNESCAPED_UNICODE)));
$connection->send(newChunk(''));
},
]
);
returnresponse()->withHeaders([
'Transfer-Encoding' => 'chunked',
'Content-Type' => 'application/json; charset=utf-8',
]);
}
}