lingoda / ai-sdk
Framework-agnostic PHP SDK for AI providers with typed results and platform abstraction
Requires
- php: ^8.4
- ext-fileinfo: *
- google-gemini-php/client: ^2.5
- mozex/anthropic-php: ^1.1
- nyholm/psr7: ^1.8
- openai-php/client: ^v0.16
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^1.0|^2.0
- psr/log: ^1.0|^2.0|^3.0
- symfony/http-client: ^7.4|^8.0
- symfony/http-client-contracts: ^3.6
- symfony/lock: ^7.4|^8.0
- symfony/rate-limiter: ^7.4|^8.0
- webmozart/assert: ^1.11
Requires (Dev)
- async-aws/bedrock-runtime: ^1.3
- dg/bypass-finals: ^1.7
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-webmozart-assert: ^2.0
- phpunit/phpunit: ^10.5
- symfony/ai-bedrock-platform: ~0.13.0
- symfony/var-dumper: ^7.4|^8.0
- symplify/easy-coding-standard: ^12.0
Suggests
- async-aws/bedrock-runtime: Required for the AWS Bedrock provider
- symfony/ai-bedrock-platform: Required for the AWS Bedrock provider (~0.13.0)
Provides
None
Conflicts
- symfony/ai-bedrock-platform: <0.13 || >=0.14
Replaces
None
- dev-main
- 2.2.0
- 2.1.0
- 2.0.1
- 2.0.0
- 1.4.1
- 1.4.0
- 1.3.0
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.1
- 1.0.0
- dev-bedrock-attachment-limits
- dev-DO-1094-replace-styfle-concurrency
- dev-MNR-06_ai_sdk_decision_rate_limiting
- dev-MNR-06_ai_sdk_2_0_1_rate_limiter_return_type
- dev-MNR-06_ai_sdk_attachments_bedrock
- dev-LW-34067-add-gemini-31-lite-support
- dev-LW-34451-gemini-structured-output-response-schema
- dev-allow-symfony-8
This package is auto-updated.
Last update: 2026-09-29 16:42:14 UTC
README
Framework-agnostic PHP SDK for AI providers with typed results and platform abstraction.
🚀 Quick Start
use Lingoda\AiSdk\Platform; use Lingoda\AiSdk\Client\OpenAI\OpenAIClientFactory; // Create client using factory $client = OpenAIClientFactory::createClient('your-api-key'); $platform = new Platform([$client]); // Simple ask() method - automatically uses default model $result = $platform->ask('Hello, AI!'); echo $result->getContent(); // TextResult // Or specify a specific model $result = $platform->ask('Hello, AI!', 'gpt-4o-mini'); echo $result->getContent(); // Audio capabilities $audioResult = $platform->textToSpeech('Hello world', $audioOptions); $transcription = $platform->transcribeAudio('/path/to/audio.mp3', $options);
📚 Documentation
| Guide | Description |
|---|---|
| Installation | Setup and Platform basics |
| Configuration | API keys and multi-provider setup |
| Quick Start | Your first AI request |
| Symfony Integration | Bundle configuration and provider-specific platforms |
| HTTP Clients | Advanced HTTP configuration |
| Logging | Debug and monitoring setup |
| Advanced Usage | Complex features and patterns |
| Security | Data protection and sanitization |
| API Reference | Complete API documentation |
| Audio | Speech synthesis and transcription |
| Interactive Examples | Live examples with real APIs |
✨ Key Features
- 🔌 Framework Agnostic - No dependencies on Symfony or other frameworks
- 🛡️ Security First - Built-in data sanitization and attribute-based protection
- 🎯 Type Safe - Strongly-typed results and prompt value objects
- 🌐 Multi-Provider - OpenAI, Anthropic, Gemini and AWS Bedrock (optional) with flexible configuration
- 📎 Attachments - PDFs, DOCX, text files and images on the user prompt, validated per model, never written to traces
- ⚖️ Decisions - Structured yes/no, choice and score answers via TypeSafe Jev
- 🎭 Capabilities - Models declare supported features (vision, tools, audio, streaming, reasoning)
- ⚡ Performance - Built-in rate limiting and token estimation with exponential backoff
- 📝 Rich Prompts - Parameterized prompts and conversation management
- 🎵 Audio Support - Text-to-speech, transcription, and translation with multiple formats
- 🔄 Streaming - Real-time response streaming support
🏗️ Architecture
Platform → Providers → Models → Clients → AI APIs
↓
Results ← Security ← Capabilities ← Response
- Platform: Main entry point for AI operations
- Providers: Manage models for each AI service (OpenAI, Anthropic, Gemini, AWS Bedrock; TypeSafe Jev for decisions)
- Models: Individual AI models with declared capabilities
- Clients: Handle API communication with rate limiting
- Results: Type-safe responses (
TextResult,BinaryResult,StreamResult,ObjectResult,ToolCallResult)
🎨 Usage Patterns
Simple Text Generation
$result = $platform->ask('Explain AI'); echo $result->getContent(); // string print_r($result->getMetadata()); // usage, model info, etc.
Parameterized Prompts
$template = UserPrompt::create('Hello {{name}}, tell me about {{topic}}'); $prompt = $template->withParameters([ 'name' => 'Alice', 'topic' => 'machine learning' ]); // Use ask() method with prompt objects $result = $platform->ask($prompt);
Conversations with Context
$conversation = Conversation::withSystem( UserPrompt::create('What is quantum computing?'), SystemPrompt::create('You are a helpful physics expert') ); // ask() method supports Conversation objects $result = $platform->ask($conversation, 'claude-sonnet-4');
Automatic Data Protection
// Sensitive data is automatically sanitized $prompt = UserPrompt::create('My email is john@example.com'); // Sent to AI as: "My email is [REDACTED_EMAIL]" $result = $platform->ask($prompt); // Disable sanitization if needed $platform = new Platform([$client], enableSanitization: false);
Documents and Images
use Lingoda\AiSdk\Prompt\Attachment; $conversation = Conversation::fromUser(UserPrompt::create('Extract the voucher fields as JSON.')) ->withAttachments(Attachment::fromBytes($pdfBytes, 'application/pdf')); $result = $platform->ask($conversation, 'amazon.nova-2-lite-v1:0');
- The SDK checks a size only where the provider documents a limit and enforces it: Bedrock Claude takes an image of up to 5 MB as base64 (3.75 MB as a file), so a larger one throws
UnsupportedCapabilityExceptionbefore the upload. Every other size limit, the request size included, is the provider's, and an oversized request fails with the provider's error. - The mime type is detected when omitted; passing it explicitly is more reliable for text formats.
- Unsupported combinations throw
UnsupportedCapabilityExceptionbefore any request. - Attachments are sent as provided: the data sanitizer only runs on the prompt text, since pattern redaction corrupts data files (long ids read as phone numbers). Redact attachments yourself if they must not reach the provider.
| Type | OpenAI | Anthropic | Gemini | Bedrock Nova | Bedrock Claude | Needs |
|---|---|---|---|---|---|---|
| yes | yes | yes | yes | yes | Capability::DOCUMENT |
|
| DOCX | no | no | no | yes | no | Capability::DOCUMENT |
| Text: TXT, CSV, Markdown, HTML, JSON (UTF-8) | yes | yes | yes | yes | yes | nothing: sent as text |
| JPEG, PNG, WebP | yes | yes | yes | yes | yes | Capability::VISION |
| GIF | yes | yes | no | yes | yes | Capability::VISION |
Attachments come before the text of the user message.
Conversation::toArray()(what tracing sees) carries onlymimeandsize, never the bytes. There is no filename on purpose.
AWS Bedrock (optional)
composer require symfony/ai-bedrock-platform:~0.13.0 async-aws/bedrock-runtime
use AsyncAws\BedrockRuntime\BedrockRuntimeClient; use Lingoda\AiSdk\Client\Bedrock\BedrockClientFactory; // Let async-aws build its own HTTP client: only then does it retry 429, 5xx and throttling $bedrock = BedrockClientFactory::createClient(new BedrockRuntimeClient(['region' => 'eu-west-1']), $logger); $platform = new Platform([$openAiClient, $bedrock], defaultProvider: 'openai'); $platform->ask($conversation, 'amazon.nova-2-lite-v1:0'); // Nova 2 Lite $platform->ask($conversation, 'anthropic.claude-haiku-4-5-20251001-v1:0'); // Claude Haiku 4.5
- Models are addressed by their Bedrock base id and sent through the region's cross-region inference profile (
eu.orus.), which also ends up inmetadata.model. Keeping data in the EU is up to you: pick aneu-region. - Nova needs the conversation to open with the user message: an assistant prompt before it throws
UnsupportedCapabilityException. Claude accepts it. - Only
temperature,max_tokens(default 4096) andresponse_formatare passed on; other options are dropped.response_formatworks on Claude Haiku 4.5, Sonnet 4.5/4.6 and Opus 4.5/4.6 and is rejected elsewhere. Claude Opus 4.7+, Sonnet 5 and Opus 5.x do not take a temperature, so it is dropped for them. - Claude Opus 5.x thinks before answering: give it enough
max_tokens, or the response has no text. - Errors become
ClientExceptionwith the HTTP status as code, without the previous exception and without the payload, so documents cannot leak into logs or Sentry. Never enable async-awsdebugin production: it logs the full request body, document included. - The runtime client needs an explicit region (config,
AWS_REGIONor~/.aws/config); the silentus-east-1fallback is refused, and onlyeu-andus-regions are accepted. - Wrap it in
RateLimitedClientwithretryTransportErrors: false, so async-aws stays the only transport retry layer.
Decisions with TypeSafe Jev
use Lingoda\AiSdk\Client\TypeSafe\TypeSafeDecisionPlatform; use Lingoda\AiSdk\Decision\Question; $jev = new TypeSafeDecisionPlatform(HttpClient::create(), $apiKey); $result = $jev->decide('Teacher log text', [ 'on_topic' => Question::noul('Is the log about the lesson?'), 'outlook' => Question::choice('How likely is the student to pass?', ['green' => 'on track', 'red' => 'unlikely']), ]); $result->getAnswer('on_topic')->isTrue(); // noul probability >= 0.5 $result->getAnswer('outlook')->choice; // 'green'
Jev is a provider (AIProvider::TYPESAFE, models jev-1.13.0, jev-latest, jev-preview, exposed through $jev->getProvider()), but a decision provider, not a chat provider: it lives behind DecisionPlatformInterface and is never registered on Platform, so ask() cannot route to it. Unknown model ids throw ModelNotFoundException before any request. The data sanitizer does not run on the state.
TypeSafe limits each account to 1,200 requests per minute and 250,000 input tokens per second (models page). Wrap the platform to stay under them:
use Lingoda\AiSdk\RateLimit\RateLimitedDecisionPlatform; use Lingoda\AiSdk\RateLimit\SymfonyRateLimiter; use Lingoda\AiSdk\RateLimit\TokenEstimatorRegistry; $jev = new RateLimitedDecisionPlatform($jev, new SymfonyRateLimiter(), TokenEstimatorRegistry::createDefault());
Each decide() takes one request and the estimated state and question tokens from the limiter (defaults: 90% of TypeSafe's limits), waits when the limiter is exhausted, and retries with exponential backoff when TypeSafe answers 429 or 529, or a gateway answers 502, 503 or 504. Other failures are rethrown at once.
🔧 Requirements
- PHP ^8.4
- ext-fileinfo
- Symfony components ^7.4 or ^8.0
- Optional for Bedrock:
symfony/ai-bedrock-platform~0.13.0 andasync-aws/bedrock-runtime - PSR-18 HTTP Client (Symfony HTTP Client included)
- PSR-7 HTTP Messages (nyholm/psr7 included)
- PSR-3 Logger (optional)
🎵 Audio Features
use Lingoda\AiSdk\Audio\OpenAI\AudioOptions; // Text-to-Speech $options = AudioOptions::textToSpeech( model: AudioSpeechModel::TTS_1, voice: AudioSpeechVoice::NOVA, format: AudioSpeechFormat::MP3 ); $audioResult = $platform->textToSpeech('Hello world', $options); file_put_contents('speech.mp3', $audioResult->getContent()); // Speech-to-Text $transcription = $platform->transcribeAudio('audio.mp3', $transcriptionOptions); echo $transcription->getContent(); // "Hello world" // Translation to English $translation = $platform->translateAudio('spanish-audio.mp3', $translationOptions);
📦 Installation
composer require lingoda/ai-sdk
🤖 Supported Models
OpenAI Models:
- GPT-5 series:
gpt-5,gpt-5-mini,gpt-5-nano(latest) - GPT-4.1 series:
gpt-4.1,gpt-4.1-mini,gpt-4.1-nano(1M context) - GPT-4o series:
gpt-4o,gpt-4o-mini(128K context) - Audio models:
whisper-1,tts-1,tts-1-hd
Anthropic Models:
- Claude 4.1:
claude-opus-4-1-20250805 - Claude 4.0:
claude-opus-4,claude-sonnet-4 - Claude 3.7:
claude-3-7-sonnet - Claude 3.5:
claude-3-5-haiku
Google Gemini Models:
- Gemini 3.1:
gemini-3.1-flash-lite(1M context) - Gemini 2.5:
gemini-2.5-pro,gemini-2.5-flash(1M context)
AWS Bedrock Models (optional; ids are Bedrock base ids, sent through the region's eu./us. inference profile):
- Amazon Nova:
amazon.nova-2-lite-v1:0(default),amazon.nova-pro-v1:0,amazon.nova-lite-v1:0,amazon.nova-micro-v1:0(text only) - Claude:
anthropic.claude-haiku-4-5-20251001-v1:0,anthropic.claude-sonnet-4-5-20250929-v1:0,anthropic.claude-opus-4-5-20251101-v1:0,anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-opus-4-7,anthropic.claude-opus-4-8,anthropic.claude-sonnet-5,anthropic.claude-opus-5,anthropic.claude-opus-5-5
TypeSafe Jev (decisions, not chat, text only): jev-1.13.0 (default), jev-latest, jev-preview
🚦 Quick Test
Run interactive examples to test the SDK:
# Offline examples (no API keys needed) php docs/usage-example.php # With OpenAI OPENAI_API_KEY=your-key php docs/usage-example.php # Multiple providers OPENAI_API_KEY=sk-proj-... \ ANTHROPIC_API_KEY=sk-ant-... \ GEMINI_API_KEY=AIza... \ php docs/usage-example.php
🛠️ Development
# Install dependencies composer install # Run tests vendor/bin/phpunit # Static analysis vendor/bin/phpstan analyse # Code style vendor/bin/ecs check --fix
🤝 Contributing
- Fork the repository
- Create a feature branch
- Add tests for your changes
- Ensure all tests pass
- Submit a pull request
📄 License
MIT License. See LICENSE for details.
Get Started: Installation Guide | Try Examples: Interactive Examples | Join Discussion: GitHub Issues