{"id":15175476,"url":"https://github.com/paparascal2020/sidekick","last_synced_at":"2026-02-17T21:05:26.648Z","repository":{"id":255437459,"uuid":"850344872","full_name":"PapaRascal2020/sidekick","owner":"PapaRascal2020","description":"Say hello to Sidekick! A Laravel package that provides a common syntax for using Claude, Mistral, Cohere and OpenAi APIs.","archived":false,"fork":false,"pushed_at":"2025-07-02T09:23:15.000Z","size":6011,"stargazers_count":26,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-28T19:56:44.201Z","etag":null,"topics":["ai","claude-ai","cohere","laravel-package","mistralai","open-source","openai"],"latest_commit_sha":null,"homepage":"https://sidekickforlaravel.com","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PapaRascal2020.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-08-31T14:12:31.000Z","updated_at":"2025-07-02T09:23:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"825c6f63-91ff-40cb-b81e-704ce8e06fe4","html_url":"https://github.com/PapaRascal2020/sidekick","commit_stats":null,"previous_names":["paparascal2020/sidekick"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/PapaRascal2020/sidekick","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaRascal2020%2Fsidekick","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaRascal2020%2Fsidekick/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaRascal2020%2Fsidekick/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaRascal2020%2Fsidekick/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PapaRascal2020","download_url":"https://codeload.github.com/PapaRascal2020/sidekick/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PapaRascal2020%2Fsidekick/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29558101,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T20:52:40.164Z","status":"ssl_error","status_checked_at":"2026-02-17T20:48:10.325Z","response_time":100,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","claude-ai","cohere","laravel-package","mistralai","open-source","openai"],"created_at":"2024-09-27T12:39:14.908Z","updated_at":"2026-02-17T21:05:26.637Z","avatar_url":"https://github.com/PapaRascal2020.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\u003cp align=\"center\"\u003e\n    \u003ca href=\"https://laravel.com\"\u003e\u003cimg alt=\"Laravel Package\" src=\"https://img.shields.io/badge/Laravel-10%2F11%2F12-red?logo=laravel\u0026logoColor=white\"/\u003e\u003c/a\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\n    \u003cimg alt=\"PHP\" src=\"https://img.shields.io/badge/PHP-8.2%2B-blue?logo=php\u0026logoColor=white\"/\u003e \u0026nbsp;\u0026nbsp;\u0026nbsp;\n    \u003cimg alt=\"Latest Version\" src=\"https://img.shields.io/packagist/v/paparascaldev/sidekick?label=Latest\"/\u003e \u0026nbsp;\u0026nbsp;\n    \u003ca href=\"https://packagist.org/packages/paparascaldev/sidekick\"\u003e\u003cimg alt=\"Packagist\" src=\"https://img.shields.io/badge/Packagist-F28D1A?logo=Packagist\u0026logoColor=white\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://hopeful-mist.lon1.cdn.digitaloceanspaces.com/sidekick_new.png\" alt=\"Sidekick\" /\u003e\n\u003c/p\u003e\n\n# Sidekick v2.0\n\nA fluent Laravel package for integrating with **OpenAI**, **Anthropic Claude**, **Mistral**, and **Cohere** AI services. Features a modern builder API, typed responses, streaming support, database-backed conversations, an embeddable chat widget, and first-class testing support.\n\n## Requirements\n\n- PHP 8.2+\n- Laravel 10, 11, or 12\n\n## Installation\n\n```bash\ncomposer require paparascaldev/sidekick\nphp artisan sidekick:install\n```\n\nOr install manually:\n\n```bash\ncomposer require paparascaldev/sidekick\nphp artisan vendor:publish --tag=sidekick-config\nphp artisan migrate\n```\n\n## Configuration\n\nAdd your API keys to `.env` (only add the providers you use):\n\n```dotenv\nSIDEKICK_OPENAI_TOKEN=your-openai-key\nSIDEKICK_CLAUDE_TOKEN=your-anthropic-key\nSIDEKICK_MISTRAL_TOKEN=your-mistral-key\nSIDEKICK_COHERE_TOKEN=your-cohere-key\n```\n\nPublish and customize the config file:\n\n```bash\nphp artisan vendor:publish --tag=sidekick-config\n```\n\n### Config File Overview\n\nThe `config/sidekick.php` file includes:\n\n```php\nreturn [\n    // Default provider when none specified\n    'default' =\u003e env('SIDEKICK_DEFAULT_PROVIDER', 'openai'),\n\n    // Default provider + model per capability\n    'defaults' =\u003e [\n        'text'          =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'gpt-4o'],\n        'image'         =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'dall-e-3'],\n        'audio'         =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'tts-1'],\n        'transcription' =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'whisper-1'],\n        'embedding'     =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'text-embedding-3-small'],\n        'moderation'    =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'text-moderation-latest'],\n    ],\n\n    // Provider API keys and base URLs\n    'providers' =\u003e [\n        'openai'    =\u003e ['api_key' =\u003e env('SIDEKICK_OPENAI_TOKEN'), 'base_url' =\u003e '...'],\n        'anthropic' =\u003e ['api_key' =\u003e env('SIDEKICK_CLAUDE_TOKEN'), 'base_url' =\u003e '...'],\n        'mistral'   =\u003e ['api_key' =\u003e env('SIDEKICK_MISTRAL_TOKEN'), 'base_url' =\u003e '...'],\n        'cohere'    =\u003e ['api_key' =\u003e env('SIDEKICK_COHERE_TOKEN'), 'base_url' =\u003e '...'],\n    ],\n\n    // Register custom providers\n    'custom_providers' =\u003e [],\n\n    // Chat widget settings\n    'widget' =\u003e [\n        'enabled'       =\u003e env('SIDEKICK_WIDGET_ENABLED', false),\n        'route_prefix'  =\u003e 'sidekick',\n        'middleware'     =\u003e ['web'],\n        'provider'      =\u003e env('SIDEKICK_WIDGET_PROVIDER', 'openai'),\n        'model'         =\u003e env('SIDEKICK_WIDGET_MODEL', 'gpt-4o'),\n        'system_prompt' =\u003e env('SIDEKICK_WIDGET_SYSTEM_PROMPT', 'You are a helpful assistant.'),\n        'max_tokens'    =\u003e 1024,\n    ],\n\n    // HTTP timeout and retry settings\n    'http' =\u003e [\n        'timeout'         =\u003e env('SIDEKICK_HTTP_TIMEOUT', 30),\n        'connect_timeout' =\u003e env('SIDEKICK_HTTP_CONNECT_TIMEOUT', 10),\n        'retry'           =\u003e ['times' =\u003e 0, 'sleep' =\u003e 100],\n    ],\n];\n```\n\n## Quick Start\n\nYou can use the `Sidekick` facade or the `sidekick()` helper function:\n\n```php\nuse PapaRascalDev\\Sidekick\\Facades\\Sidekick;\n\n// Using the facade\n$response = Sidekick::text()-\u003ewithPrompt('Hello')-\u003egenerate();\n\n// Using the helper function\n$response = sidekick()-\u003etext()-\u003ewithPrompt('Hello')-\u003egenerate();\n```\n\n### Text Generation\n\n```php\n$response = Sidekick::text()\n    -\u003eusing('openai', 'gpt-4o')\n    -\u003ewithSystemPrompt('You are a helpful assistant.')\n    -\u003ewithPrompt('What is Laravel?')\n    -\u003egenerate();\n\necho $response-\u003etext;                // \"Laravel is a PHP web framework...\"\necho $response-\u003eusage-\u003etotalTokens;  // 150\necho $response-\u003emeta-\u003elatencyMs;     // 523.4\necho $response-\u003efinishReason;        // \"stop\"\necho (string) $response;             // Same as $response-\u003etext\n```\n\n**All TextBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withPrompt(string $prompt)` | Add a user message |\n| `withSystemPrompt(string $prompt)` | Set the system prompt |\n| `withMessages(array $messages)` | Set the full message history (array of `Message` objects or arrays) |\n| `addMessage(Role $role, string $content)` | Append a single message with a specific role |\n| `withMaxTokens(int $maxTokens)` | Set max tokens (default: 1024) |\n| `withTemperature(float $temp)` | Set temperature (default: 1.0) |\n| `generate(): TextResponse` | Execute and return a `TextResponse` |\n| `stream(): StreamResponse` | Execute and return a streamable `StreamResponse` |\n\n### Streaming\n\n```php\n$stream = Sidekick::text()\n    -\u003eusing('anthropic', 'claude-sonnet-4-20250514')\n    -\u003ewithPrompt('Write a haiku about coding')\n    -\u003estream();\n\n// Iterate over chunks\nforeach ($stream as $chunk) {\n    echo $chunk;\n}\n\n// Get the full buffered text after iteration\n$fullText = $stream-\u003etext();\n\n// Or return as an SSE response from a controller\nreturn $stream-\u003etoResponse();\n```\n\n### Conversations (with DB persistence)\n\n```php\n// Start a conversation\n$convo = Sidekick::conversation()\n    -\u003eusing('openai', 'gpt-4o')\n    -\u003ewithSystemPrompt('You are a travel advisor.')\n    -\u003ewithMaxTokens(2048)\n    -\u003ebegin();\n\n$response = $convo-\u003esend('I want to visit Japan.');\necho $response-\u003etext;\n\n// Get the conversation ID to resume later\n$conversationId = $convo-\u003egetConversation()-\u003eid;\n\n// Resume later\n$convo = Sidekick::conversation()-\u003eresume($conversationId);\n$response = $convo-\u003esend('What about accommodation?');\n\n// Delete a conversation\n$convo-\u003edelete();\n```\n\n**All ConversationBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withSystemPrompt(string $prompt)` | Set the system prompt |\n| `withMaxTokens(int $maxTokens)` | Set max tokens (default: 1024) |\n| `begin(): self` | Start a new conversation (persisted to DB) |\n| `resume(string $id): self` | Resume an existing conversation by UUID |\n| `send(string $message): TextResponse` | Send a message and get a response |\n| `delete(): bool` | Delete the conversation and its messages |\n| `getConversation(): ?Conversation` | Get the underlying Eloquent model |\n\n### Image Generation\n\n```php\n$response = Sidekick::image()\n    -\u003eusing('openai', 'dall-e-3')\n    -\u003ewithPrompt('A sunset over mountains')\n    -\u003ewithSize('1024x1024')\n    -\u003ewithQuality('hd')\n    -\u003ecount(2)\n    -\u003egenerate();\n\necho $response-\u003eurl();           // First image URL\necho $response-\u003eurls;            // Array of all URLs\necho $response-\u003erevisedPrompt;   // DALL-E's revised prompt (if any)\n```\n\n**All ImageBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withPrompt(string $prompt)` | Set the image prompt |\n| `withSize(string $size)` | Set dimensions (default: `1024x1024`) |\n| `withQuality(string $quality)` | Set quality: `standard` or `hd` (default: `standard`) |\n| `count(int $count)` | Number of images to generate (default: 1) |\n| `generate(): ImageResponse` | Execute and return an `ImageResponse` |\n\n### Audio (Text-to-Speech)\n\n```php\n$response = Sidekick::audio()\n    -\u003eusing('openai', 'tts-1')\n    -\u003ewithText('Hello, welcome to Sidekick!')\n    -\u003ewithVoice('nova')\n    -\u003ewithFormat('mp3')\n    -\u003egenerate();\n\n$response-\u003esave('audio/welcome.mp3');       // Save to default disk\n$response-\u003esave('audio/welcome.mp3', 's3'); // Save to specific disk\necho $response-\u003eformat;                     // \"mp3\"\n```\n\n**All AudioBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withText(string $text)` | Set the text to speak |\n| `withVoice(string $voice)` | Set the voice (default: `alloy`) |\n| `withFormat(string $format)` | Set audio format (default: `mp3`) |\n| `generate(): AudioResponse` | Execute and return an `AudioResponse` |\n\n### Transcription\n\n```php\n$response = Sidekick::transcription()\n    -\u003eusing('openai', 'whisper-1')\n    -\u003ewithFile('/path/to/audio.mp3')\n    -\u003ewithLanguage('en')\n    -\u003egenerate();\n\necho $response-\u003etext;       // Transcribed text\necho $response-\u003elanguage;   // \"en\"\necho $response-\u003eduration;   // Duration in seconds\necho (string) $response;    // Same as $response-\u003etext\n```\n\n**All TranscriptionBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withFile(string $filePath)` | Path to the audio file |\n| `withLanguage(string $language)` | Hint the language (optional) |\n| `generate(): TranscriptionResponse` | Execute and return a `TranscriptionResponse` |\n\n### Embeddings\n\n```php\n$response = Sidekick::embedding()\n    -\u003eusing('openai', 'text-embedding-3-small')\n    -\u003ewithInput('Laravel is a great framework')\n    -\u003egenerate();\n\n$vector = $response-\u003evector();      // First embedding vector (array of floats)\n$all = $response-\u003eembeddings;       // All embedding vectors\necho $response-\u003eusage-\u003etotalTokens; // Token usage\n```\n\n**All EmbeddingBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withInput(string\\|array $input)` | Text or array of texts to embed |\n| `generate(): EmbeddingResponse` | Execute and return an `EmbeddingResponse` |\n\n### Moderation\n\n```php\n$response = Sidekick::moderation()\n    -\u003eusing('openai', 'text-moderation-latest')\n    -\u003ewithContent('Some text to moderate')\n    -\u003egenerate();\n\nif ($response-\u003eisFlagged()) {\n    // Content was flagged\n}\n\nif ($response-\u003eisFlaggedFor('violence')) {\n    // Specifically flagged for violence\n}\n\n// Inspect all categories\nforeach ($response-\u003ecategories as $category) {\n    echo \"{$category-\u003ecategory}: flagged={$category-\u003eflagged}, score={$category-\u003escore}\\n\";\n}\n```\n\n**All ModerationBuilder methods:**\n\n| Method | Description |\n|--------|-------------|\n| `using(string $provider, ?string $model)` | Set provider and model |\n| `withContent(string $content)` | Text to moderate |\n| `generate(): ModerationResponse` | Execute and return a `ModerationResponse` |\n\n### Utility Methods\n\nConvenience methods that use the default text provider:\n\n```php\n// Summarize text (returns string)\n$summary = Sidekick::summarize('Long text here...', maxLength: 500);\n\n// Translate text (returns string)\n$translated = Sidekick::translate('Hello', 'French');\n\n// Extract keywords (returns string, comma-separated)\n$keywords = Sidekick::extractKeywords('Some article text...');\n```\n\n## Knowledge Base / RAG\n\nSidekick includes a built-in RAG (Retrieval-Augmented Generation) system that lets you store business knowledge and ground AI responses in real data — preventing hallucinations about return policies, pricing, support hours, etc.\n\n### Setup\n\nRAG works out of the box with the default config. The `knowledge` section in `config/sidekick.php` controls embedding provider, chunk sizes, and search settings:\n\n```php\n'knowledge' =\u003e [\n    'embedding' =\u003e ['provider' =\u003e 'openai', 'model' =\u003e 'text-embedding-3-small'],\n    'chunking'  =\u003e ['chunk_size' =\u003e 2000, 'overlap' =\u003e 200],\n    'search'    =\u003e ['default_limit' =\u003e 5, 'min_score' =\u003e 0.3, 'driver' =\u003e VectorSearch::class],\n],\n```\n\n### Ingesting Content\n\n```php\nuse PapaRascalDev\\Sidekick\\Facades\\Sidekick;\n\n// Ingest text\nSidekick::knowledge('my-kb')\n    -\u003eingest('Our return policy is 30 days with receipt.', 'faq');\n\n// Ingest a file\nSidekick::knowledge('my-kb')\n    -\u003eingestFile('/path/to/faq.md');\n\n// Ingest multiple texts\nSidekick::knowledge('my-kb')\n    -\u003eingestMany(['Text one...', 'Text two...'], 'bulk-source');\n\n// Use a different embedding provider\nSidekick::knowledge('my-kb')\n    -\u003eusing('mistral', 'mistral-embed')\n    -\u003eingest('Content here...');\n```\n\nOr use the Artisan command:\n\n```bash\n# Ingest a file\nphp artisan sidekick:ingest my-kb --file=/path/to/faq.md\n\n# Ingest inline text\nphp artisan sidekick:ingest my-kb --text=\"Return policy is 30 days.\"\n\n# Ingest a directory of .txt/.md/.html/.csv files\nphp artisan sidekick:ingest my-kb --dir=/path/to/docs\n\n# Purge and re-ingest\nphp artisan sidekick:ingest my-kb --purge --dir=/path/to/docs\n```\n\n### Searching\n\n```php\n// Search for relevant chunks\n$results = Sidekick::knowledge('my-kb')-\u003esearch('What is your return policy?');\n\nforeach ($results as $chunk) {\n    echo $chunk-\u003econtent;       // The text content\n    echo $chunk-\u003esimilarity;    // Cosine similarity score\n    echo $chunk-\u003esource;        // Source label\n}\n```\n\n### Ask (Search + Generate)\n\n```php\n// One-liner: search KB and generate a grounded answer\n$answer = Sidekick::knowledge('my-kb')-\u003eask('What is your return policy?');\n```\n\n### Widget Integration\n\nConnect a knowledge base to the chat widget so it answers from your data:\n\n```dotenv\nSIDEKICK_WIDGET_ENABLED=true\nSIDEKICK_WIDGET_KNOWLEDGE_BASE=my-kb\n```\n\nOr in `config/sidekick.php`:\n\n```php\n'widget' =\u003e [\n    'knowledge_base'    =\u003e 'my-kb',\n    'rag_context_chunks' =\u003e 5,\n    'rag_min_score'      =\u003e 0.3,\n],\n```\n\nWhen configured, the widget will automatically search the knowledge base for each user message and inject relevant context into the system prompt. If RAG fails (API error, empty KB), it falls back gracefully to the original system prompt.\n\n### Custom Search Drivers\n\nThe default `VectorSearch` driver computes cosine similarity in PHP. For production workloads, you can swap in a custom driver (e.g., pgvector, Pinecone):\n\n```php\n// Implement the SearchesKnowledge contract\nuse PapaRascalDev\\Sidekick\\Contracts\\SearchesKnowledge;\n\nclass PgVectorSearch implements SearchesKnowledge\n{\n    public function search(KnowledgeBase $kb, array $queryEmbedding, int $limit = 5, float $minScore = 0.3): Collection\n    {\n        // Your pgvector implementation\n    }\n}\n\n// Register in config/sidekick.php\n'knowledge' =\u003e [\n    'search' =\u003e ['driver' =\u003e \\App\\Search\\PgVectorSearch::class],\n],\n```\n\n### Managing Knowledge Bases\n\n```php\n$kb = Sidekick::knowledge('my-kb');\n\n$kb-\u003echunkCount();           // Number of chunks stored\n$kb-\u003epurge();                // Delete all chunks\n$kb-\u003egetKnowledgeBase();     // Get the Eloquent model\n```\n\n## Chat Widget\n\nSidekick ships with an Alpine.js-powered chat widget you can embed in any Blade template. No Livewire required.\n\n### Enable the widget\n\nIn your `.env`:\n\n```dotenv\nSIDEKICK_WIDGET_ENABLED=true\nSIDEKICK_WIDGET_PROVIDER=openai\nSIDEKICK_WIDGET_MODEL=gpt-4o\nSIDEKICK_WIDGET_SYSTEM_PROMPT=\"You are a helpful assistant.\"\n```\n\n### Add to a Blade template\n\n```blade\n\u003cx-sidekick::chat-widget\n    position=\"bottom-right\"\n    theme=\"dark\"\n    title=\"AI Assistant\"\n    placeholder=\"Ask me anything...\"\n    button-label=\"Chat\"\n/\u003e\n```\n\n**Props:**\n\n| Prop | Default | Options |\n|------|---------|---------|\n| `position` | `bottom-right` | `bottom-right`, `bottom-left`, `top-right`, `top-left` |\n| `theme` | `light` | `light`, `dark` |\n| `title` | `Chat Assistant` | Any string |\n| `placeholder` | `Type a message...` | Any string |\n| `button-label` | `Chat` | Any string |\n\nMake sure your layout includes Alpine.js and a CSRF meta tag:\n\n```html\n\u003cmeta name=\"csrf-token\" content=\"{{ csrf_token() }}\"\u003e\n\u003cscript defer src=\"https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js\"\u003e\u003c/script\u003e\n```\n\n## Custom Providers\n\nRegister custom providers at runtime or via config:\n\n```php\n// Runtime registration\nSidekick::registerProvider('ollama', function ($app) {\n    return new OllamaProvider(config('sidekick.providers.ollama'));\n});\n\n// Or in config/sidekick.php\n'custom_providers' =\u003e [\n    'ollama' =\u003e \\App\\Sidekick\\OllamaProvider::class,\n],\n```\n\nCustom providers should implement `ProviderContract` and the relevant capability interfaces (`ProvidesText`, `ProvidesImages`, `ProvidesAudio`, `ProvidesTranscription`, `ProvidesEmbeddings`, `ProvidesModeration`).\n\n## Testing\n\nSidekick provides first-class testing support with `Sidekick::fake()`:\n\n```php\nuse PapaRascalDev\\Sidekick\\Facades\\Sidekick;\nuse PapaRascalDev\\Sidekick\\Responses\\TextResponse;\nuse PapaRascalDev\\Sidekick\\ValueObjects\\Meta;\nuse PapaRascalDev\\Sidekick\\ValueObjects\\Usage;\n\npublic function test_my_feature(): void\n{\n    $fake = Sidekick::fake([\n        new TextResponse(\n            text: 'Mocked response',\n            usage: new Usage(10, 20, 30),\n            meta: new Meta('openai', 'gpt-4o'),\n        ),\n    ]);\n\n    // ... run your code that uses Sidekick ...\n\n    $fake-\u003eassertTextGenerated();       // At least one text generation\n    $fake-\u003eassertTextGenerated(2);      // Exactly 2 text generations\n    $fake-\u003eassertNothingSent();         // No API calls were made\n    $fake-\u003eassertProviderUsed('openai');\n    $fake-\u003eassertModelUsed('gpt-4o');\n    $fake-\u003eassertPromptContains('expected text');\n}\n```\n\n## Events\n\nSidekick dispatches Laravel events you can listen to:\n\n| Event | Properties | When |\n|-------|-----------|------|\n| `RequestSending` | `provider`, `model`, `capability` | Before a request is sent |\n| `ResponseReceived` | `provider`, `model`, `capability`, `response` | After a successful response |\n| `StreamChunkReceived` | `provider`, `model`, `chunk` | For each streaming chunk |\n| `RequestFailed` | `provider`, `model`, `capability`, `exception` | When a request fails |\n\nAll events are in the `PapaRascalDev\\Sidekick\\Events` namespace.\n\n## Provider Capabilities\n\n| Capability | OpenAI | Anthropic | Mistral | Cohere |\n|-----------|--------|-----------|---------|--------|\n| Text | Yes | Yes | Yes | Yes |\n| Image | Yes | - | - | - |\n| Audio (TTS) | Yes | - | - | - |\n| Transcription | Yes | - | - | - |\n| Embedding | Yes | - | Yes | - |\n| Moderation | Yes | - | - | - |\n\n## API Key Resources\n\n- [OpenAI](https://platform.openai.com)\n- [Anthropic](https://console.anthropic.com)\n- [Mistral](https://console.mistral.ai)\n- [Cohere](https://dashboard.cohere.com)\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## License\n\nGPL-2.0-or-later. See [LICENSE](LICENSE) for details.\n\n## Stargazers\n\n[![Stargazers repo roster for @PapaRascal2020/sidekick](https://reporoster.com/stars/dark/notext/PapaRascal2020/sidekick)](https://github.com/PapaRascal2020/sidekick/stargazers)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaparascal2020%2Fsidekick","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaparascal2020%2Fsidekick","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaparascal2020%2Fsidekick/lists"}