Laravel 13.x Qwen2.5-Coder-7B-Instruct LoRA (v5)
A 7B code model fine-tuned on 235 Laravel 13.x instruction-to-code pairs with Laravel Boost guidelines baked into training. Covers 18+ Laravel class types including API Resources with relationship loading, Form Request precision hooks, and Pest feature tests. Trained on Apple M2 Pro 16GB — no cloud GPU needed.
What Changed in v5 (Sprint 3: Coverage Expansion)
| v4 | v5 | |
|---|---|---|
| Training examples | 202 | 235 (+33 new examples) |
| Val loss | 0.055 | 0.032 (best: iter 150) |
| API Resources | 2 examples | 14 examples — whenLoaded, $this->when, mergeWhen, whenNotNull, ResourceCollection |
| Form Requests | 3 examples | 13 examples — messages(), attributes(), prepareForValidation(), after() |
| Pest Tests | 0 examples | 15 examples — CRUD, auth, pagination, fakes |
| Artisan regression | 5/5 × 11/11 | 5/5 × 11/11 (no regression) |
Sprint 3 eval results (E1–E8)
| Eval | Category | Result | Notes |
|---|---|---|---|
| E1 | API Resource | ✅ 8/8 | whenLoaded (posts + subscription), whenNotNull (email_verified_at) |
| E2 | API Resource | ✅ 8/8 | whenLoaded (category), $this->when (is_featured, cost_price) |
| E3 | API Resource | ✅ 6/8 | ResourceCollection with with() for aggregated meta |
| E4 | Form Request | ✅ pass | messages(), attributes(), prepareForValidation() + $this->merge() |
| E5 | Form Request | ✅ pass | after() with Hash::check(), Validator closure |
| E6 | Pest Test | ⚠️ pass* | Correct test logic; style defaults to PHPUnit class unless explicitly prompted |
| E7 | Pest Test | ⚠️ pass* | Pagination test passes in real Laravel |
| E8 | Pest Test | ⚠️ pass* | Auth/ownership tests pass in real Laravel |
*Pest tests pass functionally in a real Laravel 13.2.0 + SQLite project (14/14 assertions pass), but the model defaults to PHPUnit class-based syntax unless the prompt explicitly requests Pest function syntax.
Real-world validation
All eval prompts were tested in a real Laravel 13.2.0 + SQLite project:
php artisan test → 14/14 passed (28 assertions)
Tests cover: POST create (auth, 401, 422 validation), GET paginated (meta.total), DELETE own/other (204, 403).
Root causes found in Sprint 3
| Gap | Root cause | v5 fix |
|---|---|---|
API Resources: bare $this->posts |
Model saw shell from pretraining but no whenLoaded examples |
12 examples with whenLoaded, $this->when, mergeWhen, whenNotNull, whenCounted |
Form Requests: no messages() |
Model generated rules() only — basic shell from pretraining |
10 examples with messages(), attributes(), prepareForValidation(), after() |
| Pest tests: PHPUnit class style | Strong pretraining prior to class-based tests; uses() not anchored |
15 Pest examples + explicit BOOST_CONTEXT block; prompt must specify "Pest function syntax" |
Evaluation: API Resource (E1)
Prompt: "Create UserResource with optional posts collection (whenLoaded) and subscription (whenLoaded). Use whenNotNull for email_verified_at."
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'email_verified_at' => $this->whenNotNull($this->email_verified_at),
'posts' => PostResource::collection($this->whenLoaded('posts')),
'subscription' => SubscriptionResource::make($this->whenLoaded('subscription')),
'created_at' => $this->created_at,
];
}
}
Correct: whenLoaded used (not $this->posts), whenNotNull used (not ternary), returns array.
Evaluation: Form Request (E4)
Prompt: "StoreInvoiceRequest with messages(), attributes(), prepareForValidation() that rounds amount."
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'client_id' => ['required', 'exists:clients,id'],
'amount' => ['required', 'numeric', 'min:0.01'],
'due_date' => ['required', 'date', 'after:today'],
'notes' => ['nullable', 'string', 'max:1000'],
];
}
public function messages(): array
{
return [
'client_id.required' => 'The client is required.',
'amount.required' => 'The amount is required.',
'amount.numeric' => 'The amount must be a number.',
'due_date.required' => 'The due date is required.',
];
}
public function attributes(): array
{
return [
'client_id' => 'Client',
'due_date' => 'Due Date',
];
}
public function prepareForValidation(): void
{
$this->merge([
'amount' => round($this->amount, 2),
]);
}
}
Correct: messages() uses field.rule keys, prepareForValidation() uses $this->merge() (not direct assignment).
Evaluation: Pest Test (E6)
Prompt: "Pest test for POST /api/posts: auth creates (201 + assertDatabaseHas), unauth returns 401, missing title returns 422 with validation errors."
<?php
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('authenticated user creates a post with title and body', function () {
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/posts', ['title' => 'Test Post', 'body' => 'Body text.'])
->assertCreated();
$this->assertDatabaseHas('posts', ['title' => 'Test Post', 'user_id' => $user->id]);
});
test('unauthenticated user gets 401', function () {
$this->postJson('/api/posts', ['title' => 'Test Post', 'body' => 'Body text.'])
->assertUnauthorized();
});
test('authenticated user sends missing title gets 422', function () {
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/posts', ['body' => 'Body text.'])
->assertUnprocessable()
->assertJsonValidationErrors(['title']);
});
Tested in real Laravel 13.2.0 — 3/3 assertions pass.
Usage
With adapter (recommended)
from mlx_lm import load, generate
SYSTEM = """You are a senior Laravel developer. Write clean, production-ready Laravel 13.x code.
Output only the PHP file contents — no markdown, no explanation, no ```php fences.
### API Resources
// Always extend JsonResource. toArray() returns array, not JsonResponse.
// Relationships: $this->whenLoaded('relation', fn() => RelatedResource::make($this->relation))
// Collections: $this->whenLoaded('items', fn() => ItemResource::collection($this->items))
// Conditional field: $this->when($condition, $value)
// Conditional block: $this->mergeWhen($condition, ['field' => $value])
// Nullable field: $this->whenNotNull($this->deleted_at)
// ResourceCollection: override with() to add pagination metadata
// Never call toArray() manually — Laravel calls it automatically
### Form Request precision
// messages(): return ['field.rule' => 'Custom message'] — overrides default error text
// attributes(): return ['field' => 'Human Name'] — used in :attribute placeholder
// prepareForValidation(): call $this->merge([...]) to normalize input before rules run
// after(): return [fn(Validator $v) => $v->errors()->addIf($condition, 'field', 'msg')]
// passedValidation(): runs after all rules pass — use for side effects, not validation
// NEVER use $this->validate() — that is a Controller method
### Pest Feature Tests
// File: tests/Feature/SomeTest.php
// uses(RefreshDatabase::class); at top of file
// HTTP: $this->getJson('/api/route'), postJson(), putJson(), patchJson(), deleteJson()
// Status: ->assertOk() (200), ->assertCreated() (201), ->assertNoContent() (204)
// ->assertUnprocessable() (422), ->assertUnauthorized() (401), ->assertForbidden() (403)
// JSON: ->assertJson(['key' => 'value']), ->assertJsonStructure(['data' => ['id', 'name']])
// ->assertJsonCount(3, 'data'), ->assertJsonPath('data.0.name', 'Alice')
// DB: assertDatabaseHas('table', ['col' => 'val']), assertDatabaseMissing(), assertDatabaseCount()
// Auth: $this->actingAs(User::factory()->create())
// Fakes: Queue::fake(), Event::fake(), Mail::fake() then ::assertPushed/Dispatched/Sent
// IMPORTANT: Pest uses function syntax (uses/test/it) NOT class-based syntax
### Artisan Commands
// Always extend Illuminate\\Console\\Command
// handle() returns Command::SUCCESS or Command::FAILURE (never raw int)
// count(): use count($array) — Command has NO $this->count property
// Accumulators: ALL keys in same array use (?? 0) + $value pattern
// Initialize ALL closure variables BEFORE the closure definition
// Facades\\Progress does NOT exist"""
model, tok = load(
"mlx-community/Qwen2.5-Coder-7B-Instruct-4bit",
adapter_path="fchis/Laravel-13x-Qwen2.5-Coder-7B-Instruct-LoRA"
)
messages = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": "Create a UserResource with whenLoaded for posts (PostResource collection) and subscription (SubscriptionResource). Use whenNotNull for email_verified_at."}
]
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
output = generate(model, tok, prompt=text, max_tokens=1200)
if '<|im_end|>' in output:
output = output[:output.index('<|im_end|>')]
print(output.strip())
CLI pipeline
pip install mlx-lm
git clone https://github.com/florinel-chis/laravel-ai-gen
cd your-laravel-project
python3 laravel-gen.py --model 7b "UserResource with posts collection and subscription via whenLoaded"
Model Details
| Detail | Value |
|---|---|
| Base model | Qwen2.5-Coder-7B-Instruct (4-bit via MLX) |
| Fine-tuning | LoRA, 8 layers, rank=8, scale=20, lr=1e-5 |
| Training data | 235 instruction-to-code pairs with Boost guidelines |
| Resumed from | v4 iter 150 (val_loss 0.055) |
| Best checkpoint | Iter 150 (val_loss 0.032) |
| Training hardware | Apple M2 Pro, 16GB unified memory |
| Training time | ~19 minutes (150 iters, max_seq_length 1500) |
| Peak memory | 10.820 GB |
| Val loss | 0.032 (iter 150) |
Training History
| Version | Examples | Val loss | Key change |
|---|---|---|---|
| v1 (1B) | 90 | — | Proof of concept |
| v2 (7B) | 162 | 0.178 | First 7B, repetition loops |
| v3 (7B) | 187 | 0.076 | +25 artisan examples, fixed repetition |
| v4 (7B) | 202 | 0.055 | +15 precision examples, 3 bug patterns eliminated |
| v5 (7B) | 235 | 0.032 | +33 examples: API Resources, Form Requests, Pest Tests |
Patterns Covered (18+ class types)
| Category | Examples | Patterns |
|---|---|---|
| Eloquent | Models, relationships, scopes, casts, soft deletes | 20+ |
| Database | Migrations (create, alter, pivot), factories, seeders | 15+ |
| HTTP | Controllers (API resource, short), Form Requests | 25+ |
| API Resources | JsonResource, ResourceCollection, whenLoaded, $this->when, mergeWhen | 14 |
| Form Requests | rules, messages, attributes, prepareForValidation, after, passedValidation | 13 |
| Tests | Pest feature tests — CRUD, auth, pagination, validation, queue/mail fakes | 15 |
| Queue | Jobs (ShouldQueue, constructor injection) | 3 |
| Events | Events (Dispatchable), Listeners (queued), Observers | 5 |
| Notifications | Mail + database channels, Queueable | 3 |
| Console | Artisan commands — data I/O, CSV/JSON/HTTP, interactive, precision patterns | 40+ |
| Views | Blade templates, View Composers, Blade components | 8 |
| Mailable (envelope, content, markdown) | 2 |
Known Limitations
- Pest syntax: model defaults to PHPUnit class-based tests unless prompt explicitly says "Pest function syntax (uses/test/it)"
- Unseen patterns (Livewire, Inertia.js, complex Eloquent scopes) still limited
- End-token stripping required in post-processing:
output[:output.index('<|im_end|>')] - Laravel official 17-task benchmark coverage: ~5/17 tasks (added Resources, Form precision, Tests to the previous 3)
Companion Models
| Model | Role | Link |
|---|---|---|
| Planner (1.7B) | Decompose features into tasks | fchis/Laravel-13x-Planner-Qwen3-1.7B-LoRA |
| Coder 3B | Generate code (faster) | fchis/Laravel-13x-Qwen2.5-Coder-3B-Instruct-LoRA |
| Coder 7B | Generate code (best quality) | You're here |
| CLI tool | End-to-end pipeline | github.com/florinel-chis/laravel-ai-gen |
| Training data | 235 examples | fchis/Laravel-13x-Code-Instructions |
License
Apache 2.0
- Downloads last month
- 945
4-bit