Skip to content

PHP

PHP আর Laravel থেকে Tokens ব্যবহার করুন: openai-php/client package, openai-php/laravel package আর সাধারণ cURL। Base URL, প্রথম call, streaming, tool call, timeout আর Tokens-এর error code।

যেসব tool-এ কাজ করেPHP
Markdown-এ দেখুন
এই পাতায়

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#

bash
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 করুন#

hello.php
<?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;
bash
php hello.php

OpenAI::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 যাচাই করতে চাইলে:

php
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-এর হিসেব আসে না:

php
$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-এর পেজ দেখে নিন।

php
$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_timeoutConnect করার জন্য কত সেকেন্ড অপেক্ষা করবে। Default 0, মানে অনন্তকাল অপেক্ষা।
timeoutপুরো request-এর মোট সময়, সেকেন্ডে। Default 0, মানে অনন্তকাল অপেক্ষা।
read_timeoutStream করা body-র প্রতিটা read-এর timeout। Default হলো default_socket_timeout ini setting-এর মান।

Non-streamed সবচেয়ে লম্বা উত্তরের চেয়ে timeout বেশি রাখুন, প্রথম উদাহরণে যেমন আছে। Stream-এর বেলায় মোট timeout আপনার stream পড়ার সময়টাও গোনে, তাই লম্বা উত্তর মাঝপথে কেটে যেতে পারে। Stream-এর জন্য এমন client নিন যাতে মোট সময়ের সীমা নেই, আর ভরসা read_timeout-এর ওপর:

php
$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 পাওয়া যায়:

php
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#

bash
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-গুলো আছে:

.env
OPENAI_API_KEY=tok_live_your_key
OPENAI_BASE_URL=https://tokens.bd/v1
OPENAI_REQUEST_TIMEOUT=300
VariableConfig keyনোট
OPENAI_API_KEYapi_keyআপনার Tokens key।
OPENAI_BASE_URLbase_uriDefault api.openai.com/v1। https://tokens.bd/v1 দিন, /v1 সমেত।
OPENAI_REQUEST_TIMEOUTrequest_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 রাখুন।

config/openai.php
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 করুন#

php
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 করুন#

routes/web.php
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 ব্যবহার করে।

curl-hello.php
<?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]:

curl-stream.php
<?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 ছাড়াই দেখতে চাইলে:

bash
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_endpointBase URI ভুল। https://tokens.bd/v1 দিন, /v1 সমেত, শেষে /chat/completions ছাড়া।
404 model_not_foundModel id ভুল। /models থেকে copy করুন।
401 missing_api_key বা invalid_api_keyKey ফাঁকা ছিল বা ভুল। 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_creditsbilling-এ টাকা যোগ করুন।
429 rate_limited বা concurrency_limitRetry-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 সঙ্গে সঙ্গে অকেজো হয়ে যায়।

এই পাতাটা কি কাজে লেগেছে?

এখনো আটকে আছেন? Support ticket খুলুন

আপনার agent set up করতে সাহায্য লাগবে?

Connection tester দিয়ে সংযোগ পরীক্ষা করে নিন, অথবা একটা API key তৈরি করুন।