Sidekick v2.1
Sidekick Logo

Sidekick v2.1

A fluent Laravel package for integrating with OpenAI, Anthropic Claude, Mistral, and Cohere AI services.

Builder API • Typed Responses • Streaming • Tool Calling • Structured Output • Conversations • Image Generation • TTS • Transcription • Embeddings • Moderation • RAG • Chat Widget • Testing

Laravel 10/11/12 PHP 8.2+ Latest Version Packagist Tests

# Requirements

  • PHP 8.2 or higher
  • Laravel 10, 11, or 12

# Installation

Install via Composer then run the install command:

composer require paparascaldev/sidekick

php artisan sidekick:install

This publishes the config file and runs migrations. Alternatively, install manually:

composer require paparascaldev/sidekick

php artisan vendor:publish --tag=sidekick-config

php artisan migrate

# Configuration

Add your API keys to .env:

SIDEKICK_OPENAI_TOKEN=your-openai-key
SIDEKICK_CLAUDE_TOKEN=your-anthropic-key
SIDEKICK_MISTRAL_TOKEN=your-mistral-key
SIDEKICK_COHERE_TOKEN=your-cohere-key

The published config file (config/sidekick.php) looks like this:

return [
    // Default provider when none specified
    'default' => env('SIDEKICK_DEFAULT_PROVIDER', 'openai'),

    // Default provider + model per capability
    'defaults' => [
        'text'          => ['provider' => 'openai', 'model' => 'gpt-4o'],
        'image'         => ['provider' => 'openai', 'model' => 'dall-e-3'],
        'audio'         => ['provider' => 'openai', 'model' => 'tts-1'],
        'transcription' => ['provider' => 'openai', 'model' => 'whisper-1'],
        'embedding'     => ['provider' => 'openai', 'model' => 'text-embedding-3-small'],
        'moderation'    => ['provider' => 'openai', 'model' => 'text-moderation-latest'],
    ],

    // Provider API keys and base URLs
    'providers' => [
        'openai'    => ['api_key' => env('SIDEKICK_OPENAI_TOKEN'), 'base_url' => '...'],
        'anthropic' => ['api_key' => env('SIDEKICK_CLAUDE_TOKEN'), 'base_url' => '...'],
        'mistral'   => ['api_key' => env('SIDEKICK_MISTRAL_TOKEN'), 'base_url' => '...'],
        'cohere'    => ['api_key' => env('SIDEKICK_COHERE_TOKEN'), 'base_url' => '...'],
    ],

    // Register custom providers
    'custom_providers' => [],

    // Chat widget settings
    'widget' => [
        'enabled'       => env('SIDEKICK_WIDGET_ENABLED', false),
        'route_prefix'  => 'sidekick',
        'middleware'     => ['web'],
        'provider'      => env('SIDEKICK_WIDGET_PROVIDER', 'openai'),
        'model'         => env('SIDEKICK_WIDGET_MODEL', 'gpt-4o'),
        'system_prompt' => env('SIDEKICK_WIDGET_SYSTEM_PROMPT', 'You are a helpful assistant.'),
        'max_tokens'    => 1024,
    ],

    // HTTP timeout and retry settings
    'http' => [
        'timeout'         => env('SIDEKICK_HTTP_TIMEOUT', 30),
        'connect_timeout' => env('SIDEKICK_HTTP_CONNECT_TIMEOUT', 10),
        'retry'           => ['times' => 0, 'sleep' => 100],
    ],
];

# Quick Start

Use the Facade or the global helper:

use PapaRascalDev\Sidekick\Facades\Sidekick;

// Using the facade
$response = Sidekick::text()->withPrompt('Hello')->generate();

// Using the helper function
$response = sidekick()->text()->withPrompt('Hello')->generate();

# Text Generation

$response = Sidekick::text()
    ->using('openai', 'gpt-4o')
    ->withSystemPrompt('You are a helpful assistant.')
    ->withPrompt('What is Laravel?')
    ->generate();

echo $response->text;                // "Laravel is a PHP web framework..."
echo $response->usage->totalTokens;  // 150
echo $response->meta->latencyMs;     // 523.4
echo $response->finishReason;        // "stop"
echo (string) $response;             // Same as $response->text

TextBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withPrompt(string $prompt)Add a user message
withSystemPrompt(string $prompt)Set the system prompt
withMessages(array $messages)Set the full message history
addMessage(Role $role, string $content)Append a single message with a specific role
withMaxTokens(int $maxTokens)Set max tokens (default: 1024)
withTemperature(float $temp)Set temperature (default: 1.0)
generate(): TextResponseExecute and return a TextResponse
stream(): StreamResponseExecute and return a streamable StreamResponse

# Streaming

$stream = Sidekick::text()
    ->using('anthropic', 'claude-sonnet-4-20250514')
    ->withPrompt('Write a haiku about coding')
    ->stream();

// Iterate over chunks
foreach ($stream as $chunk) {
    echo $chunk;
}

// Get the full buffered text after iteration
$fullText = $stream->text();

// Or return as an SSE response from a controller
return $stream->toResponse();

# Tool Calling

Give the model tools it can call, and Sidekick runs them for you. Define a tool with a name, a description, a JSON Schema for its arguments, and a handler. Sidekick sends the tool to the model, executes your handler when the model asks for it, feeds the result back, and loops until the model returns a final answer. Supported on OpenAI, Anthropic, and Mistral.

use PapaRascalDev\Sidekick\Facades\Sidekick;
use PapaRascalDev\Sidekick\ValueObjects\Tool;

$response = Sidekick::text()
    ->using('openai', 'gpt-4o')
    ->withPrompt('What is the weather in London?')
    ->withTools([
        Tool::make(
            name: 'get_weather',
            description: 'Get the current weather for a city.',
            parameters: [
                'type' => 'object',
                'properties' => [
                    'city' => ['type' => 'string', 'description' => 'The city name'],
                ],
                'required' => ['city'],
            ],
            handler: fn (array $args) => "Sunny, 22C in {$args['city']}",
        ),
    ])
    ->generate();

echo $response->text; // "It is currently sunny and 22C in London."

Manual handling

If a tool has no handler, Sidekick stops after the model asks for it and hands the calls back to you to run yourself:

$response = Sidekick::text()
    ->using('openai', 'gpt-4o')
    ->withPrompt('What is the weather in London?')
    ->withTools([
        Tool::make('get_weather', 'Get the current weather for a city.', [
            'type' => 'object',
            'properties' => ['city' => ['type' => 'string']],
        ]),
    ])
    ->generate();

if ($response->hasToolCalls()) {
    foreach ($response->toolCalls as $call) {
        echo $call->name;               // "get_weather"
        echo $call->arguments['city'];  // "London"
        echo $call->id;                 // provider tool call id
    }
}

Tool Methods

MethodDescription
withTools(array $tools)Add tools (Tool objects or arrays)
withTool(Tool $tool)Add a single tool
withMaxToolCalls(int $max)Max automatic tool rounds (default: 5)

Tool::make() accepts name, description, parameters (a JSON Schema object), and an optional handler callable. Cohere does not support tool calling yet and throws a clear exception if tools are passed.

# Structured Output

Get typed, validated JSON back from the model instead of free text. Pass a JSON Schema and Sidekick decodes the response onto TextResponse::$structured. Supported on OpenAI, Anthropic, and Mistral.

$response = Sidekick::text()
    ->using('openai', 'gpt-4o')
    ->withPrompt('Extract the person: John Doe, age 42, lives in London.')
    ->withSchema([
        'type' => 'object',
        'properties' => [
            'name' => ['type' => 'string'],
            'age'  => ['type' => 'integer'],
            'city' => ['type' => 'string'],
        ],
        'required' => ['name', 'age', 'city'],
        'additionalProperties' => false,
    ])
    ->generate();

$response->structured;      // ['name' => 'John Doe', 'age' => 42, 'city' => 'London']
$response->hasStructured(); // true

By default Sidekick asks for a strict schema (name response, strict true). On OpenAI and Mistral, strict mode requires every object to set additionalProperties: false and to list all of its keys in required. Pass a custom name or relax strict mode when you need to:

use PapaRascalDev\Sidekick\ValueObjects\Schema;

// Inline
->withSchema($schema, name: 'person', strict: false)

// Or with a Schema object
->withSchema(Schema::make($schema, name: 'person', strict: false))

OpenAI and Mistral use the native json_schema response format. Anthropic has no native equivalent, so Sidekick forces a single tool whose input matches your schema and reads the structured result back from it. Either way you get the same decoded array on $response->structured. Cohere is not supported yet and throws a clear exception.

Structured Methods

MethodDescription
withSchema(array|Schema $schema, string $name = 'response', bool $strict = true)Ask the model for JSON matching a JSON Schema

# Conversations

Database-backed conversations with automatic message persistence:

// Start a conversation
$convo = Sidekick::conversation()
    ->using('openai', 'gpt-4o')
    ->withSystemPrompt('You are a travel advisor.')
    ->withMaxTokens(2048)
    ->begin();

$response = $convo->send('I want to visit Japan.');
echo $response->text;

// Get the conversation ID to resume later
$conversationId = $convo->getConversation()->id;

// Resume later
$convo = Sidekick::conversation()->resume($conversationId);
$response = $convo->send('What about accommodation?');

// Delete a conversation
$convo->delete();

ConversationBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withSystemPrompt(string $prompt)Set the system prompt
withMaxTokens(int $maxTokens)Set max tokens (default: 1024)
begin(): selfStart a new conversation (persisted to DB)
resume(string $id): selfResume an existing conversation by UUID
send(string $message): TextResponseSend a message and get a response
delete(): boolDelete the conversation and its messages
getConversation(): ?ConversationGet the underlying Eloquent model

# Image Generation

$response = Sidekick::image()
    ->using('openai', 'dall-e-3')
    ->withPrompt('A sunset over mountains')
    ->withSize('1024x1024')
    ->withQuality('hd')
    ->count(2)
    ->generate();

echo $response->url();           // First image URL
echo $response->urls;            // Array of all URLs
echo $response->revisedPrompt;   // DALL-E's revised prompt (if any)

ImageBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withPrompt(string $prompt)Set the image prompt
withSize(string $size)Set dimensions (default: 1024x1024)
withQuality(string $quality)Set quality: standard or hd
count(int $count)Number of images to generate (default: 1)
generate(): ImageResponseExecute and return an ImageResponse

# Audio (Text-to-Speech)

$response = Sidekick::audio()
    ->using('openai', 'tts-1')
    ->withText('Hello, welcome to Sidekick!')
    ->withVoice('nova')
    ->withFormat('mp3')
    ->generate();

$response->save('audio/welcome.mp3');       // Save to default disk
$response->save('audio/welcome.mp3', 's3'); // Save to specific disk
echo $response->format;                     // "mp3"

AudioBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withText(string $text)Set the text to speak
withVoice(string $voice)Set the voice (default: alloy)
withFormat(string $format)Set audio format (default: mp3)
generate(): AudioResponseExecute and return an AudioResponse

# Transcription

$response = Sidekick::transcription()
    ->using('openai', 'whisper-1')
    ->withFile('/path/to/audio.mp3')
    ->withLanguage('en')
    ->generate();

echo $response->text;       // Transcribed text
echo $response->language;   // "en"
echo $response->duration;   // Duration in seconds
echo (string) $response;    // Same as $response->text

TranscriptionBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withFile(string $filePath)Path to the audio file
withLanguage(string $language)Hint the language (optional)
generate(): TranscriptionResponseExecute and return a TranscriptionResponse

# Embeddings

$response = Sidekick::embedding()
    ->using('openai', 'text-embedding-3-small')
    ->withInput('Laravel is a great framework')
    ->generate();

$vector = $response->vector();      // First embedding vector (array of floats)
$all = $response->embeddings;       // All embedding vectors
echo $response->usage->totalTokens; // Token usage

EmbeddingBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withInput(string|array $input)Text or array of texts to embed
generate(): EmbeddingResponseExecute and return an EmbeddingResponse

# Moderation

$response = Sidekick::moderation()
    ->using('openai', 'text-moderation-latest')
    ->withContent('Some text to moderate')
    ->generate();

if ($response->isFlagged()) {
    // Content was flagged
}

if ($response->isFlaggedFor('violence')) {
    // Specifically flagged for violence
}

// Inspect all categories
foreach ($response->categories as $category) {
    echo "{$category->category}: flagged={$category->flagged}, score={$category->score}\n";
}

ModerationBuilder Methods

MethodDescription
using(string $provider, ?string $model)Set provider and model
withContent(string $content)Text to moderate
generate(): ModerationResponseExecute and return a ModerationResponse

# Utility Methods

Convenient shorthand methods on the facade:

// Summarize text (returns string)
$summary = Sidekick::summarize('Long text here...', maxLength: 500);

// Translate text (returns string)
$translated = Sidekick::translate('Hello', 'French');

// Extract keywords (returns string, comma-separated)
$keywords = Sidekick::extractKeywords('Some article text...');

# Knowledge Base / RAG

Sidekick includes a built-in Retrieval-Augmented Generation system with vector search, chunking, and knowledge base management.

Configuration

RAG settings in config/sidekick.php:

'knowledge' => [
    'embedding' => ['provider' => 'openai', 'model' => 'text-embedding-3-small'],
    'chunking'  => ['chunk_size' => 2000, 'overlap' => 200],
    'search'    => ['default_limit' => 5, 'min_score' => 0.3, 'driver' => VectorSearch::class],
],

Ingesting Content

use PapaRascalDev\Sidekick\Facades\Sidekick;

// Ingest text
Sidekick::knowledge('my-kb')
    ->ingest('Our return policy is 30 days with receipt.', 'faq');

// Ingest a file
Sidekick::knowledge('my-kb')
    ->ingestFile('/path/to/faq.md');

// Ingest multiple texts
Sidekick::knowledge('my-kb')
    ->ingestMany(['Text one...', 'Text two...'], 'bulk-source');

// Use a different embedding provider
Sidekick::knowledge('my-kb')
    ->using('mistral', 'mistral-embed')
    ->ingest('Content here...');

Artisan Commands

# Ingest a file
php artisan sidekick:ingest my-kb --file=/path/to/faq.md

# Ingest inline text
php artisan sidekick:ingest my-kb --text="Return policy is 30 days."

# Ingest a directory of .txt/.md/.html/.csv files
php artisan sidekick:ingest my-kb --dir=/path/to/docs

# Purge and re-ingest
php artisan sidekick:ingest my-kb --purge --dir=/path/to/docs

Searching

$results = Sidekick::knowledge('my-kb')->search('What is your return policy?');

foreach ($results as $chunk) {
    echo $chunk->content;       // The text content
    echo $chunk->similarity;    // Cosine similarity score
    echo $chunk->source;        // Source label
}

Ask (Search + Generate)

One-liner: search the knowledge base and generate a grounded answer:

$answer = Sidekick::knowledge('my-kb')->ask('What is your return policy?');

Widget Integration

Connect a knowledge base to the chat widget:

SIDEKICK_WIDGET_ENABLED=true
SIDEKICK_WIDGET_KNOWLEDGE_BASE=my-kb

Or in config/sidekick.php:

'widget' => [
    'knowledge_base'     => 'my-kb',
    'rag_context_chunks' => 5,
    'rag_min_score'      => 0.3,
],

When configured, the widget automatically searches the knowledge base for each user message and injects relevant context into the system prompt. If RAG fails, it falls back gracefully to the original system prompt.

Custom Search Drivers

use PapaRascalDev\Sidekick\Contracts\SearchesKnowledge;

class PgVectorSearch implements SearchesKnowledge
{
    public function search(KnowledgeBase $kb, array $queryEmbedding, int $limit = 5, float $minScore = 0.3): Collection
    {
        // Your pgvector implementation
    }
}

// Register in config/sidekick.php
'knowledge' => [
    'search' => ['driver' => \App\Search\PgVectorSearch::class],
],

Managing Knowledge Bases

$kb = Sidekick::knowledge('my-kb');

$kb->chunkCount();           // Number of chunks stored
$kb->purge();                // Delete all chunks
$kb->getKnowledgeBase();     // Get the Eloquent model

# Chat Widget

Sidekick ships with a drop-in Alpine.js chat widget. Enable it in your .env:

SIDEKICK_WIDGET_ENABLED=true
SIDEKICK_WIDGET_PROVIDER=openai
SIDEKICK_WIDGET_MODEL=gpt-4o
SIDEKICK_WIDGET_SYSTEM_PROMPT="You are a helpful assistant."

Blade Component

<x-sidekick::chat-widget
    position="bottom-right"
    theme="dark"
    title="AI Assistant"
    placeholder="Ask me anything..."
    button-label="Chat"
/>

Widget Props

PropDefaultOptions
positionbottom-rightbottom-right, bottom-left, top-right, top-left
themelightlight, dark
titleChat AssistantAny string
placeholderType a message...Any string
button-labelChatAny string

Prerequisites

The widget requires Alpine.js and a CSRF meta tag:

<meta name="csrf-token" content="{{ csrf_token() }}">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

# Custom Providers

Register your own AI providers at runtime or via config:

// Runtime registration
Sidekick::registerProvider('ollama', function ($app) {
    return new OllamaProvider(config('sidekick.providers.ollama'));
});

// Or in config/sidekick.php
'custom_providers' => [
    'ollama' => \App\Sidekick\OllamaProvider::class,
],

Custom providers should implement ProviderContract and the relevant capability interfaces: ProvidesText, ProvidesImages, ProvidesAudio, ProvidesTranscription, ProvidesEmbeddings, ProvidesModeration.

# Testing

Sidekick ships with first-class testing support via Sidekick::fake():

use PapaRascalDev\Sidekick\Facades\Sidekick;
use PapaRascalDev\Sidekick\Responses\TextResponse;
use PapaRascalDev\Sidekick\ValueObjects\Meta;
use PapaRascalDev\Sidekick\ValueObjects\Usage;

public function test_my_feature(): void
{
    $fake = Sidekick::fake([
        new TextResponse(
            text: 'Mocked response',
            usage: new Usage(10, 20, 30),
            meta: new Meta('openai', 'gpt-4o'),
        ),
    ]);

    // ... run your code that uses Sidekick ...

    $fake->assertTextGenerated();         // At least one text generation
    $fake->assertTextGenerated(2);        // Exactly 2 text generations
    $fake->assertNothingSent();           // No API calls were made
    $fake->assertProviderUsed('openai');
    $fake->assertModelUsed('gpt-4o');
    $fake->assertPromptContains('expected text');
}

# Events

All events are in the PapaRascalDev\Sidekick\Events namespace:

EventPropertiesWhen
RequestSendingprovider, model, capabilityBefore a request is sent
ResponseReceivedprovider, model, capability, responseAfter a successful response
StreamChunkReceivedprovider, model, chunkFor each streaming chunk
RequestFailedprovider, model, capability, exceptionWhen a request fails

# Provider Capabilities

Capability OpenAI Anthropic Mistral Cohere
TextYesYesYesYes
Tool callingYesYesYes-
Structured outputYesYesYes-
ImageYes---
Audio (TTS)Yes---
TranscriptionYes---
EmbeddingYes-Yes-
ModerationYes---

# Upgrading from v1

Sidekick v2.0 is a complete rewrite with breaking changes. Follow these steps to upgrade.

1. Requirements

  • PHP 8.2+ (was 8.0+)
  • Laravel 10+ (dropped Laravel 9 support)

2. Update Composer

composer require paparascaldev/sidekick:^2.0

3. Publish the new config

php artisan vendor:publish --tag=sidekick-config --force

4. Update .env

SIDEKICK_OPENAI_TOKEN=your-key
SIDEKICK_CLAUDE_TOKEN=your-key
SIDEKICK_MISTRAL_TOKEN=your-key
SIDEKICK_COHERE_TOKEN=your-key

5. Run migrations

php artisan migrate
Note: The conversations table has been updated. The class column is now provider, and max_tokens changed from bigInteger to unsignedInteger. Existing conversation data is not automatically migrated — back it up before running migrations.

6. Update your code

Text generation

// v1
$driver = new OpenAi();
$sidekick = Sidekick::create($driver);
$response = $sidekick->complete('gpt-4', 'system prompt', 'user message', [], 1024);

// v2
use PapaRascalDev\Sidekick\Facades\Sidekick;
$response = Sidekick::text()
    ->using('openai', 'gpt-4o')
    ->withSystemPrompt('system prompt')
    ->withPrompt('user message')
    ->generate();

echo $response->text;  // Typed DTO instead of raw array

Conversations

// v1
$convo = new SidekickConversation();
$convo->begin(new OpenAi(), 'gpt-4', 'System prompt');
$response = $convo->sendMessage('Hello');

// v2
$convo = Sidekick::conversation()
    ->using('openai', 'gpt-4o')
    ->withSystemPrompt('System prompt')
    ->begin();

$response = $convo->send('Hello');
echo $response->text;

Image generation

// v1
$sidekick = Sidekick::create(new OpenAi());
$response = $sidekick->image()->generate('dall-e-3', 'A sunset');

// v2
$response = Sidekick::image()
    ->using('openai', 'dall-e-3')
    ->withPrompt('A sunset')
    ->generate();

echo $response->url();

Audio generation

// v1
$sidekick = Sidekick::create(new OpenAi());
$response = $sidekick->audio()->fromText('tts-1', 'Hello');

// v2
$response = Sidekick::audio()
    ->using('openai', 'tts-1')
    ->withText('Hello')
    ->generate();

$response->save('audio/hello.mp3');

Utilities

// v1
$utils = sidekickUtilities(new OpenAi());
$summary = $utils->summarize('Long text...');

// v2
$summary = Sidekick::summarize('Long text...');
$translated = Sidekick::translate('Hello', 'French');
$keywords = Sidekick::extractKeywords('Some text');

Helper functions

// v1
$driver = sidekick(new OpenAi());
$convo = sidekickConversation();
$utils = sidekickUtilities(new OpenAi());

// v2
$manager = sidekick(); // Returns SidekickManager
$manager->text()->using('openai', 'gpt-4o')->withPrompt('Hi')->generate();

7. Remove old playground

If you previously installed the v1 playground, remove these files:

  • resources/views/Pages/Sidekick/
  • resources/views/Components/Sidekick/
  • routes/web.sidekick.php
  • Remove require base_path('routes/web.sidekick.php'); from routes/web.php

The v2 chat widget replaces the playground.

Removed Classes

v1 Classv2 Replacement
Sidekick (static factory)SidekickManager via facade
SidekickConversationConversationBuilder
SidekickDriverInterfaceProviderContract + capability interfaces
Drivers\OpenAiProviders\OpenAiProvider
Drivers\ClaudeProviders\AnthropicProvider
Drivers\MistralProviders\MistralProvider
Drivers\CohereProviders\CohereProvider
Features\CompletionIntegrated into providers
Features\ImageImageBuilder
Features\AudioAudioBuilder
Features\EmbeddingEmbeddingBuilder
Features\ModerateModerationBuilder
Features\TranscribeTranscriptionBuilder
Utilities\UtilitiesSidekickManager utility methods
Facades\SidekickConversationUse Sidekick::conversation()
Models\SidekickConversationModels\Conversation
Models\SidekickConversationMessageModels\ConversationMessage

New in v2

  • Fluent builder API for all capabilities
  • Typed readonly response DTOs
  • Streaming support with SSE
  • Event system (RequestSending, ResponseReceived, etc.)
  • Sidekick::fake() for testing
  • Alpine.js chat widget
  • Custom provider registration
  • Knowledge Base / RAG with vector search
  • Publishable config file