Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ai Gemini Platform Laravel Package

symfony/ai-gemini-platform

Google Gemini platform bridge for Symfony AI, letting your PHP apps call Gemini's generateContent and streaming endpoints plus batchEmbedContents embeddings through a consistent Symfony AI interface.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up. This is a Symfony AI bridge, so you need the platform core alongside it:

composer require symfony/ai-platform symfony/ai-gemini-platform

Get a Gemini API key from Google AI Studio (not Vertex AI — different auth surface, different scope; see Gotchas). Put it in .env:

GEMINI_API_KEY=...

Register the platform once, as a singleton, in a dedicated provider so it's obvious where AI is bootstrapped:

php artisan make:provider AiServiceProvider
// app/Providers/AiServiceProvider.php
public function register(): void
{
    $this->app->singleton(PlatformInterface::class, function () {
        return GeminiPlatform::create(
            apiKey: config('services.gemini.key'),
        );
    });
}

Add services.gemini.key => env('GEMINI_API_KEY') to config/services.php. Don't hardcode the key; don't put the platform in a controller.

Where to look first. The README links are the primary source: the generateContent reference for the request/response shape, the embeddings reference for task types, and the text generation guide for multimodal examples. The Tests/Fixtures/ directory (image.jpg, audio.mp3, document.pdf, with licenses documented) is the fastest way to see the exact input structure this bridge expects for each media type.

First use case. Start with a single-turn text call from a service class, not a controller:

$messages = new MessageBag(
    new SystemMessage('You are a concise assistant.'),
    new UserMessage('Summarise this support thread: ' . $thread),
);

$result = $platform->invoke('gemini-3-flash-preview', $messages);
$text = $result->getResult()->getContent();

That's the whole loop: message bag in, text out. Everything else below is a variation on that.

Implementation Patterns

Wrap it, don't scatter it. The single highest-value pattern is putting the platform behind your own AiClient interface with a small number of domain-flavoured methods (summarise(), embed(), chatStream()). The bridge is pre-1.0 and swaps providers for a living; you want that swap to be one class, not a grep-and-replace across the app.

interface AiClient
{
    public function summarise(string $text): string;
    /** @return float[] */
    public function embedQuery(string $text): array;
    public function streamChat(MessageBag $messages): iterable;
}

Streaming, Laravel-style. The bridge emits semantic DeltaInterface deltas over SSE. Pipe them straight into a StreamedResponse:

public function stream(Request $request)
{
    return response()->stream(function () use ($request) {
        $bag = new MessageBag(
            new SystemMessage('...'),
            new UserMessage($request->string('prompt')),
        );
        foreach ($this->ai->stream('gemini-3-flash-preview', $bag) as $delta) {
            echo 'data: ' . json_encode(['text' => $delta->getText()]) . "\n\n";
            ob_flush(); flush();
        }
        echo "data: [DONE]\n\n";
    }, 200, [
        'Content-Type'  => 'text/event-stream',
        'Cache-Control' => 'no-cache',
        'X-Accel-Buffering' => 'no',
    ]);
}

Disable nginx buffering and PHP output buffering, or your "streaming" will arrive as one blob. On the JS side use EventSource or fetch with a reader; EventSource is GET-only, which usually forces a signed URL pattern.

Embeddings for RAG. Use batchEmbedContents in one round-trip for ingestion, and the query-time task type on the other side. The task type is not cosmetic — mixing RETRIEVAL_DOCUMENT and RETRIEVAL_QUERY matters for retrieval quality. Store vectors in whatever you already have (pgvector, Pinecone, whereVectorSimilarTo if you're on Postgres). Keep the embedding model in config next to the chat model; they version separately.

Multimodal ingestion. The bridge accepts image, audio and PDF bytes. For PDFs, chunk at the document level in your own pipeline rather than relying on the model to page through; it's cheaper and produces better citations. Validate MIME and size in a FormRequest before the bytes ever reach the platform — Gemini's limits are the outer bound, not your inner one.

Tool calling. Define tools as Symfony AI tool objects, keep schemas narrow (Gemini's schema validation is stricter than OpenAI's), and always handle the "model returned an empty tool-call roundtrip" case — v0.11.0 made the bridge tolerant of it, but your handler still needs to produce a follow-up message, or the conversation silently stalls.

Cost tracking — do this from day one. Since v0.14.0, TokenUsage carries the model identifier. Log a row per call:

$usage = $result->getTokenUsage();
AiUsage::create([
    'model'        => $usage->getModel(),
    'input_tokens' => $usage->getPromptTokens(),
    'output_tokens'=> $usage->getCompletionTokens(),
    'user_id'      => auth()->id(),
    'feature'      => 'summarise',
    'created_at'   => now(),
]);

Join that to a rate table you own (gemini-3-flash-preview input rate, output rate, effective-from date) and you have per-feature, per-tenant margin reporting. The package gives you usage, not pricing — the rate table, the aggregation job, and the showback report are yours.

Queue it. There is no async story in this bridge. Any non-interactive call should be a ShouldQueue job with $tries, $backoff and a failed() handler. For fan-out (embed 500 documents), use Bus::batch() with a rate-limit middleware — Gemini will 429 you hard otherwise.

Testing. Inject a fake PlatformInterface or a fake AiClient in tests — never hit the API in CI. Keep a handful of VCR-style recorded fixtures for the bridge layer itself, and use Tests/Fixtures as reference shape for media payloads.

Gotchas and Tips

Pin the version and read the changelog before bumping. This is v0.14, pre-1.0. Minor releases carry behavioural fixes: v0.9.0 changed streaming framing to SSE, v0.10.0 started throwing ModelNotFoundException on 404, v0.11.0 changed finish_reason normalisation and fixed thinking/multi-part stream crashes, v0.14.0 added model to TokenUsage. Use "0.14.*", not ^0.14, and treat every upgrade as a small project.

Gemini API ≠ Vertex AI. This bridge targets the Gemini API surface (API key auth, Google AI Studio). If you need GCP data residency, GCP billing controls, or VPC-SC, that's the Vertex path — verify separately. Do not assume this package gives you enterprise compliance.

Model IDs drift. The catalog is refreshed automatically each release. Hardcoding gemini-3.1-pro-preview works until it's retired. Catch ModelNotFoundException at the boundary and fall back to a catalogued default from config rather than 500-ing.

Retry transient failures, don't retry everything. v0.11.0 added a retryable ServerException for transient 5xx. Catch that specifically and retry — do not blanket-retry ModelNotFoundException (won't help) or ExceedContextSizeException (won't fit).

Surface rate-limit messages. RateLimitExceededException now carries the API message. Log it. When you're tuning concurrency, the difference between a per-minute token quota and a per-minute request quota is in that string.

Context-size failures are your chunker's fault, not the model's. Catch ExceedContextSizeException, chunk, and retry once on a larger model — then fail. Silent truncation is worse than a clear error.

Tool-schema quirks are load-bearing. The bridge strips the JSON-Schema $schema meta-key and wraps function_response in a Protobuf Struct — both were bugs that got fixed, both mean schemas you hand to tools need testing against the current version, not written once and forgotten.

UTF-8 handling. v0.10.0 replaces malformed UTF-8 in request bodies rather than failing. That's convenient, but it means bad input silently becomes slightly different input. Sanitise at your boundary if the content matters (e.g. legal text), or at least log when replacement fires.

Nothing is cached. Gemini charges for identical prompts. Hash the prompt + model + parameters and use Laravel's cache for deterministic calls. This is often the biggest single cost lever, and it's entirely your responsibility.

Wrap it, pin it, smoke-test it. Five stars and zero dependents is real information: no external battle-testing beyond Symfony's own use. The maintainers are recognisable Symfony core contributors and the changelog is dense with small correctness fixes (a healthy sign), but the org's reputation is underwriting this, not its adoption. Keep it behind your AiClient interface, keep a smoke test per capability (text, stream, embed, tool call, multimodal), and make the swap to a different bridge or the raw Gemini REST API a one-file change.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo