lingoda/ai-bundle
Lingoda AI Bundle is a Symfony package that makes it easy to add AI-powered features to PHP applications. It provides configurable services and integration points for building intelligent workflows with minimal setup. Ideal for teams experimenting with AI in existing Symfony projects.
What you're actually looking at. lingoda/ai-bundle is a Symfony bundle that wraps the Lingoda AI PHP SDK (an AI provider abstraction — text generation, embeddings, etc.). The bundle's job is framework glue: service definitions, a configuration tree, probably an autowired client, and maybe profiler/console integration. Practically none of that glue is portable to Laravel, which has no bundle registration, no config/packages/*.yaml merge, and no compiler passes.
First decision: bundle or SDK? In a Laravel app, install the underlying SDK, not the bundle. The bundle is only worth pulling in if you're doing a hybrid or you want to read its DI wiring as a reference implementation for how to configure the SDK.
composer require lingoda/ai-php-sdk # the thing you actually want in Laravel
Where to look first, in this order:
README — provider list, config keys, and whether it pins an SDK major.<Vendor>/AIBundle/DependencyInjection/Configuration.php — this is the authoritative list of options (base URLs, API keys, default model, timeouts). Documentation usually lags this file.<Vendor>/AIBundle/DependencyInjection/*Extension.php — shows which services are registered, which are public, and what tags are used. This is your integration map.Client / provider classes — the things you'll actually bind into the Laravel container.First use case. Wire the SDK client as a singleton in a service provider and make one call from an artisan command, not from a controller:
// app/Providers/AiServiceProvider.php — illustrative; check README for real class names
public function register(): void
{
$this->app->singleton(AiClient::class, fn ($app) => new AiClient(
apiKey: config('services.ai.key'),
defaultModel: config('services.ai.model'),
));
}
Then php artisan tinker → resolve the client → send one prompt. Doing this from a command rather than an HTTP request keeps timeouts, output buffering, and streaming out of the picture while you verify credentials and provider reachability.
Expectation setting: 1 star, 0 dependents, one maintainer org. Treat this as an internal-style package, not a community-vetted one. Budget time to read the source.
Wrap the client in your own gateway. The single highest-leverage pattern with a low-adoption AI package: never inject the third-party client directly into controllers/jobs. Put an AiGateway interface in app/Contracts and a concrete implementation behind it. You get:
final class SdkAiGateway implements AiGateway
{
public function __construct(private AiClient $client) {}
public function complete(string $prompt, array $opts = []): string
{
return retry(3, fn () => $this->client->complete($prompt, $opts), 200, fn ($e) => $e instanceof RateLimited);
}
}
Push all calls into queued jobs. AI calls are 1–30s network-bound operations. In Laravel that's a job with $timeout, $tries, and backoff(). Use RateLimited middleware or Horizon rate limiting so a burst of dispatches doesn't 429 you. This is the biggest practical difference from the Symfony-bundle idiom, where you'd often reach for a messenger handler.
Cache aggressively on deterministic inputs. Embeddings are stable for a given (model, text) pair. Hash the input, cache the vector, store it alongside your records. Same for classification-style prompts with temperature: 0. This alone usually pays for the integration.
Config lives in config/services.php, not env() at call sites. The bundle's YAML tree maps cleanly onto a Laravel config array:
// config/services.php
'ai' => [
'key' => env('AI_API_KEY'),
'model' => env('AI_MODEL', 'default-from-readme'),
'timeout' => env('AI_TIMEOUT', 30),
],
Then read config('services.ai.*') everywhere. env() calls outside config files return null after php artisan config:cache — a classic post-deploy "works locally, broken in prod" trap.
Testing. Bind a fake gateway in TestCase::setUp() or use $this->instance(AiGateway::class, $fake). For contract tests against the real SDK, record provider responses to fixtures once and replay them — re-recording on every test run is slow and flaky.
Streaming. If the SDK supports it, stream from a controller via response()->stream() or an SSE endpoint. Keep the streaming path separate from the synchronous path; they have different timeout and error-recovery semantics (you can't retry mid-stream).
The bundle itself won't run in Laravel. Anything relying on Bundle registration, Extension::load(), tagged services, or compiler passes is dead weight. If you composer require it accidentally, you'll pull in Symfony DI components that conflict with or duplicate Laravel's container. Use the SDK.
Version pinning is non-negotiable. Check composer.json for the bundle→SDK version constraint and pin both exactly ("1.2.*" or a commit hash). With 0 dependents and 1 star, a patch release can quietly change a default model, timeout, or method signature and nothing else in the ecosystem will have caught it. If the SDK's public API looks unstable, vendor-fork it.
Read the Configuration.php before trusting the README. Options like a custom base URL (needed for proxies, Azure OpenAI, or self-hosted gateways) are frequently undocumented. Same for whether the API key is required at container-build time — if it is, config:cache in a build step without the secret will fail.
Timeouts are layered and all of them matter. SDK HTTP timeout, PHP max_execution_time, PHP-FPM request_terminate_timeout, nginx fastcgi_read_timeout, queue worker --timeout, and Horizon's timeout (which must be shorter than the worker's). A 60s generation with a 30s worker timeout kills the job after you've already paid for the tokens. Set the SDK timeout explicitly rather than relying on defaults.
Error taxonomy. Distinguish three classes and handle them differently: transport failures (retry, idempotent), rate limits / 429 (retry with exponential backoff + jitter, or requeue), and provider-side rejections like content policy or context-length overflow (do not retry — fail the job, log the prompt length). Blanket catch (\Throwable) + retry will burn quota on deterministic failures.
Cost and token visibility. Log prompt/completion token counts per call against a user/job identifier from day one. Retrofitting usage accounting after the first surprise invoice is painful, and the SDK may not surface usage stats on every provider.
Debugging the wire. Add Http::globalMiddleware(...) / a logging middleware on the underlying HTTP client, or use Laravel Telescope's HTTP client watcher, to see the actual request body. Most "the model ignores my instructions" bugs are a JSON-encoding or message-role mistake, not a model problem. If the bundle registers a Symfony profiler panel, that's a good reference for which fields it considers interesting.
Extension points worth knowing before you fork. The bundle's DI extension is the place custom providers or a decorated client would be registered. If you need multi-provider routing (cheap model for classification, expensive for generation), implement it in your AiGateway with two bound clients rather than fighting the bundle's single-default-provider assumption.
Verify, don't assume. Every class name and config key above should be confirmed against the repo's README and DependencyInjection/ directory before you commit to the integration — with a package this small, the source is the documentation.
How can I help you explore Laravel packages today?