Tokens OpenAI Chat Completions protocol বোঝে, তাই যে PHP code HTTP request পাঠাতে পারে, সেটা দিয়েই Tokens চলে। এই পেজে তিনটা উপায় আছে: সাধারণ PHP-তে community package openai-php/client, Laravel app-এর ভেতরে openai-php/laravel, আর কোনো package ছাড়া raw cURL। Laravel-এর নিজের agent package নিয়ে জানতে দেখুন Laravel AI SDK।
কীসের সঙ্গে মিলিয়ে দেখা হয়েছে
openai-php/client আর openai-php/laravel, দুটোর version 0.21.0 (released 17 September 2026, checked October 2026)-এর README ও source, আর PHP ও Guzzle-এর documentation দেখে এই পেজ লেখা। Code-গুলো documentation আর source-এর সঙ্গে মিলিয়ে দেখা হয়েছে, Tokens-এর বিরুদ্ধে শুরু থেকে শেষ পর্যন্ত চালিয়ে দেখা হয়নি। এই package-গুলো community চালায়, OpenAI বা Tokens publish করে না।
যা লাগবে#
- PHP 8.2 বা তার পরের version (দুটো package-ই
php ^8.2চায়) আর Composer। - API keys থেকে নেওয়া একটা Tokens key,
TOKENS_API_KEYনামে export করা। - /models থেকে একটা model id।
সব উদাহরণে base URL হলো https://tokens.bd/v1। এতে /v1 আছে। openai-php/client-এ এটা withBaseUri()-তে হুবহু এভাবেই দিন: library নিজে একটা / আর তার পরে resource path জুড়ে নেয়, ফলে request যায় https://tokens.bd/v1/chat/completions-এ। শেষে নিজে slash বা /chat/completions বসাবেন না। পুরো URL https:// সমেত লিখুন। scheme বাদ দিলে library নিজে https:// বসিয়ে দেয়।
openai-php/client#
Install#
composer require openai-php/client guzzlehttp/guzzle
export TOKENS_API_KEY="tok_live_your_key"Package-টার একটা PSR-18 HTTP client লাগে। README বলছে, হয় php-http/discovery Composer plugin-কে অনুমতি দিন, নয়তো Guzzle-এর মতো একটা client নিজে install করুন। উপরের মতো Guzzle install করাই সবচেয়ে সোজা।
Client বানান আর call করুন#
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$client = OpenAI::factory()
->withApiKey((string) getenv('TOKENS_API_KEY'))
->withBaseUri('https://tokens.bd/v1')
->withHttpClient(new GuzzleHttp\Client([
'connect_timeout' => 10,
'timeout' => 300,
]))
->make();
$response = $client->chat()->create([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [
['role' => 'system', 'content' => 'Answer in one short paragraph.'],
['role' => 'user', 'content' => 'When should I use a queue instead of running code in the request?'],
],
'max_tokens' => 400,
]);
echo $response->choices[0]->message->content, PHP_EOL;
echo "{$response->usage->promptTokens} in, {$response->usage->completionTokens} out", PHP_EOL;php hello.phpOpenAI::factory(), withApiKey(), withBaseUri(), withHttpClient() আর make() README-তে দেওয়া factory method। আলাদা কোনো header লাগলে withHttpHeader() প্রতিটা request-এ সেটা জুড়ে দেয়। Tokens-এর জন্য OpenAI::client($key) ব্যবহার করবেন না: এতে base URI দেওয়ার জায়গাই নেই, ফলে আপনার key OpenAI-র কাছে চলে যাবে।
Model বেছে নিন#
model-এ Tokens-এর id দিন, /models-এ যেভাবে লেখা আছে ঠিক সেভাবে, যেমন deepseek/deepseek-v4.1-flash। Code থেকে id যাচাই করতে চাইলে:
foreach ($client->models()->list()->data as $model) {
echo $model->id, PHP_EOL;
}id ভুল হলে 404 model_not_found আসে।
Response stream করুন#
createStreamed() call করে result-এর ওপর loop চালান। Usage চেয়ে নিতে ভুলবেন না, নইলে stream-এ token-এর হিসেব আসে না:
$stream = $client->chat()->createStreamed([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [
['role' => 'user', 'content' => 'Write a haiku about merge conflicts.'],
],
'stream_options' => ['include_usage' => true],
]);
foreach ($stream as $chunk) {
$text = $chunk->choices[0]->delta->content ?? null;
if ($text !== null) {
echo $text;
flush();
}
if ($chunk->usage !== null) {
echo PHP_EOL, "{$chunk->usage->promptTokens} in, {$chunk->usage->completionTokens} out", PHP_EOL;
}
}include_usage চালু থাকলে শেষ chunk-এ choices list ফাঁকা থাকে আর শুধু usage থাকে। ?? null সেই কেসটাই সামলায়। বাকি সব chunk-এ usage হলো null। আরও দেখুন Streaming।
Stream চলার মাঝে key বা কোনো limit-এ সমস্যা হলে library foreach-এর ভেতর থেকেই OpenAI\Exceptions\ErrorException throw করে। তাই loop-টা আপনার try block-এর ভেতরে রাখুন।
Tool call#
যে model tool calling সাপোর্ট করে, শুধু সেখানেই এটা চলে। আগে /models-এ model-এর পেজ দেখে নিন।
$tools = [[
'type' => 'function',
'function' => [
'name' => 'get_weather',
'description' => 'Current weather for a city',
'parameters' => [
'type' => 'object',
'properties' => ['city' => ['type' => 'string']],
'required' => ['city'],
],
],
]];
$messages = [['role' => 'user', 'content' => 'Is it raining in Dhaka?']];
$first = $client->chat()->create([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => $messages,
'tools' => $tools,
]);
$message = $first->choices[0]->message;
if ($message->toolCalls !== []) {
$messages[] = [
'role' => 'assistant',
'content' => $message->content,
'tool_calls' => array_map(fn ($call) => [
'id' => $call->id,
'type' => 'function',
'function' => ['name' => $call->function->name, 'arguments' => $call->function->arguments],
], $message->toolCalls),
];
foreach ($message->toolCalls as $call) {
$args = json_decode($call->function->arguments, true);
$result = ['city' => $args['city'], 'condition' => 'light rain', 'temp_c' => 29]; // your real lookup here
$messages[] = [
'role' => 'tool',
'tool_call_id' => $call->id,
'content' => json_encode($result),
];
}
$final = $client->chat()->create([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => $messages,
'tools' => $tools,
]);
echo $final->choices[0]->message->content, PHP_EOL;
}Request আর response-এর গড়ন আছে Tool calling পেজে।
লম্বা request-এর timeout#
README বলছে, default timeout নির্ভর করে আপনি কোন HTTP client ব্যবহার করছেন তার ওপর। বাড়াতে হলে একটা configure করা client withHttpClient()-এ দিন। Guzzle-এর ক্ষেত্রে:
| Option | মানে (Guzzle docs অনুযায়ী) |
|---|---|
connect_timeout | Connect করার জন্য কত সেকেন্ড অপেক্ষা করবে। Default 0, মানে অনন্তকাল অপেক্ষা। |
timeout | পুরো request-এর মোট সময়, সেকেন্ডে। Default 0, মানে অনন্তকাল অপেক্ষা। |
read_timeout | Stream করা body-র প্রতিটা read-এর timeout। Default হলো default_socket_timeout ini setting-এর মান। |
Non-streamed সবচেয়ে লম্বা উত্তরের চেয়ে timeout বেশি রাখুন, প্রথম উদাহরণে যেমন আছে। Stream-এর বেলায় মোট timeout আপনার stream পড়ার সময়টাও গোনে, তাই লম্বা উত্তর মাঝপথে কেটে যেতে পারে। Stream-এর জন্য এমন client নিন যাতে মোট সময়ের সীমা নেই, আর ভরসা read_timeout-এর ওপর:
$streamClient = OpenAI::factory()
->withApiKey((string) getenv('TOKENS_API_KEY'))
->withBaseUri('https://tokens.bd/v1')
->withHttpClient(new GuzzleHttp\Client([
'connect_timeout' => 10,
'timeout' => 0,
'read_timeout' => 120,
]))
->make();Tokens-এর দিকে, gateway upstream থেকে response header আসার জন্য সর্বোচ্চ 600 সেকেন্ড অপেক্ষা করে। Streaming করলে এত সময় চুপচাপ একটা connection ধরে বসে থাকতে হয় না।
আপনার client-এর আগেই PHP নিজেও লম্বা request থামিয়ে দিতে পারে। php.ini-র web server setting max_execution_time web request-এর জন্য default 30 সেকেন্ড (command line-এ কোনো সীমা নেই)। Web app-এ লম্বা generation হলে উত্তর stream করুন, নয়তো call-টা একটা queue job-এ চালান।
Error সামলান#
Library Tokens-এর OpenAI-ধাঁচের error body পড়ে নেয়, তাই ErrorException-এ Tokens-এর code পাওয়া যায়:
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\TransporterException;
try {
$client->chat()->create([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [['role' => 'user', 'content' => 'hi']],
]);
} catch (ErrorException $e) {
$requestId = $e->response->getHeaderLine('x-tokens-request-id');
error_log(sprintf('%d %s %s request=%s', $e->getStatusCode(), $e->getErrorCode(), $e->getErrorMessage(), $requestId));
if ($e->getErrorCode() === 'insufficient_credits') {
// top up at https://tokens.bd/dashboard/billing
}
} catch (TransporterException $e) {
error_log('Network problem, timeout or server error: ' . $e->getMessage());
}Library-র source থেকে, HTTP client Guzzle হলে: JSON error body সহ যেকোনো 4xx response (401, 402, 403, 404 আর 429-ও) ErrorException হয়ে আসে। Network failure, timeout আর 5xx response আসে TransporterException হয়ে, আর Guzzle-এর exception থাকে $e->getPrevious()-এ, response এসে থাকলে তাতে সেটাও থাকে। যেসব HTTP client error status-এ exception ছোড়ে না, তাদের জন্য library RateLimitException আর ServerException-ও বানিয়ে রেখেছে, দুটোতেই একটা public $response আছে।
প্রতিটা failure-এর সঙ্গে x-tokens-request-id log করুন, তাহলে support আপনার request খুঁজে বের করতে পারবে। সচরাচর যেসব code পাবেন: invalid_api_key (401), model_not_allowed_on_key আর tier_permission_denied (403), insufficient_credits (402), আর rate_limited, concurrency_limit ও window_exhausted (429)। পুরো তালিকা Errors পেজে।
openai-php/laravel#
Laravel package-টা একই client-কে একটা service provider আর OpenAI facade-এর ভেতরে মুড়ে দেয়। এটা চায় PHP 8.2+ আর Laravel ^11.29, ^12.12 বা ^13.0।
Install#
composer require openai-php/laravel
php artisan openai:installদ্বিতীয় command config/openai.php বানায় আর .env-এর শেষে ফাঁকা OPENAI_API_KEY ও OPENAI_ORGANIZATION লাইন জুড়ে দেয়। Tokens-এ organization লাগে না, তাই OPENAI_ORGANIZATION লাইনটা মুছে দিন।
Configure#
README-তে এই variable-গুলো আছে:
OPENAI_API_KEY=tok_live_your_key
OPENAI_BASE_URL=https://tokens.bd/v1
OPENAI_REQUEST_TIMEOUT=300| Variable | Config key | নোট |
|---|---|---|
OPENAI_API_KEY | api_key | আপনার Tokens key। |
OPENAI_BASE_URL | base_uri | Default api.openai.com/v1। https://tokens.bd/v1 দিন, /v1 সমেত। |
OPENAI_REQUEST_TIMEOUT | request_timeout | সেকেন্ডে, default 30। লম্বা উত্তরের জন্য বাড়ান। |
অন্য কিছু OPENAI_API_KEY পড়লে Tokens-এর আলাদা নাম নিন
OPENAI_API_KEY নামটা অনেক package-ই পড়ে। যেমন laravel/ai-এর built-in openai provider এটা পড়ে আর default-এ OpenAI-র কাছে পাঠায়। ওখানে Tokens key বসালে অন্য কোনো package সেটা ভুল জায়গায় পাঠিয়ে দিতে পারে। নিরাপদ উপায়: config/openai.php বদলে নিজের নামগুলো পড়ান, আর OPENAI_* unset রাখুন।
return [
'api_key' => env('TOKENS_API_KEY'),
'base_uri' => env('TOKENS_BASE_URL', 'https://tokens.bd/v1'),
'request_timeout' => env('TOKENS_REQUEST_TIMEOUT', 300),
];Publish করা file-এ organization আর project-ও আছে। ও দুটো বাদ দিন, নয়তো null রাখুন। যে server config cache করে, সেখানে .env বা config বদলানোর পর php artisan config:clear চালান।
Call করুন#
use OpenAI\Laravel\Facades\OpenAI;
$response = OpenAI::chat()->create([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [
['role' => 'user', 'content' => 'Explain job batching in Laravel in two sentences.'],
],
]);
echo $response->choices[0]->message->content;Facade-এ ওপরের client-এর মতোই chat(), models() আর বাকি resource আছে, তাই streaming, tool call আর error handling আগের section-এর মতোই। README-র নিজের উদাহরণে OpenAI::responses() আছে। Tokens /v1/responses-ও চালায় (Responses), কিন্তু এই পেজে যাচাই করা হয়েছে উপরের Chat Completions রূপটাই।
Route থেকে stream করুন#
use Illuminate\Support\Facades\Route;
use OpenAI\Laravel\Facades\OpenAI;
Route::get('/ask', function () {
return response()->stream(function () {
$stream = OpenAI::chat()->createStreamed([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [['role' => 'user', 'content' => 'Explain queues in Laravel.']],
]);
foreach ($stream as $chunk) {
$text = $chunk->choices[0]->delta->content ?? null;
if ($text !== null) {
echo $text;
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
}
}, 200, ['Content-Type' => 'text/plain; charset=utf-8', 'Cache-Control' => 'no-cache']);
});Laravel-এ timeout#
Package নিজের Guzzle client বানায় request_timeout-কে Guzzle-এর timeout option হিসেবে দিয়ে। এটা মোট সময়ের সীমা, তাই stream করা উত্তরও এর মধ্যে পড়ে। আপনার সবচেয়ে লম্বা উত্তরের চেয়ে এটা বেশি রাখুন। যদি Guzzle-এর read_timeout দরকার হয়, তাহলে factory দিয়ে client বানান (আগের section) আর নিজের service provider-এ package-টার client-এর জায়গায় সেটা register করুন।
Raw cURL#
কোনো Composer package লাগে না। এটা PHP-র curl extension ব্যবহার করে।
<?php
declare(strict_types=1);
$ch = curl_init('https://tokens.bd/v1/chat/completions');
$requestId = '';
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('TOKENS_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [['role' => 'user', 'content' => 'Say hello in five words.']],
'max_tokens' => 100,
], JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 300,
CURLOPT_HEADERFUNCTION => function ($ch, string $header) use (&$requestId): int {
if (stripos($header, 'x-tokens-request-id:') === 0) {
$requestId = trim(substr($header, strlen('x-tokens-request-id:')));
}
return strlen($header);
},
]);
$body = curl_exec($ch);
if ($body === false) {
fwrite(STDERR, 'cURL error ' . curl_errno($ch) . ': ' . curl_error($ch) . PHP_EOL);
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
if ($status >= 400) {
$error = $data['error'] ?? [];
fwrite(STDERR, sprintf("%d %s %s request=%s\n", $status, $error['code'] ?? '', $error['message'] ?? '', $requestId));
exit(1);
}
echo $data['choices'][0]['message']['content'], PHP_EOL;CURLOPT_TIMEOUT হলো request-এর জন্য মোট অনুমোদিত সময়, আর 0 (default) মানে কোনো সীমা নেই। CURLOPT_CONNECTTIMEOUT শুধু connection করার সময়টা ধরে।
Stream করতে হলে "stream": true পাঠান আর CURLOPT_WRITEFUNCTION দিয়ে body আসার সঙ্গে সঙ্গে পড়ুন। Body-টা data: {...} লাইনের একটা সারি, শেষে থাকে data: [DONE]:
<?php
declare(strict_types=1);
$buffer = '';
$errorBody = '';
$ch = curl_init('https://tokens.bd/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('TOKENS_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'model' => 'deepseek/deepseek-v4.1-flash',
'messages' => [['role' => 'user', 'content' => 'Write a haiku about merge conflicts.']],
'stream' => true,
'stream_options' => ['include_usage' => true],
], JSON_THROW_ON_ERROR),
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 0, // no total limit; streams can be long
CURLOPT_WRITEFUNCTION => function ($ch, string $data) use (&$buffer, &$errorBody): int {
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
$errorBody .= $data; // an error comes back as plain JSON, not as a stream
return strlen($data);
}
$buffer .= $data;
while (($pos = strpos($buffer, "\n")) !== false) {
$line = trim(substr($buffer, 0, $pos));
$buffer = substr($buffer, $pos + 1);
if (!str_starts_with($line, 'data:')) {
continue;
}
$payload = trim(substr($line, 5));
if ($payload === '[DONE]') {
continue;
}
$chunk = json_decode($payload, true);
$text = $chunk['choices'][0]['delta']['content'] ?? '';
if ($text !== '') {
echo $text;
flush();
}
}
return strlen($data);
},
]);
$ok = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($ok === false) {
fwrite(STDERR, 'cURL error ' . curl_errno($ch) . ': ' . curl_error($ch) . PHP_EOL);
exit(1);
}
if ($status >= 400) {
fwrite(STDERR, "HTTP $status: $errorBody" . PHP_EOL);
exit(1);
}
curl_close($ch);
echo PHP_EOL;Write function-কে যত byte পেয়েছে ঠিক তত return করতে হবে, নইলে cURL transfer বাতিল করে দেয়। choices[0] পড়তে ?? লাগে, কারণ include_usage চালু থাকলে শেষ chunk-এ কোনো choice থাকে না।
ঠিকঠাক চলছে কি না দেখুন#
ওপরের যেকোনো program চালান। ছোট একটা উত্তর print হলে বুঝবেন key, URL আর model id তিনটাই ঠিক আছে। PHP ছাড়াই দেখতে চাইলে:
curl -s https://tokens.bd/v1/models -H "Authorization: Bearer $TOKENS_API_KEY"সীমাবদ্ধতা#
openai-php/clientশুধু OpenAI API-র মডেল জানে। Tokens নিজে যা যোগ করেছে (যেমন Models and usage-এ বলা usage endpoint), তার জন্য package-এ কোনো method নেই। সেটা cURL বা Guzzle দিয়ে call করুন।- Tokens প্রতি অ্যাকাউন্টে মিনিটপ্রতি request আর একসাথে চলা request সীমিত রাখে। একসাথে অনেক job চালানো queue worker pool
429 concurrency_limitখেতে পারে। দেখুন Rate limits। - Browser-এর JavaScript থেকে Tokens call করবেন না। Tokens কোনো CORS header পাঠায় না, আর key-ও সবার চোখে পড়ে যাবে। Call-টা PHP থেকে করে result ফেরত দিন।
- PHP-FPM আর web server output buffer করে রাখে। Browser-এ stream করতে
ob_flush(),flush()লাগতে পারে, আর nginx-এ ওই route-এর response buffering বন্ধ করতে হতে পারে।
সমস্যা হলে#
| লক্ষণ | কারণ ও সমাধান |
|---|---|
404 unsupported_endpoint | Base URI ভুল। https://tokens.bd/v1 দিন, /v1 সমেত, শেষে /chat/completions ছাড়া। |
404 model_not_found | Model id ভুল। /models থেকে copy করুন। |
401 missing_api_key বা invalid_api_key | Key ফাঁকা ছিল বা ভুল। PHP process-এ variable set না থাকলে getenv() false দেয়। PHP-FPM অনেক সময় shell variable দেখতে পায় না, তাই .env, config file বা clear_env = no ব্যবহার করুন। Laravel-এ php artisan config:clear চালান। |
To use stream requests you must provide an stream handler closure via the OpenAI factory (exception message) | আপনি এমন custom HTTP client দিয়েছেন যা Guzzle বা Symfony নয়। Guzzle নিন, অথবা README-তে যেমন বলা আছে withStreamHandler() জুড়ুন। |
cURL error 28: Operation timed out অথবা অনেকক্ষণ অপেক্ষার পর TransporterException | আপনার নিজের timeout response আসার সময়ের চেয়ে ছোট। timeout বাড়ান, নয়তো stream করুন। |
| Stream-এর মাঝপথে পেজ থেমে যায় | মোট timeout বা max_execution_time request থামিয়ে দিয়েছে। দুটোই বাড়ান। |
402 insufficient_credits | billing-এ টাকা যোগ করুন। |
429 rate_limited বা concurrency_limit | Retry-After-এ যত সেকেন্ড বলা আছে অপেক্ষা করুন, নয়তো একসাথে কম request চালান। window_exhausted মানে plan-এর window শেষ; reset না হওয়া পর্যন্ত retry করে লাভ নেই। |
Tokens-এর সব error code আছে Errors পেজে, আর প্রতিটার সমাধান Troubleshooting-এ।
Warning
Key রাখুন environment variable বা secrets manager-এ, কখনো commit করা file-এ নয়। Leak হলে /dashboard/keys-এ গিয়ে rotate করুন। পুরোনো secret সঙ্গে সঙ্গে অকেজো হয়ে যায়।