adachsoft / ai-integration
Unified AI tool-calling chat abstraction for PHP 8.3 with pluggable SPI providers (OpenAI, Deepseek) and a clean Public API.
Requires
- php: ^8.3
- adachsoft/collection: ^3.0
- guzzlehttp/guzzle: ^7.9
Requires (Dev)
- adachsoft/console-io: ^0.2
- adachsoft/php-code-style: ^0.4
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1.38
- phpunit/phpunit: ^13.0
- rector/rector: ^2.3
- vlucas/phpdotenv: ^5.6
This package is auto-updated.
Last update: 2026-08-19 06:35:19 UTC
README
Unified AI tool-calling chat abstraction for PHP 8.3 with pluggable providers and a clean, framework-agnostic Public API.
- Public API: simple facade to send chat messages, define tools (function-calling), and receive results.
- Built-in providers: OpenAI and Deepseek.
- SPI: implement your own provider by fulfilling a tiny interface and DTO set.
- HTTP logging: optional, via a small logger interface.
- Zero-framework: no container required; everything is manually wired with a builder.
Requirements
- PHP 8.3+
- ext-json, ext-mbstring (standard in most PHP installations)
Installation
composer require adachsoft/ai-integration
Quick start (Public API)
use AdachSoft\AiIntegration\PublicApi\Builder\ToolCallingChatFacadeBuilder;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ChatMessageDto;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ChatRoleEnum;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\Collection\ChatMessageDtoCollection;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\Collection\ToolDefinitionDtoCollection;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ToolCallingChatRequestDto;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ToolDefinitionDto;
$builder = ToolCallingChatFacadeBuilder::create()
->withOpenAi(apiKey: getenv('OPENAI_API_KEY'));
$messages = new ChatMessageDtoCollection([
ChatMessageDto::createSystemMessage('You are a helpful assistant.'),
ChatMessageDto::createUserMessage('Add 2 and 3 using a tool and show the token.'),
]);
$tools = new ToolDefinitionDtoCollection([
new ToolDefinitionDto(
name: 'sum',
description: 'Returns JSON {result: string, token: string}',
parametersSchema: [
'type' => 'object',
'properties' => [
'a' => ['type' => 'number'],
'b' => ['type' => 'number'],
],
'required' => ['a', 'b'],
'additionalProperties' => false,
],
),
]);
$request = new ToolCallingChatRequestDto(
messages: $messages,
tools: $tools,
providerId: 'openai',
modelId: 'gpt-4o-mini',
parameters: [
// Free-form provider parameters.
// They are forwarded 1:1 to the provider payload (no defaults are injected).
// Example (only if your model supports it):
// 'temperature' => 0.0,
],
);
$facade = $builder->build();
$response = $facade->chat($request);
if ($response->result !== null) {
echo $response->result; // final model answer (should include tool token if your prompt enforces it)
}
foreach ($response->toolCalls as $call) {
// inspect tool calls if needed
}
Chat messages and tool calls (Public API)
ChatMessageDto is the main DTO used to build a conversation. Its content can be either a plain string or a list of structured blocks for multimodal payloads (see the next section):
public function __construct(
public ChatRoleEnum $role,
public string|array $content,
public ToolCallDtoCollection $toolCalls,
public array $metadata = [],
) { }
To make common cases easier and to guarantee that toolCalls is never null, use the static factories:
ChatMessageDto::createSystemMessage(string|array $content, array $metadata = []): self;
ChatMessageDto::createUserMessage(string|array $content, array $metadata = []): self;
ChatMessageDto::createAssistantMessage(string|array $content, ToolCallDtoCollection $toolCalls, array $metadata = []): self;
ChatMessageDto::createToolMessage(string|array $content, ToolCallDtoCollection $toolCalls, array $metadata = []): self;
Typical patterns:
- system/user messages: empty
ToolCallDtoCollection(created by the factory), - assistant messages: may include tool calls when the model proposes or summarizes tool usage,
- tool messages: should carry tool results and the corresponding
ToolCallDtoinstances intoolCalls.
Multimodal (image + text) chat with OpenAI
For OpenAI models that support vision (for example gpt-4o), you can send multimodal content by passing a list of content blocks as the content of a ChatMessageDto. The list is forwarded 1:1 to the provider payload.
Each block in the list follows the OpenAI format, for example:
['type' => 'text', 'text' => 'Describe this image']['type' => 'image_url', 'image_url' => ['url' => 'data:image/jpeg;base64,BASE64_ENCODED_IMAGE']]
A minimal example using the Public API:
use AdachSoft\AiIntegration\PublicApi\Builder\ToolCallingChatFacadeBuilder;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ChatMessageDto;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\Collection\ChatMessageDtoCollection;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\Collection\ToolDefinitionDtoCollection;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ToolCallingChatRequestDto;
$builder = ToolCallingChatFacadeBuilder::create()
->withOpenAi(apiKey: getenv('OPENAI_API_KEY'));
// Produce a JPEG binary using any method you like (GD, Imagick, file_get_contents, ...)
$imageBinary = /* string JPEG binary */;
// Encode it as a data URL understood by OpenAI vision models
$dataUrl = 'data:image/jpeg;base64,' . base64_encode($imageBinary);
$messages = new ChatMessageDtoCollection([
ChatMessageDto::createUserMessage([
[
'type' => 'text',
'text' => 'Read the text from this image and respond with the exact text only.',
],
[
'type' => 'image_url',
'image_url' => [
'url' => $dataUrl,
],
],
]),
]);
$request = new ToolCallingChatRequestDto(
messages: $messages,
tools: new ToolDefinitionDtoCollection([]), // no tools needed for pure vision OCR
providerId: 'openai',
modelId: 'gpt-4o', // or another OpenAI multimodal model
parameters: [
'max_tokens' => 100,
],
);
$facade = $builder->build();
$response = $facade->chat($request);
echo $response->result; // should contain the text read from the image
Notes:
- The library does not generate images; you are responsible for providing a valid JPEG/PNG binary and building the data URL.
- The
contentarray is passed as-is to the OpenAI-compatible payload builder, so you must follow the provider's multimodal format. - Always use a model that explicitly supports vision (for example
gpt-4o) when sending images.
Generation parameters (parameters)
Generation settings are a free-form associative array and are provider-specific. The library forwards them 1:1 to the provider payload.
Important:
- Do not rely on implicit defaults (e.g.
temperature). If you do not pass a key, it is not sent. - Passing unsupported parameters may result in a provider error.
Request cost
Request cost is optional. This library does not ship a pricing implementation and does not depend on any pricing package. To obtain ToolCallingChatResponseDto->cost, the host application must supply its own calculator via SPI.
Without a calculator, cost is always null.
Adapter pattern (host application)
use AdachSoft\AiIntegration\Spi\Cost\CostCalculatorInterface;
use AdachSoft\AiIntegration\Spi\Cost\Dto\CostCalculationRequestDto;
use AdachSoft\AiIntegration\Spi\Cost\Dto\CostSpiDto;
final class MyCostCalculatorAdapter implements CostCalculatorInterface
{
public function __construct(
private readonly /* pricing engine */ $engine,
private readonly /* pricing source */ $pricingSource,
) {}
public function calculate(CostCalculationRequestDto $dto): ?CostSpiDto
{
// 1) no usage -> null
if ($dto->tokenUsage === null) {
return null;
}
// 2) load price list for $dto->providerId + $dto->modelId; missing -> null
// 3) build usage from $dto->tokenUsage->inputTokens/outputTokens
// and keys in $dto->tokenUsage->details (e.g. cached_input_tokens)
// 4) optionally read provider-returned cost from $dto->responseMetadata['raw_usage']
// 5) compute cost with your engine
// 6) return CostSpiDto(amount as decimal string, currency, details)
return new CostSpiDto(
amount: '0.001234',
currency: 'USD',
details: [],
);
}
}
Registration
use AdachSoft\AiIntegration\PublicApi\Builder\ToolCallingChatFacadeBuilder;
$facade = ToolCallingChatFacadeBuilder::create()
->withOpenAi($apiKey)
->withCostCalculator(new MyCostCalculatorAdapter($engine, $pricingSource))
->build();
$response = $facade->chat($request);
if ($response->cost !== null) {
echo $response->cost->amount . ' ' . $response->cost->currency;
}
Notes:
- Calculator implementations must return
null(not throw) when a price cannot be determined. - Provider-specific billing fields travel in
tokenUsage.details(for examplecached_input_tokens). - OpenAI-compatible parsing may also expose raw usage under response metadata key
raw_usagefor calculators that need provider-native cost fields.
Providers
- OpenAI (provider id:
openai)- Endpoint: https://api.openai.com/v1/chat/completions
- Env:
OPENAI_API_KEY
- Deepseek (provider id:
deepseek)- Endpoint: https://api.deepseek.com/chat/completions
- Env:
DEEPSEEK_API_KEY
If you pass modelId as null, each provider uses its default. For OpenAI, a safe starter is gpt-4o-mini; for Deepseek: deepseek-chat.
HTTP logging and CLI example
You can inject your own HTTP traffic logger via the builder. A convenient demonstration script is included:
php bin/test-ai-tool-chat.php --provider=openai --model=gpt-4o-mini --show-meta=on --log-http=1 --log-headers=0
The script runs 3 scenarios (smoke and two tool-calling flows) and optionally pretty-prints HTTP request/response payloads.
SPI (Service Provider Interface)
Do not depend on internals. External integrations should use only:
AdachSoft\AiIntegration\PublicApi- the facade and DTOs to call the model.AdachSoft\AiIntegration\Spi- the small interface and DTOs to implement your own provider.
In addition to the core Public API and SPI, the library exposes a reusable, public module under src/Support/OpenAiCompatible/* with DTOs, payload builders and HTTP helpers for OpenAI-compatible chat.completions endpoints. If you already have an OpenAI-compatible HTTP API, you can use the ready-to-use SPI provider OpenAiCompatibleToolCallingChatSpi to bridge your endpoint with the domain model.
All other namespaces (Application, Domain, Infrastructure) are internal and may change at any time.
SPI guides (one document per SPI):
- docs/SPI.md — index
- docs/spi/tool-calling.md — tool-calling provider SPI
- docs/spi/cost-calculation.md — request cost SPI
- docs/spi/http-traffic-logging.md — HTTP traffic logging SPI
Production verification of built-in SPI providers
When you implement a new built-in ToolCalling provider inside this library
(for example, a new provider wired into ToolCallingChatFacadeBuilder), you
must add a production test that verifies the provider end-to-end using the
shared base test:
Tests\\Production\\ToolCalling\\AbstractToolCallingProviderProductionTestCase
To add a new provider production test:
- Ensure your provider is exposed on the builder, for example:
ToolCallingChatFacadeBuilder::withMyProvider(string $apiKey): self.
Create
tests/Production/ToolCalling/MyProviderProductionTest.php:use AdachSoft\\AiIntegration\\PublicApi\\Builder\\ToolCallingChatFacadeBuilder; use PHPUnit\\Framework\\Attributes\\Group; #[Group('external')] final class MyProviderProductionTest extends AbstractToolCallingProviderProductionTestCase { protected function getProviderId(): string { return 'my-provider'; } protected function getApiKeyEnvName(): string { return 'MY_PROVIDER_API_KEY'; } protected function getDefaultModelId(): string { return 'my-model-id'; } protected function configureBuilder(ToolCallingChatFacadeBuilder $builder, string $apiKey): ToolCallingChatFacadeBuilder { return $builder->withMyProvider(apiKey: $apiKey); } }- Provide a real API key via environment variable (
MY_PROVIDER_API_KEY) or.envfile. The base test will:- resolve the API key from env/
.envusingresolveApiKeyFromEnvironment(), - resolve the model id from env (for example,
MY-PROVIDER_MODEL) orgetDefaultModelId().
- resolve the API key from env/
- Run production tests for the provider (for example):
vendor/bin/phpunit tests/Production/ToolCalling/MyProviderProductionTest.php --group external
The base test will validate both:
- a simple smoke chat that must return a non-empty final result, and
- a minimal tool-calling scenario (sum of 2 and 3) that must:
- produce at least one tool call with non-empty arguments,
- keep a non-empty
toolCallId, - return a final answer that includes
5.
Testing
- Run unit and integration tests:
composer test - Production checks (require API keys): see
tests/Productionandbin/test-ai-tool-chat.php.