A modern PHP 8.1+ client library for interacting with the Blesta AI API (ai.blesta.com). This library provides a simple, intuitive interface for chat completions, streaming responses, model listings, and credit balance management.
- Modern PHP 8.1+: Uses typed properties, readonly classes, and named arguments
- Streaming Support: Real-time Server-Sent Events (SSE) streaming for chat completions
- Comprehensive Error Handling: Specific exception types for different error scenarios
- PSR-4 Compliant: Follows PHP-FIG standards with proper autoloading
- Well Documented: Extensive PHPDoc comments and usage examples
- Guzzle HTTP Client: Robust HTTP handling with built-in retry logic
- PHP 8.1 or higher
- Composer
- Guzzle HTTP client (^7.0)
Install via Composer:
composer require blesta/ai-clientOr add to your composer.json:
{
"require": {
"blesta/ai-client": "^1.0"
}
}<?php
require_once 'vendor/autoload.php';
use BlestaAi\Client\BlestaAiClient;
// Initialize the client with your API key
$client = new BlestaAiClient('your-api-key');
// Send a chat completion request
$response = $client->chatCompletion('openai/gpt-4', [
['role' => 'user', 'content' => 'Hello!']
]);
echo $response->getContent();
echo "Cost: $" . $response->usage->cost;
echo "Balance: $" . $response->usage->remainingBalance;use BlestaAi\Client\BlestaAiClient;
$client = new BlestaAiClient('your-api-key');
$response = $client->chatCompletion('openai/gpt-4', [
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'What is 2+2?']
], [
'temperature' => 0.7,
'max_tokens' => 100
]);
// Access response content
echo $response->getContent();
// Access usage information
echo "Model: {$response->model}\n";
echo "Tokens used: {$response->usage->totalTokens}\n";
echo "Cost: $" . number_format($response->usage->cost, 6) . "\n";
echo "Remaining balance: $" . number_format($response->usage->remainingBalance, 4) . "\n";Stream responses in real-time:
$client->streamChatCompletion(
'openai/gpt-4',
[
['role' => 'user', 'content' => 'Tell me a story']
],
function ($chunk, $data) {
// Print content as it arrives
if ($data && isset($data['choices'][0]['delta']['content'])) {
echo $data['choices'][0]['delta']['content'];
flush();
}
// Usage data is in the final chunk
if ($data && isset($data['usage'])) {
echo "\nCost: $" . $data['usage']['cost'];
echo "\nBalance: $" . $data['usage']['balance_remaining'];
}
},
[
'temperature' => 0.8,
'max_tokens' => 500
]
);$models = $client->getModels();
foreach ($models as $model) {
echo "{$model->id}\n";
echo " Prompt price: $" . $model->promptPrice . " per 1K tokens\n";
echo " Completion price: $" . $model->completionPrice . " per 1K tokens\n";
}$balance = $client->getCredits();
echo "Current balance: $" . number_format($balance, 4);For development or custom deployments:
$client = new BlestaAiClient(
apiKey: 'your-api-key',
baseUrl: 'http://localhost:3030/api/v1'
);Set a custom timeout (in seconds):
$client = new BlestaAiClient(
apiKey: 'your-api-key',
baseUrl: 'https://ai.blesta.com/api/v1',
timeout: 60 // 60 seconds
);The library provides specific exception types for different error scenarios:
use BlestaAi\Client\Exceptions\AuthenticationException;
use BlestaAi\Client\Exceptions\InsufficientCreditsException;
use BlestaAi\Client\Exceptions\ValidationException;
use BlestaAi\Client\Exceptions\BlestaAiException;
try {
$response = $client->chatCompletion('openai/gpt-4', [
['role' => 'user', 'content' => 'Hello!']
]);
} catch (AuthenticationException $e) {
// Invalid or missing API key
echo "Authentication failed: {$e->getMessage()}";
} catch (InsufficientCreditsException $e) {
// Not enough credits
echo "Insufficient credits!";
echo "Required: $" . $e->required;
echo "Available: $" . $e->available;
} catch (ValidationException $e) {
// Invalid request parameters
echo "Validation errors: " . json_encode($e->getErrors());
} catch (BlestaAiException $e) {
// General API error
echo "API error: {$e->getMessage()} (Code: {$e->getCode()})";
}public function __construct(
string $apiKey,
string $baseUrl = 'https://ai.blesta.com/api/v1',
int $timeout = 30
)Send a non-streaming chat completion request.
public function chatCompletion(
string $model,
array $messages,
array $options = []
): ChatCompletionParameters:
$model- Model identifier (e.g., "openai/gpt-4", "anthropic/claude-3-sonnet")$messages- Array of message objects with 'role' and 'content'$options- Optional parameters:temperature(float): 0-2, default varies by modelmax_tokens(int): Maximum tokens to generatetop_p(float): 0-1, nucleus samplingfrequency_penalty(float): -2 to 2presence_penalty(float): -2 to 2
Returns: ChatCompletion object
Throws: AuthenticationException, InsufficientCreditsException, ValidationException, BlestaAiException
Send a streaming chat completion request.
public function streamChatCompletion(
string $model,
array $messages,
callable $callback,
array $options = []
): voidParameters:
$model- Model identifier$messages- Array of message objects$callback- Function called for each chunk:function(string $chunk, ?array $data): void$options- Same aschatCompletion()
Get list of available models with pricing.
public function getModels(): arrayReturns: Array of Model objects
Get current credit balance.
public function getCredits(): floatReturns: Current balance as a float
readonly class ChatCompletion
{
public string $id;
public string $model;
public array $choices;
public Usage $usage;
public int $created;
public function getContent(): string;
public function getFinishReason(): string;
public function toArray(): array;
}readonly class Usage
{
public int $promptTokens;
public int $completionTokens;
public int $totalTokens;
public float $cost;
public float $remainingBalance;
public function toArray(): array;
}readonly class Model
{
public string $id;
public string $name;
public ?string $description;
public ?float $promptPrice;
public ?float $completionPrice;
public ?int $contextLength;
public function toArray(): array;
}The examples/ directory contains complete, runnable examples:
chat_completion.php- Basic non-streaming chat completionstreaming_chat.php- Real-time streaming responselist_models.php- List all available models with pricingcheck_credits.php- Check your credit balance
To run an example:
cd examples
php chat_completion.phpNote: Update the $apiKey variable in each example with your actual API key.
The API supports models from various providers:
- OpenAI:
openai/gpt-4,openai/gpt-3.5-turbo, etc. - Anthropic:
anthropic/claude-3-opus,anthropic/claude-3-sonnet, etc. - X.AI:
x-ai/grok-4-fast, etc. - Google:
google/gemini-pro, etc. - And many more...
Use getModels() to retrieve the current list of available models with pricing.
php-client-library/
├── src/
│ ├── BlestaAiClient.php # Main client class
│ ├── Exceptions/
│ │ ├── BlestaAiException.php # Base exception
│ │ ├── AuthenticationException.php
│ │ ├── InsufficientCreditsException.php
│ │ └── ValidationException.php
│ └── Models/
│ ├── ChatCompletion.php
│ ├── Model.php
│ └── Usage.php
├── examples/ # Usage examples
├── composer.json # Dependencies
└── README.md # This file
cd php-client-library
composer installThis library is designed to be used within Blesta modules or plugins to integrate AI functionality. Here's a basic example:
class MyBlestaModule extends Module
{
public function processAiRequest($apiKey, $userMessage)
{
$client = new \BlestaAi\Client\BlestaAiClient($apiKey);
try {
$response = $client->chatCompletion('openai/gpt-4', [
['role' => 'user', 'content' => $userMessage]
]);
return [
'success' => true,
'content' => $response->getContent(),
'cost' => $response->usage->cost,
'balance' => $response->usage->remainingBalance
];
} catch (\BlestaAi\Client\Exceptions\BlestaAiException $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}When integrating AI functionality into Blesta, you'll need to store chat conversations and messages for context management. This schema follows Blesta's database conventions and supports multi-tenant architecture.
The schema uses two main tables:
ai_conversations- Stores conversation sessions/threadsai_messages- Stores individual messages (system prompts, user messages, assistant responses)
Additional settings like remaining tokens, temperature, and max_tokens can be stored in Blesta's existing settings table.
Stores conversation sessions with immutable (append-only) design following Blesta's log table pattern.
CREATE TABLE `ai_conversations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) unsigned NOT NULL COMMENT 'Multi-tenant support (normally 1)',
`staff_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Staff member ID (0 = system)',
`title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Conversation title/description',
`model` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'AI model used (e.g., openai/gpt-4)',
`status` enum('active','archived') COLLATE utf8_unicode_ci NOT NULL DEFAULT 'active',
`date_created` datetime NOT NULL COMMENT 'When conversation was created',
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `staff_id` (`staff_id`),
KEY `status` (`status`),
KEY `date_created` (`date_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;Field Descriptions:
id: Auto-increment primary keycompany_id: Links to Blesta's company table for multi-tenant support (typically 1)staff_id: Links to Blesta's staff table (0 = system-generated, >0 = staff member)title: Optional human-readable title for the conversationmodel: The AI model identifier used for this conversationstatus: Active conversations vs. archived ones (no physical deletion for audit trail)date_created: Timestamp when conversation was started
Stores individual messages within conversations, including system prompts, user input, and AI responses.
CREATE TABLE `ai_messages` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`conversation_id` int(10) unsigned NOT NULL COMMENT 'Parent conversation',
`role` enum('system','user','assistant') COLLATE utf8_unicode_ci NOT NULL COMMENT 'Message role',
`content` mediumtext COLLATE utf8_unicode_ci NOT NULL COMMENT 'Message content',
`prompt_tokens` int(10) unsigned DEFAULT NULL COMMENT 'Tokens in prompt (for assistant messages)',
`completion_tokens` int(10) unsigned DEFAULT NULL COMMENT 'Tokens in completion (for assistant messages)',
`cost` decimal(10, 4) DEFAULT NULL COMMENT 'Credit cost for this API call (for assistant messages)',
`date_created` datetime NOT NULL COMMENT 'When message was created',
PRIMARY KEY (`id`),
KEY `conversation_id` (`conversation_id`),
KEY `role` (`role`),
KEY `date_created` (`date_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;Field Descriptions:
id: Auto-increment primary keyconversation_id: Foreign key toai_conversations.idrole: Message type:system- System prompts that set AI behavioruser- Messages from users/staffassistant- Responses from the AI
content: The actual message text (usesmediumtextto support long responses)prompt_tokens: Number of tokens in the prompt (populated for assistant responses)completion_tokens: Number of tokens in the completion (populated for assistant responses)cost: Credit cost for this API call (populated for assistant responses)date_created: Timestamp when message was added
Immutable/Append-Only Design:
- Messages are never edited or deleted, only marked as archived
- Maintains complete audit trail of all AI interactions
- Follows Blesta's
log_*table pattern for compliance
Message Roles:
- system: Contains instructions/prompts that guide AI behavior (e.g., "You are a helpful Blesta support assistant")
- user: Input from staff members or system
- assistant: Responses from the AI model
Credit/Cost Tracking:
- Tokens and credit costs are tracked per-message for granular reporting
- Only assistant messages have token/cost data
- Enables per-conversation and per-staff usage analysis
Multi-Tenancy:
company_idsupports multiple Blesta installations in same database- Standard Blesta pattern for enterprise deployments
// Create conversation
$conversationId = $this->Record->insert('ai_conversations', [
'company_id' => 1,
'staff_id' => $staffId, // or 0 for system
'title' => 'Customer Support Chat',
'model' => 'openai/gpt-4',
'status' => 'active',
'date_created' => date('Y-m-d H:i:s')
]);
// Add system prompt
$this->Record->insert('ai_messages', [
'conversation_id' => $conversationId,
'role' => 'system',
'content' => 'You are a helpful Blesta support assistant. Provide clear, accurate answers about Blesta features.',
'date_created' => date('Y-m-d H:i:s')
]);// Add user message to conversation
$this->Record->insert('ai_messages', [
'conversation_id' => $conversationId,
'role' => 'user',
'content' => $userQuestion,
'date_created' => date('Y-m-d H:i:s')
]);
// Retrieve conversation context (all messages for this conversation)
$messages = $this->Record->select(['role', 'content'])
->from('ai_messages')
->where('conversation_id', '=', $conversationId)
->order(['id' => 'ASC'])
->fetchAll();
// Send to AI API
$client = new \BlestaAi\Client\BlestaAiClient($apiKey);
$response = $client->chatCompletion('openai/gpt-4', $messages);
// Save AI response
$this->Record->insert('ai_messages', [
'conversation_id' => $conversationId,
'role' => 'assistant',
'content' => $response->getContent(),
'prompt_tokens' => $response->usage->promptTokens,
'completion_tokens' => $response->usage->completionTokens,
'cost' => $response->usage->cost,
'date_created' => date('Y-m-d H:i:s')
]);// Get all conversations for a staff member
$conversations = $this->Record->select()
->from('ai_conversations')
->where('company_id', '=', $companyId)
->where('staff_id', '=', $staffId)
->where('status', '=', 'active')
->order(['date_created' => 'DESC'])
->fetchAll();
// Get all messages in a conversation
$messages = $this->Record->select()
->from('ai_messages')
->where('conversation_id', '=', $conversationId)
->order(['id' => 'ASC'])
->fetchAll();// Total credits used by a staff member
$totalCredits = $this->Record->select(['SUM(m.cost) as total_credits'])
->from('ai_messages', 'm')
->innerJoin('ai_conversations', 'c', 'c.id', '=', 'm.conversation_id', false)
->where('c.staff_id', '=', $staffId)
->where('c.company_id', '=', $companyId)
->fetch();
// Total tokens used by company
$totalTokens = $this->Record->select([
'SUM(m.prompt_tokens) as total_prompt_tokens',
'SUM(m.completion_tokens) as total_completion_tokens'
])
->from('ai_messages', 'm')
->innerJoin('ai_conversations', 'c', 'c.id', '=', 'm.conversation_id', false)
->where('c.company_id', '=', $companyId)
->fetch();// Archive conversations older than 90 days
$this->Record->where('company_id', '=', $companyId)
->where('date_created', '<', date('Y-m-d H:i:s', strtotime('-90 days')))
->update('ai_conversations', ['status' => 'archived']);Use Blesta's existing settings table for AI-related configuration:
// Store API key per company
Settings::setSetting('ai_api_key', $apiKey, $companyId);
// Store default model
Settings::setSetting('ai_default_model', 'openai/gpt-4', $companyId);
// Store default parameters
Settings::setSetting('ai_temperature', '0.7', $companyId);
Settings::setSetting('ai_max_tokens', '500', $companyId);
// Store remaining balance cache (updated after each API call)
Settings::setSetting('ai_balance', $balance, $companyId);When implementing this schema, create a Blesta migration file:
// /path/to/blesta/components/upgrades/tasks/upgrade_x_x_x.php
class UpgradeXxx extends UpgradeUtil
{
public function __construct(Record $Record)
{
parent::__construct($Record);
}
public function up()
{
// Create ai_conversations table
if (!$this->Record->tableExists('ai_conversations')) {
$this->Record->query("
CREATE TABLE `ai_conversations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) unsigned NOT NULL,
`staff_id` int(10) unsigned NOT NULL DEFAULT 0,
`title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`model` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`status` enum('active','archived') COLLATE utf8_unicode_ci NOT NULL DEFAULT 'active',
`date_created` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
KEY `staff_id` (`staff_id`),
KEY `status` (`status`),
KEY `date_created` (`date_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
");
}
// Create ai_messages table
if (!$this->Record->tableExists('ai_messages')) {
$this->Record->query("
CREATE TABLE `ai_messages` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`conversation_id` int(10) unsigned NOT NULL,
`role` enum('system','user','assistant') COLLATE utf8_unicode_ci NOT NULL,
`content` mediumtext COLLATE utf8_unicode_ci NOT NULL,
`prompt_tokens` int(10) unsigned DEFAULT NULL,
`completion_tokens` int(10) unsigned DEFAULT NULL,
`cost` decimal(10, 4) DEFAULT NULL COMMENT 'Credit cost',
`date_created` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `conversation_id` (`conversation_id`),
KEY `role` (`role`),
KEY `date_created` (`date_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
");
}
}
public function down()
{
// Drop tables in reverse order
$this->Record->query("DROP TABLE IF EXISTS `ai_messages`");
$this->Record->query("DROP TABLE IF EXISTS `ai_conversations`");
}
}-
Context Management: Load only necessary messages to stay within token limits
- Consider limiting context to last N messages or last X tokens
- Summarize old messages if conversations get very long
-
Credit Control: Monitor usage per staff member and set limits
- Implement daily/monthly credit usage caps
- Alert when approaching limits
-
Privacy: Ensure sensitive data is handled appropriately
- Consider encryption for content field if storing PII
- Implement proper access controls
-
Performance: Add indexes for common queries
- Index on
company_id,staff_id,conversation_id - Consider archiving old conversations
- Index on
-
Error Handling: Store failed requests for debugging
- Consider adding an error log table
- Track API errors separately from successful messages
MIT License
For issues, questions, or contributions, please visit:
- API Documentation: https://ai.blesta.com/docs/api (in development mode)
- Blesta Support: https://www.blesta.com/support/
- Chat completions (streaming and non-streaming)
- Model listing with pricing
- Credit balance checking
- Comprehensive exception handling
- PSR-4 autoloading
- Full documentation and examples