set('vision.enabled', true); config()->set('vision.gateway.base_url', 'https://vision.local'); config()->set('vision.lm_studio.base_url', 'https://lmstudio.local'); config()->set('vision.lm_studio.model', 'google/gemma-3-4b'); config()->set('cdn.files_url', 'https://files.local'); $photography = ContentType::query()->create([ 'name' => 'Photography', 'slug' => 'photography', ]); Category::query()->create([ 'content_type_id' => $photography->id, 'name' => 'Flowers', 'slug' => 'flowers', 'is_active' => true, ]); $user = User::factory()->create(); $artwork = Artwork::factory()->create([ 'user_id' => $user->id, 'hash' => 'cmdaa112233', 'file_name' => 'rose-closeup.jpg', 'title' => 'Rose Study', ]); Http::fake([ 'https://vision.local/analyze/all' => Http::response([ 'clip' => [ ['tag' => 'rose', 'confidence' => 0.96], ['tag' => 'flower', 'confidence' => 0.91], ], 'yolo' => [ ['label' => 'flower', 'confidence' => 0.79], ], 'blip' => 'a close up photograph of a rose bud with soft natural background', ], 200), 'https://lmstudio.local/v1/chat/completions' => Http::response([ 'choices' => [ [ 'message' => [ 'content' => json_encode([ 'rose macro', 'flower close-up', 'soft petals', 'natural light', 'botanical photography', 'pink tones', 'shallow depth', 'floral detail', 'macro photography', 'garden bloom', ], JSON_THROW_ON_ERROR), ], ], ], ], 200), ]); $this->artisan('artworks:ai-suggest', ['artwork_id' => $artwork->id]) ->expectsOutputToContain('provider: lm_studio') ->expectsOutputToContain('tags: rose-macro, flower-close-up') ->expectsOutputToContain('content type: photography | category: flowers') ->assertSuccessful(); $assist = ArtworkAiAssist::query()->where('artwork_id', $artwork->id)->first(); expect($assist)->not->toBeNull(); expect($assist?->status)->toBe(ArtworkAiAssist::STATUS_READY); expect($assist?->tag_suggestions_json)->not->toBeEmpty(); expect(collect($assist?->tag_suggestions_json ?? [])->contains(fn (array $row): bool => ($row['tag'] ?? null) === 'rose-macro'))->toBeTrue(); expect($assist?->raw_response_json['tag_generation']['raw_content'] ?? null)->toBe('["rose macro","flower close-up","soft petals","natural light","botanical photography","pink tones","shallow depth","floral detail","macro photography","garden bloom"]'); expect($assist?->raw_response_json['tag_generation']['image_url'] ?? null)->toBe('https://files.local/artworks/md/cm/da/cmdaa112233.webp'); }); it('supports overriding the provider to together from the artisan command', function (): void { config()->set('vision.enabled', true); config()->set('vision.gateway.base_url', 'https://vision.local'); config()->set('vision.together.base_url', 'https://api.together.xyz'); config()->set('vision.together.endpoint', '/v1/chat/completions'); config()->set('vision.together.model', 'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo'); config()->set('vision.together.api_key', 'together-test-key'); config()->set('cdn.files_url', 'https://files.local'); $photography = ContentType::query()->create([ 'name' => 'Photography', 'slug' => 'photography', ]); Category::query()->create([ 'content_type_id' => $photography->id, 'name' => 'Flowers', 'slug' => 'flowers', 'is_active' => true, ]); $user = User::factory()->create(); $artwork = Artwork::factory()->create([ 'user_id' => $user->id, 'hash' => 'tgaa11223344', 'file_name' => 'rose-closeup.jpg', 'title' => 'Together Rose Study', ]); Http::fake([ 'https://vision.local/analyze/all' => Http::response([ 'clip' => [ ['tag' => 'rose', 'confidence' => 0.96], ['tag' => 'flower', 'confidence' => 0.91], ], 'yolo' => [ ['label' => 'flower', 'confidence' => 0.79], ], 'blip' => 'a close up photograph of a rose bud with soft natural background', ], 200), 'https://api.together.xyz/v1/chat/completions' => Http::response([ 'choices' => [ [ 'message' => [ 'content' => json_encode([ 'rose macro', 'flower close-up', 'soft petals', 'natural light', 'botanical photography', 'pink tones', 'shallow depth', 'floral detail', 'macro photography', 'garden bloom', ], JSON_THROW_ON_ERROR), ], ], ], ], 200), ]); $this->artisan('artworks:ai-suggest', [ 'artwork_id' => $artwork->id, '--provider' => 'together', ]) ->expectsOutputToContain('provider: together') ->expectsOutputToContain('tags: rose-macro, flower-close-up') ->assertSuccessful(); $assist = ArtworkAiAssist::query()->where('artwork_id', $artwork->id)->first(); expect($assist)->not->toBeNull(); expect($assist?->raw_response_json['tag_generation']['provider'] ?? null)->toBe('together'); expect($assist?->raw_response_json['tag_generation']['endpoint'] ?? null)->toBe('https://api.together.xyz/v1/chat/completions'); Http::assertSent(function (\Illuminate\Http\Client\Request $request): bool { return $request->url() === 'https://api.together.xyz/v1/chat/completions' && $request->hasHeader('Authorization', 'Bearer together-test-key'); }); });