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/ai-conversation-model: ^0.2.0
- 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
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 21:40:42 UTC
README
Unified AI tool-calling chat abstraction for PHP 8.3 with pluggable providers and a framework-agnostic Public API.
- Public API for chat requests and provider selection.
- Built-in OpenAI and DeepSeek integrations.
- A small SPI for custom providers and optional request-cost calculation.
- Optional HTTP traffic logging and no framework-container requirement.
Requirements and installation
- PHP 8.3 or later
ext-jsonandext-mbstring
composer require adachsoft/ai-integration
The conversation messages, content parts, tools, usage, and cost use the shared adachsoft/ai-conversation-model types.
Quick start
use AdachSoft\AiConversationModel\Content\Collection\ContentPartCollection;
use AdachSoft\AiConversationModel\Content\TextContentPartDto;
use AdachSoft\AiConversationModel\Message\ChatMessageDto;
use AdachSoft\AiConversationModel\Message\ChatMessageIdVo;
use AdachSoft\AiConversationModel\Message\ChatRoleEnum;
use AdachSoft\AiConversationModel\Message\Collection\ChatMessageDtoCollection;
use AdachSoft\AiConversationModel\Tool\Collection\ToolCallDtoCollection;
use AdachSoft\AiConversationModel\Tool\Collection\ToolDefinitionDtoCollection;
use AdachSoft\AiConversationModel\Tool\Collection\ToolResultDtoCollection;
use AdachSoft\AiConversationModel\Tool\ToolDefinitionDto;
use AdachSoft\AiConversationModel\Tool\ToolNameVo;
use AdachSoft\AiIntegration\Contract\ToolCalling\Dto\ChatChoiceDto;
use AdachSoft\AiIntegration\PublicApi\Builder\ToolCallingChatFacadeBuilder;
use AdachSoft\AiIntegration\PublicApi\ToolCalling\Dto\ToolCallingChatRequestDto;
$createdAt = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
$messages = new ChatMessageDtoCollection([
new ChatMessageDto(
id: new ChatMessageIdVo('00000000-0000-4000-8000-000000000001'),
createdAt: $createdAt,
role: ChatRoleEnum::System,
content: new ContentPartCollection([
new TextContentPartDto('You are a helpful assistant.'),
]),
toolCalls: new ToolCallDtoCollection([]),
toolResults: new ToolResultDtoCollection([]),
metadata: [],
),
new ChatMessageDto(
id: new ChatMessageIdVo('00000000-0000-4000-8000-000000000002'),
createdAt: $createdAt->modify('+1 second'),
role: ChatRoleEnum::User,
content: new ContentPartCollection([
new TextContentPartDto('Add 2 and 3 using the sum tool.'),
]),
toolCalls: new ToolCallDtoCollection([]),
toolResults: new ToolResultDtoCollection([]),
metadata: [],
),
]);
$tools = new ToolDefinitionDtoCollection([
new ToolDefinitionDto(
name: new ToolNameVo('sum'),
description: 'Adds two numbers.',
parametersSchema: [
'type' => 'object',
'properties' => [
'a' => ['type' => 'number'],
'b' => ['type' => 'number'],
],
'required' => ['a', 'b'],
'additionalProperties' => false,
],
),
]);
$facade = ToolCallingChatFacadeBuilder::create()
->withOpenAi(apiKey: getenv('OPENAI_API_KEY'))
->build();
$completion = $facade->chat(new ToolCallingChatRequestDto(
messages: $messages,
tools: $tools,
providerId: 'openai',
parameters: ['temperature' => 0.1],
metadata: [],
modelId: 'gpt-4o-mini',
timeoutSeconds: 30,
));
// A completion may contain multiple independent choices. Select one explicitly.
$choice = null;
foreach ($completion->choices as $candidate) {
$choice = $candidate;
break;
}
if (!$choice instanceof ChatChoiceDto) {
throw new RuntimeException('The provider returned no choices.');
}
// Append the selected assistant turn before any tool results.
$messagesForNextRequest = iterator_to_array($messages);
$messagesForNextRequest[] = new ChatMessageDto(
id: new ChatMessageIdVo('00000000-0000-4000-8000-000000000003'),
createdAt: $createdAt->modify('+2 seconds'),
role: ChatRoleEnum::Assistant,
content: $choice->content,
toolCalls: $choice->toolCalls,
toolResults: new ToolResultDtoCollection([]),
metadata: $choice->metadata,
);
// Create one ToolResultDto for every call in the selected choice.
$results = [];
foreach ($choice->toolCalls as $call) {
// Execute the requested tool with $call->arguments and use its actual result.
$results[] = new \AdachSoft\AiConversationModel\Tool\ToolResultDto(
toolCallId: $call->id,
toolName: $call->name,
content: new ContentPartCollection([
new TextContentPartDto(json_encode(['result' => 5], JSON_THROW_ON_ERROR)),
]),
);
}
if ($results !== []) {
$messagesForNextRequest[] = ChatMessageDto::toolResults(
new ChatMessageIdVo('00000000-0000-4000-8000-000000000004'),
$createdAt->modify('+3 seconds'),
new ToolResultDtoCollection($results),
);
}
$nextRequest = new ToolCallingChatRequestDto(
messages: new ChatMessageDtoCollection($messagesForNextRequest),
tools: $tools,
providerId: 'openai',
parameters: ['temperature' => 0.1],
metadata: [],
modelId: 'gpt-4o-mini',
);
$nextCompletion = $facade->chat($nextRequest);
ToolCallingChatRequestDto takes messages, tools, providerId, parameters, and metadata, followed by optional modelId and timeoutSeconds. The shared message DTO requires an ID and creation time; keep IDs unique and timestamps in conversation order. A choice is not automatically added to history: the caller selects it and appends its assistant message. Each non-empty ToolResultDto contains the matching call ID, tool name, and content parts.
Supported media
Use shared MediaContentPartDto content parts. detail is supplied as a named media parameter where supported.
| Content | Role and provider | Supported source and details |
|---|---|---|
Images (image/jpeg, image/png, image/gif, image/webp) | User, OpenAI | Base64 or URL; optional detail is sent as image_url.detail. |
| Images | User, DeepSeek | Base64 or URL. DeepSeek also accepts an image FileId; the MIME type may be omitted for a file ID. |
PDF (application/pdf) | User, OpenAI | Base64 with a filename, or FileId; URL sources are not supported. |
Input audio (audio/wav, audio/mpeg) | User, OpenAI | Base64; these map to the provider's WAV and MP3 input-audio formats. |
| Generated audio | Assistant, OpenAI | The assistant media part refers to the provider audio object through parameters['provider_audio_id']. |
OpenAI-compatible chat-completions tool results cannot carry media as tool-message content. For a tool result containing media, add an additional user message after the assistant message and all tool results. Use one such user message per media-bearing result; the library does not turn this into native tool-result media support.
For DeepSeek reasoning, select the relevant choice and preserve its metadata['openai_compatible_message_fields']['reasoning_content'] when creating the assistant history message. Passing the selected choice's metadata through unchanged preserves this field.
Extending OpenAI-compatible content handling
The built-in encoders are composed through ContentPartEncoderCollection and implement ContentPartEncoderInterface. Supply extra encoders through the extraEncoders parameter of the OpenAI or DeepSeek builder methods; extra encoders are checked before the defaults. MessageFormatterInterface isolates provider-specific role and message formatting.
OpenAiCompatibleToolCallingChatSpiFactory::create() also accepts optional extraEncoders and messageFormatter arguments. A null formatter selects the standard OpenAI-compatible formatter. Unsupported or conflicting content is rejected with UnsupportedContentException before an HTTP request is sent.
Request cost
Cost calculation is optional and has no built-in pricing tables. CostCalculatorInterface::calculate(CostCalculationRequestDto $request) returns ?AdachSoft\AiConversationModel\Cost\CostDto. The request's $tokenUsage is the shared AdachSoft\AiConversationModel\Usage\TokenUsageDto.
The calculator is called once for the whole completion. Without a calculator, the provider's cost is preserved when present. With a calculator, a returned cost replaces the provider cost; returning null preserves the provider cost. See the cost-calculation SPI guide.
Provider usage details are available in $tokenUsage->details. The parser preserves provider-specific nested values rather than flattening them into aliases. For example, inspect the prompt_tokens_details.cached_tokens value inside the corresponding entry in details; see the guide for handling its preserved shape.
Providers, logging, and CLI demonstrator
- OpenAI (
openai):https://api.openai.com/v1/chat/completions; setOPENAI_API_KEY. - DeepSeek (
deepseek):https://api.deepseek.com/chat/completions; setDEEPSEEK_API_KEY.
A CLI demonstrator runs a smoke request and two tool-calling scenarios:
php bin/test-ai-tool-chat.php --provider=openai --model=gpt-4o-mini --show-meta=on --log-http=1 --log-headers=0
php bin/test-ai-tool-chat.php --provider=deepseek --model=deepseek-flash
HTTP traffic logging is optional and can be enabled with --log-http=1. Request headers are hidden unless --log-headers=1 is also supplied.
Limitations
- Streaming is not supported.
- Chat Completions does not generate images and does not accept video input.
deepseek-v4-prodoes not support vision.deepseek-chathas been withdrawn; usedeepseek-flashfor DeepSeek chat and vision requests.- The package does not implement production file upload or the Responses API.
SPI documentation
External integrations should use the Public API to call the model and the SPI to implement providers. The reusable OpenAI-compatible toolkit is documented in the tool-calling SPI guide. The package also documents cost calculation and HTTP traffic logging.
Migration from the previous model
The following previous type names are listed only to identify migration targets.
| Previous type or field | Current type or field |
|---|---|
PublicApi\ToolCalling\Dto\ChatMessageDto | AiConversationModel\Message\ChatMessageDto |
PublicApi\ToolCalling\Dto\ChatRoleEnum | AiConversationModel\Message\ChatRoleEnum |
PublicApi\ToolCalling\Dto\ToolCallDto | AiConversationModel\Tool\ToolCallDto |
PublicApi\ToolCalling\Dto\ToolDefinitionDto | AiConversationModel\Tool\ToolDefinitionDto |
PublicApi\ToolCalling\Dto\TokenUsageDto | AiConversationModel\Usage\TokenUsageDto |
PublicApi\ToolCalling\Dto\CostDto | AiConversationModel\Cost\CostDto |
PublicApi\ToolCalling\Dto\ToolCallingChatResponseDto | Contract\ToolCalling\Dto\ChatCompletionDto with choices |
| Public message, call, and definition collections | Shared ChatMessageDtoCollection, ToolCallDtoCollection, and ToolDefinitionDtoCollection |
Domain\Model\ChatMessage, ChatRoleEnum, ToolCall, ToolDefinition, TokenUsage, ChatResponse | Shared message, tool, usage, and ChatCompletionDto types |
Domain\Model\ToolCallingChatPrompt | Domain\Model\ToolCallingChatPromptDto |
| Domain message, call, and definition collections | Shared ChatMessageDtoCollection, ToolCallDtoCollection, and ToolDefinitionDtoCollection |
Spi\ToolCalling\Dto\ChatMessageSpiDto, ToolCallSpiDto, ToolDefinitionSpiDto, TokenUsageSpiDto | Shared message, tool, and TokenUsageDto types |
Spi\ToolCalling\Dto\ToolCallingChatSpiRequest | Spi\ToolCalling\Dto\ToolCallingChatSpiRequestDto |
Spi\ToolCalling\Dto\ToolCallingChatSpiResponse | Contract\ToolCalling\Dto\ChatCompletionDto |
| SPI message, call, and definition collections | Shared message, tool, and definition collections |
Spi\Cost\Dto\CostSpiDto | AiConversationModel\Cost\CostDto |
Support\OpenAiCompatible\Dto\ChatMessage, ToolCall, ToolDefinition, TokenUsage, ChatResult, ToolCallingChatRequest | Shared message, tool, usage, and completion types; ToolCallingChatRequestDto remains the Support request DTO |
| Public, Domain, and Support response mappers | Removed; pass the shared model through the flow |
TokenUsageDto::$details access previously based on raw_usage or the cached_input_tokens alias | Read provider usage fields from the shared TokenUsageDto::$details; nested fields retain their original shape |
Testing
Run the two configured non-production suites independently:
vendor/bin/phpunit --testsuite Unit
vendor/bin/phpunit --testsuite Integration