Skip to content

Repository files navigation

webman/openai

English | 简体中文

Non-blocking OpenAI client for PHP with coroutine support and a built-in connection pool, designed for Workerman / webman.

Installation

composer require webman/openai

Requires PHP 8.1+ and a Workerman 5.1+ runtime.

Enable coroutines in webman
Set webman.eventLoop in config/process.php to Workerman\Events\Fiber::class.
If the swoole or swow extension is installed, you may use Workerman\Events\Swoole::class or Workerman\Events\Swow::class instead.


Quick overview (Workerman)

Non-streaming

<?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();

Streaming

<?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();

Chat: completions (Webman)

Note
API usage is the same whether you run under Workerman or webman; the examples below use webman.

Coroutines · non-streaming

<?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);
}
}

Coroutines · streaming

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
}
}

Async callbacks · streaming

<?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',
]);
}
}

Async callbacks · non-streaming

<?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',
]);
}
}

Tool / function calling (tools)

<?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);
}
}

Image: generations (Webman)

Coroutines

<?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);
}
}

Async callbacks

<?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',
]);
}
}

Embedding: vectors (Webman)

Coroutines

<?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);
}
}

Async callbacks

<?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',
]);
}
}

Audio: text-to-speech (TTS) (Webman)

Coroutines

<?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',
]);
}
}

Coroutines · streaming

<?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(''));
}
}

Async callbacks · streaming

<?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',
]);
}
}

Audio: speech-to-text (STT / transcriptions) (Webman)

Coroutines · non-streaming

<?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);
}
}

Coroutines · streaming (SSE events)

<?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]);
}
}

Async callbacks · non-streaming

<?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']);
}
}

Async callbacks · streaming

<?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]);
}
}

Gateway compatibility: Azure OpenAI

Coroutines · Chat streaming

$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']],
]);

Async callbacks · Chat streaming

$chat = newChat([
'api' => 'https://YOUR_RESOURCE.openai.azure.com',
'apikey' => getenv('AZURE_OPENAI_KEY') ?: 'xxx',
'isAzure' => true,
]);

Gateway compatibility: Alibaba Cloud DashScope (OpenAI-compatible mode)

Documentation: https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope

Coroutines · Chat streaming

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']],
]);

Async callbacks · Chat streaming

$chat = newChat([
'api' => 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
'apikey' => getenv('DASHSCOPE_API_KEY') ?: 'xxx',
]);

Optional parameters

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).

Response headers: with_response

Non-streaming

[$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 support with_response to obtain the Response object; usage is the same.

Streaming

[$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'] ?? '';
}

Exceptions and errors

Error handling is unified across coroutine and async styles via the same Webman\Openai\OpenAIException class.


Coroutines · non-streaming

<?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);
}
}
}

Coroutines · streaming

<?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(''));
}
}

Async callbacks · non-streaming

<?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',
]);
}
}

About

OpenAI PHP asynchronous client for workerman and webman.

Topics

Resources

Stars

71 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages