[{"data":1,"prerenderedAt":2798},["ShallowReactive",2],{"profile-data":3,"compact-case-studies":88,"testimonials-data":129,"recent-blog-posts-2":232},{"id":4,"title":5,"availability":6,"avatar":20,"clientSatisfaction":21,"currentFocus":22,"description":26,"experience":27,"extension":28,"footer":29,"heroHeadline":32,"meta":33,"name":34,"pricingRanges":35,"projectsDelivered":46,"social":48,"stem":61,"tagline":62,"whoIWorkWith":63,"workApproach":83,"__hash__":87},"profile\u002Fprofile.yml","Senior Software Engineer | Full-Stack Developer | DevOps Enthusiast",{"status":7,"statusText":8,"startDate":9,"startDateContext":10,"description":11,"responseTime":12,"timezone":13,"slotsAvailable":14,"paymentTerms":15,"cta":16,"note":19},"available","Available for new projects","April 2025","Next opening","Open to freelance, consulting, and collaborative projects. Flexible with remote, async, and agile workflows. Comfortable working across time zones and with distributed teams.","3h","GMT+5",3,"20% upfront, rest on milestones",{"text":17,"url":18},"Email Now To Discuss Your Project Or Idea","mailto:mubaidr@gmail.com","I aim to reply as quickly as possible with the attention your message deserves.","\u002Fmubaidr.png",100,[23,24,25],"Infrastructure as Code (IaC)","Cloud-native tooling and observability","AI-assisted development workflows","Delivering robust, scalable, and user-focused software solutions that drive business success.",13,"yml",{"message":30,"lastUpdated":31},"Thank you for your interest. I look forward to collaborating and building something exceptional together.","2025-06-28T12:00:00.000Z","Senior Software Engineer building scalable systems and developer tools. Open source maintainer",{},"Muhammad Ubaid R.",{"mvp":36,"architectureAudit":41,"hourly":45},{"min":37,"max":38,"currency":39,"description":40},5000,15000,"USD","Full-stack MVP development",{"min":42,"max":43,"currency":39,"description":44},1500,3000,"System architecture review and recommendations",{"min":21,"max":46,"currency":39,"description":47},125,"Hourly consulting and development",[49,53,57],{"name":50,"url":51,"icon":52},"GitHub","https:\u002F\u002Fgithub.com\u002Fmubaidr","i-ph-github-logo",{"name":54,"url":55,"icon":56},"LinkedIn","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fmubaidr","i-ph-linkedin-logo",{"name":58,"url":59,"icon":60},"X","https:\u002F\u002Fx.com\u002Fmubaidr","i-ph-x-logo","profile","Delivered 125+ projects with 100% client satisfaction. 13 years building scalable systems.",[64,68,72,75,79],{"name":65,"icon":66,"description":67},"Startups","i-ph-rocket-launch","Early-stage companies building MVPs and scaling products",{"name":69,"icon":70,"description":71},"SMEs","i-ph-buildings","Small to medium enterprises optimizing and modernizing systems",{"name":73,"icon":70,"description":74},"Enterprise","Large organizations requiring architecture and performance expertise",{"name":76,"icon":77,"description":78},"Agencies","i-ph-users-three","Digital agencies needing technical leadership and delivery support",{"name":80,"icon":81,"description":82},"Individual Founders","i-ph-user","Solo founders turning ideas into production-ready applications",[84,85,86],"Architecting solutions and managing product lifecycles - optimizing performance, scalability, and security","Leading agile teams and collaborating with clients - Ensuring clarity, timeliness, and adaptability in all project phases","Automating workflows and ensuring code quality","kah6FarLEWIb1aJERf-A2v09d44jRxlAI-CWvfBY268",{"id":89,"caseStudies":90,"extension":28,"meta":126,"stem":127,"__hash__":128},"caseStudies\u002Fcase-studies.yml",[91,98,103,108,114,120],{"id":92,"problem":93,"solution":94,"metric":95,"link":96,"projectRef":97},4,"Marketplace startup needed to disrupt 10-25% commission model with scalable multi-marketplace architecture","Built subscription-based platform with 4 marketplaces, 72-hr funds release, 3-way dispute resolution, and KYC verification","28K+ signups, 600K+ visitors across 5 countries, $18K+ raised, IPO-track","\u002Fprojects",11,{"id":14,"problem":99,"solution":100,"metric":101,"link":96,"projectRef":102},"Enterprise struggled with slow document search and information retrieval","Created AI chat platform with OCR and intelligent document indexing","50K+ documents, 90% faster search, 95% query accuracy",15,{"id":104,"problem":105,"solution":106,"metric":107,"link":96,"projectRef":102},5,"Enterprise struggled with 50K+ unsearchable documents and manual review bottlenecks","Developed AI chat platform with OCR, vector search, and RAG — 95% query accuracy, 90% faster retrieval","50K+ docs processed, 200K+ queries, 5K+ users, 95% accuracy",{"id":109,"problem":110,"solution":111,"metric":112,"link":96,"projectRef":113},6,"Teams drowning in WhatsApp chats with no way to extract insights or automate responses","Built AI-powered platform with QR auth, chat summarization, RAG-based Q&A, and reply suggestions","Multi-phone support, real-time SSE, pgvector search, JWT auth",16,{"id":115,"problem":116,"solution":117,"metric":118,"link":96,"projectRef":119},1,"Vacation rental marketplace needed real-time features and secure payments at scale","Built WebSocket-powered platform with 3D Secure payments and fraud detection","10K+ properties, 50K+ guests, 99.9% uptime",10,{"id":121,"problem":122,"solution":123,"metric":124,"link":96,"projectRef":125},2,"Recruitment process was slow and manual with high time-to-hire","Developed AI-powered platform with automated workflows and candidate matching","100+ companies, 40% faster hiring, 2K+ successful hires",8,{},"case-studies","Jx3U2B15exBkdp9C544iJEo3VEpCXYlisrPBjtloogI",[130,147,163,180,194,207,220],{"id":131,"title":132,"avatar":133,"company":134,"extension":28,"featured":135,"meta":136,"name":137,"project":138,"projectId":92,"quote":139,"rating":104,"results":140,"stem":145,"__hash__":146},"testimonials\u002Ftestimonials\u002F1.guilherme-r.yml","Startup Founder","https:\u002F\u002Fwww.freelancer.com\u002Fppic\u002F246986674\u002Flogo\u002F20882715\u002Fprofile_logo_20882715.jpg","Tech Startup",true,{},"Guilherme R.","SaaS Platform MVP","Muhammad delivered our MVP 2 weeks ahead of schedule and 20% under budget. His expertise in Vue.js helped us achieve 40% faster load times than our previous solution. Highly recommended!",[141,142,143,144],"Delivered 2 weeks early","20% under budget","40% performance improvement","Zero post-launch bugs","testimonials\u002F1.guilherme-r","i_CJ2uqqnbdVi0zCV5y3b6MajUVlUBUuUiv3-CNidAc",{"id":148,"title":149,"avatar":150,"company":151,"extension":28,"featured":135,"meta":152,"name":153,"project":154,"projectId":14,"quote":155,"rating":104,"results":156,"stem":161,"__hash__":162},"testimonials\u002Ftestimonials\u002F2.mark-b.yml","Product Manager","https:\u002F\u002Fwww.freelancer.com\u002Fppic\u002F57102564\u002Flogo\u002F3093471\u002Fprofile_logo_3093471.jpg","E-commerce Agency",{},"Mark B.","E-commerce Platform Modernization","Perfect 5\u002F5 across all metrics! Muhammad transformed our legacy system into a modern, scalable solution. Revenue increased by 35% within 3 months of launch. Outstanding work!",[157,158,159,160],"35% revenue increase","50% faster page loads","Zero downtime migration","100% client satisfaction","testimonials\u002F2.mark-b","42ofg15Nigptx404Ooum_-8RrQ_i90waZWlQE4qHvuc",{"id":164,"title":165,"avatar":166,"company":167,"extension":28,"featured":135,"meta":168,"name":169,"project":170,"projectId":171,"quote":172,"rating":104,"results":173,"stem":178,"__hash__":179},"testimonials\u002Ftestimonials\u002F3.chris-m.yml","CTO","https:\u002F\u002Fwww.freelancer.com\u002Fppic\u002F205903495\u002Flogo\u002F51131990\u002Fprofile_logo_51131990.jpg","FinTech Startup",{},"Chris M.","Real-time Financial Dashboard",null,"Muhammad exceeded expectations! Delivered a complex financial dashboard on time and budget. The solution handles 10,000+ concurrent users with 99.9% uptime. Already planning our next project together!",[174,175,176,177],"10,000+ concurrent users","99.9% uptime achieved","On time & budget delivery","30% faster than competitors","testimonials\u002F3.chris-m","VdkdbqdxsWB3JN-CvBnyFkM_8A2U67ch5Losjc594Gc",{"id":181,"title":182,"avatar":183,"company":183,"extension":28,"featured":135,"meta":184,"name":185,"project":186,"projectId":171,"quote":187,"rating":104,"results":188,"stem":192,"__hash__":193},"testimonials\u002Ftestimonials\u002F4.yossi-g.yml","Client","",{},"Yossi G.","Custom CRM Integration","The work Muhammad did was very good, and he found solutions to unexpected problems.",[189,190,191],"Resolved unexpected API issues","Integrated with legacy systems","Delivered on time despite challenges","testimonials\u002F4.yossi-g","rl-PhA5zg8TKfjJ0lfxYsutNrVlYEJ7OFGaOm9zGodU",{"id":195,"title":182,"avatar":183,"company":183,"extension":28,"featured":196,"meta":197,"name":198,"project":199,"projectId":171,"quote":200,"rating":104,"results":201,"stem":205,"__hash__":206},"testimonials\u002Ftestimonials\u002F5.chris-h.yml",false,{},"Chris H.","E-commerce Website Redesign","mubaidr did a great job and delivered exactly what we were expecting. He followed our request very well and was patient to work through all of our requests.",[202,203,204],"Met all feature requests","Smooth communication throughout","Improved user experience","testimonials\u002F5.chris-h","jsi8wkPP1_H2GFoeZN85TTPw0dOqG1OdkayKR0xrjvQ",{"id":208,"title":182,"avatar":209,"company":183,"extension":28,"featured":196,"meta":210,"name":211,"project":212,"projectId":171,"quote":213,"rating":104,"results":214,"stem":218,"__hash__":219},"testimonials\u002Ftestimonials\u002F6.courtenay-d.yml","https:\u002F\u002Fwww.freelancer.com\u002Fppic\u002F56912067\u002Flogo\u002F2748700\u002Fprofile_logo_2748700.jpg",{},"Courtenay D.","WordPress Plugin Development","Good - I could not do this stuff - required someone of his skills!",[215,216,217],"Delivered custom plugin as specified","Provided clear documentation","Enabled new business workflow","testimonials\u002F6.courtenay-d","KEBnL-Meu081fBf1HrTehS6CUbdKKKNMvRVrEGf5nmM",{"id":221,"title":182,"avatar":183,"company":183,"extension":28,"featured":196,"meta":222,"name":223,"project":224,"projectId":171,"quote":225,"rating":104,"results":226,"stem":230,"__hash__":231},"testimonials\u002Ftestimonials\u002F7.wahidal-a.yml",{},"Wahidal A.","Mobile App Enhancement","Muhammad Ubaid is dedicated, and professional, and maintains a positive attitude. He consistently exceeds expectations and willingly accommodates modifications. I highly recommend him and look forward to future collaborations.",[227,228,229],"Implemented requested modifications quickly","Provided ongoing support","Exceeded performance expectations","testimonials\u002F7.wahidal-a","lPBf8fOG6124x1lW5UTJv-tHJHtMDooQODH92-FqIR0",[233,831],{"id":234,"title":235,"abstract":171,"author":236,"authorUrl":171,"body":237,"date":815,"dateUpdated":171,"description":816,"excerpt":171,"extension":817,"featured":135,"headline":171,"image":818,"meta":819,"navigation":135,"ogImage":171,"path":821,"seo":822,"series":171,"seriesDescription":171,"seriesOrder":171,"socialImage":171,"stem":823,"tags":824,"__hash__":830},"blog\u002Fblog\u002F36-gem-team-v1.75.0-release.md","gem-team v1.75.0 — Pre-Flight Verification, A11y Audits & Agent Quality Gates","mubaidr",{"type":238,"value":239,"toc":800},"minimark",[240,257,262,267,278,289,312,321,324,328,334,360,362,366,373,511,513,517,521,532,538,542,557,560,582,586,599,601,605,611,618,705,707,711,755,757,761,764,784,786,796],[241,242,243,244,248,249,252,253,256],"p",{},"gem-team v1.75.0 represents a significant step toward production-grade AI-assisted development. This release focuses on ",[245,246,247],"strong",{},"quality gates",", ",[245,250,251],{},"verification protocols",", and ",[245,254,255],{},"agent reasoning discipline"," — the infrastructure that turns \"AI-generated code\" into \"production-ready code.\"",[258,259,261],"h2",{"id":260},"whats-new-in-v1750","What's New in v1.75.0",[263,264,266],"h3",{"id":265},"new-quality-gates-verification","🚀 New Quality Gates & Verification",[241,268,269,272,273,277],{},[245,270,271],{},"Pre-Flight Verification Steps"," — Every agent now runs a pre-flight checklist before executing tasks. This catches configuration issues, missing dependencies, and context gaps ",[274,275,276],"em",{},"before"," they waste tokens or produce broken output.",[241,279,280,283,284,288],{},[245,281,282],{},"Visual Diff Thresholds"," (",[285,286,287],"code",{},"quality.visual_diff_threshold",") — Configure pixel-perfect or perceptual thresholds for browser testing. Catch unintended UI changes in PRs automatically.",[241,290,291,283,294,297,298,301,302,301,305,301,308,311],{},[245,292,293],{},"Accessibility Audit Levels",[285,295,296],{},"quality.a11y_audit_level",") — Choose from ",[285,299,300],{},"none"," | ",[285,303,304],{},"basic",[285,306,307],{},"strict",[285,309,310],{},"wcag-aa"," to enforce accessibility standards at the agent level. The new A11y cache uses page snapshot hashes for instant re-runs.",[241,313,314,283,317,320],{},[245,315,316],{},"Screenshot on Failure",[285,318,319],{},"testing.screenshot_on_failure",") — Automatic visual capture when browser tests fail, making debugging visual regressions trivial.",[322,323],"hr",{},[263,325,327],{"id":326},"documentation-agent-discipline","📝 Documentation & Agent Discipline",[241,329,330,333],{},[245,331,332],{},"Impact Triage & Scope Discipline"," — Agent documentation now includes explicit impact triage (low\u002Fmedium\u002Fhigh\u002Fcritical) and scope discipline rules. Agents must declare blast radius before acting.",[241,335,336,339,340,301,343,346,347,301,350,301,353,301,356,359],{},[245,337,338],{},"gem-designer-mobile Parsing Modes"," — New parsing mode descriptions for mobile UI analysis: ",[285,341,342],{},"create",[285,344,345],{},"validate"," with scope targeting (",[285,348,349],{},"component",[285,351,352],{},"screen",[285,354,355],{},"navigation",[285,357,358],{},"design_system",").",[322,361],{},[263,363,365],{"id":364},"housekeeping-major-agent-workflow-improvements","🧹 Housekeeping: Major Agent Workflow Improvements",[241,367,368,369,372],{},"This release includes ",[245,370,371],{},"15+ housekeeping commits"," that fundamentally improve how agents reason and execute:",[374,375,376,393],"table",{},[377,378,379],"thead",{},[380,381,382,387,390],"tr",{},[383,384,386],"th",{"align":385},"left","Improvement",[383,388,389],{"align":385},"Agent(s) Affected",[383,391,392],{"align":385},"Impact",[394,395,396,410,423,436,448,461,474,486,498],"tbody",{},[380,397,398,404,407],{},[399,400,401],"td",{"align":385},[245,402,403],{},"Devil's Advocate Step",[399,405,406],{"align":385},"All agents",[399,408,409],{"align":385},"Agents now explicitly challenge their own assumptions before proceeding",[380,411,412,417,420],{},[399,413,414],{"align":385},[245,415,416],{},"Differential Diagnosis",[399,418,419],{"align":385},"gem-debugger",[399,421,422],{"align":385},"Structured root-cause analysis with minimal reproduction steps",[380,424,425,430,433],{},[399,426,427],{"align":385},[245,428,429],{},"Hypothesis-Driven Planning",[399,431,432],{"align":385},"gem-planner",[399,434,435],{"align":385},"Plans start with falsifiable hypotheses, not assumptions",[380,437,438,443,445],{},[399,439,440],{"align":385},[245,441,442],{},"Ownership Principle",[399,444,406],{"align":385},[399,446,447],{"align":385},"Clear ownership declarations prevent diffusion of responsibility",[380,449,450,455,458],{},[399,451,452],{"align":385},[245,453,454],{},"Budget Enforcement",[399,456,457],{"align":385},"gem-researcher",[399,459,460],{"align":385},"Token\u002Ftime budgets with early-exit criteria",[380,462,463,468,471],{},[399,464,465],{"align":385},[245,466,467],{},"Scope Conflict Handling",[399,469,470],{"align":385},"gem-orchestrator",[399,472,473],{"align":385},"Detects and resolves conflicting task scopes in wave scheduling",[380,475,476,481,483],{},[399,477,478],{"align":385},[245,479,480],{},"Contradiction Validation",[399,482,406],{"align":385},[399,484,485],{"align":385},"Flags incomplete reasoning and logical contradictions",[380,487,488,493,495],{},[399,489,490],{"align":385},[245,491,492],{},"Step-by-Step Validation",[399,494,406],{"align":385},[399,496,497],{"align":385},"Mandatory checkpoints at each workflow phase",[380,499,500,505,508],{},[399,501,502],{"align":385},[245,503,504],{},"Edge Case Expansion",[399,506,507],{"align":385},"gem-debugger, gem-planner",[399,509,510],{"align":385},"Red step now includes edge case enumeration",[322,512],{},[258,514,516],{"id":515},"why-this-matters","Why This Matters",[263,518,520],{"id":519},"from-vibe-coding-to-verified-engineering","From \"Vibe Coding\" to Verified Engineering",[241,522,523,524,527,528,531],{},"Early AI coding tools optimized for ",[274,525,526],{},"speed of generation",". gem-team optimizes for ",[245,529,530],{},"correctness of outcome",".",[241,533,534,535,537],{},"The v1.75.0 quality gates mean:\n-when an agent won't just \"write code\" — it will verify the code compiles, passes tests, meets accessibility standards, and doesn't introduce visual regressions ",[274,536,276],{}," presenting it to you.",[263,539,541],{"id":540},"agent-reasoning-you-can-audit","Agent Reasoning You Can Audit",[241,543,544,545,548,549,552,553,556],{},"The Devil's Advocate step, Differential Diagnosis, and Hypothesis-Driven Planning create an ",[245,546,547],{},"audit trail of reasoning",". You can see ",[274,550,551],{},"why"," an agent made a decision, not just ",[274,554,555],{},"what"," it produced.",[241,558,559],{},"This is critical for:",[561,562,563,570,576],"ul",{},[564,565,566,569],"li",{},[245,567,568],{},"Code review"," — Review the reasoning, not just the diff",[564,571,572,575],{},[245,573,574],{},"Compliance"," — Demonstrate due diligence in AI-assisted development",[564,577,578,581],{},[245,579,580],{},"Learning"," — Understand patterns that lead to better outcomes",[263,583,585],{"id":584},"production-ready-defaults","Production-Ready Defaults",[241,587,588,589,248,592,248,595,598],{},"The new configuration options (",[285,590,591],{},"visual_diff_threshold",[285,593,594],{},"a11y_audit_level",[285,596,597],{},"screenshot_on_failure",") have sensible defaults but are fully tunable. Teams can start strict and relax, or start permissive and tighten — the framework adapts to your maturity.",[322,600],{},[258,602,604],{"id":603},"migration-guide","Migration Guide",[241,606,607,610],{},[245,608,609],{},"No breaking changes"," in v1.75.0. All new features are opt-in via configuration.",[241,612,613,614,617],{},"To enable pre-flight verification globally, add to your ",[285,615,616],{},".gem-team.yaml",":",[619,620,624],"pre",{"className":621,"code":622,"language":623,"meta":183,"style":183},"language-yaml shiki shiki-themes material-theme-lighter github-light github-dark monokai","orchestrator:\n  pre_flight_verification: true\n  quality:\n    visual_diff_threshold: 0.02 # 2% pixel difference\n    a11y_audit_level: \"wcag-aa\"\n    testing:\n      screenshot_on_failure: true\n","yaml",[285,625,626,638,649,656,671,688,695],{"__ignoreMap":183},[627,628,630,634],"span",{"class":629,"line":115},"line",[627,631,633],{"class":632},"sHsBP","orchestrator",[627,635,637],{"class":636},"swvn1",":\n",[627,639,640,643,645],{"class":629,"line":121},[627,641,642],{"class":632},"  pre_flight_verification",[627,644,617],{"class":636},[627,646,648],{"class":647},"s8HiA"," true\n",[627,650,651,654],{"class":629,"line":14},[627,652,653],{"class":632},"  quality",[627,655,637],{"class":636},[627,657,658,661,663,667],{"class":629,"line":92},[627,659,660],{"class":632},"    visual_diff_threshold",[627,662,617],{"class":636},[627,664,666],{"class":665},"sYThS"," 0.02",[627,668,670],{"class":669},"ss7Ak"," # 2% pixel difference\n",[627,672,673,676,678,682,685],{"class":629,"line":104},[627,674,675],{"class":632},"    a11y_audit_level",[627,677,617],{"class":636},[627,679,681],{"class":680},"siCPE"," \"",[627,683,310],{"class":684},"sLACW",[627,686,687],{"class":680},"\"\n",[627,689,690,693],{"class":629,"line":109},[627,691,692],{"class":632},"    testing",[627,694,637],{"class":636},[627,696,698,701,703],{"class":629,"line":697},7,[627,699,700],{"class":632},"      screenshot_on_failure",[627,702,617],{"class":636},[627,704,648],{"class":647},[322,706],{},[258,708,710],{"id":709},"links-resources","Links & Resources",[561,712,713,725,735,745],{},[564,714,715,717,718],{},[245,716,50],{},": ",[719,720,724],"a",{"href":721,"rel":722},"https:\u002F\u002Fgithub.com\u002Fmubaidr\u002Fgem-team",[723],"nofollow","mubaidr\u002Fgem-team",[564,726,727,717,730],{},[245,728,729],{},"Changelog",[719,731,734],{"href":732,"rel":733},"https:\u002F\u002Fgithub.com\u002Fmubaidr\u002Fgem-team\u002Fblob\u002Fmain\u002FCHANGELOG.md",[723],"CHANGELOG.md",[564,736,737,717,740],{},[245,738,739],{},"Installation",[719,741,744],{"href":742,"rel":743},"https:\u002F\u002Fgithub.com\u002Fmubaidr\u002Fgem-team#quick-start",[723],"Quick Start Guide",[564,746,747,717,750],{},[245,748,749],{},"Documentation",[719,751,754],{"href":752,"rel":753},"https:\u002F\u002Fgithub.com\u002Fmubaidr\u002Fgem-team\u002Ftree\u002Fmain\u002F.apm\u002Fagents",[723],"Agent Reference",[322,756],{},[258,758,760],{"id":759},"whats-next","What's Next",[241,762,763],{},"v1.76.0 will focus on:",[561,765,766,772,778],{},[564,767,768,771],{},[245,769,770],{},"Skill extraction automation"," — Convert successful patterns into reusable agent skills",[564,773,774,777],{},[245,775,776],{},"Cross-agent memory sharing"," — Context envelope improvements for multi-agent workflows",[564,779,780,783],{},[245,781,782],{},"MCP server integration"," — Native Model Context Protocol support for external tool access",[322,785],{},[241,787,788],{},[274,789,790,791,795],{},"gem-team is an open-source multi-agent orchestration framework for AI-assisted development. Built by developers, for developers. ",[719,792,794],{"href":721,"rel":793},[723],"Star us on GitHub"," if you find it useful.",[797,798,799],"style",{},"html pre.shiki code .sHsBP, html code.shiki .sHsBP{--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D;--shiki-sepia:#F92672}html pre.shiki code .swvn1, html code.shiki .swvn1{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .s8HiA, html code.shiki .s8HiA{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sYThS, html code.shiki .sYThS{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .ss7Ak, html code.shiki .ss7Ak{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit;--shiki-sepia:#88846F;--shiki-sepia-font-style:inherit}html pre.shiki code .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"title":183,"searchDepth":121,"depth":121,"links":801},[802,807,812,813,814],{"id":260,"depth":121,"text":261,"children":803},[804,805,806],{"id":265,"depth":14,"text":266},{"id":326,"depth":14,"text":327},{"id":364,"depth":14,"text":365},{"id":515,"depth":121,"text":516,"children":808},[809,810,811],{"id":519,"depth":14,"text":520},{"id":540,"depth":14,"text":541},{"id":584,"depth":14,"text":585},{"id":603,"depth":121,"text":604},{"id":709,"depth":121,"text":710},{"id":759,"depth":121,"text":760},"2026-06-22","gem-team v1.75.0 introduces pre-flight verification steps, visual diff thresholds, accessibility audit levels, and major agent workflow improvements including Devil's Advocate, Differential Diagnosis, and Hypothesis-driven planning.","md","\u002Fimg\u002Fblog\u002F36-gem-team-v1.75.0-release\u002Fbanner.svg",{"readingTime":820},"6 min read","\u002Fblog\u002F36-gem-team-v1.75.0-release",{"title":235,"description":816},"blog\u002F36-gem-team-v1.75.0-release",[825,826,827,828,829],"Gem Team","AI Agents","Open Source","Release Notes","Developer Tools","VOcmxWbMsNsAXVncuh6jLg6GBmx9-iapSl7PmGakb6Y",{"id":832,"title":833,"abstract":834,"author":236,"authorUrl":835,"body":836,"date":2776,"dateUpdated":2776,"description":2777,"excerpt":171,"extension":817,"featured":135,"headline":833,"image":171,"meta":2778,"navigation":135,"ogImage":171,"path":2780,"seo":2781,"series":2782,"seriesDescription":2783,"seriesOrder":115,"socialImage":2784,"stem":2790,"tags":2791,"__hash__":2797},"blog\u002Fblog\u002F42-laravel-ai-agent-hybrid-backends.md","Laravel + AI Agent Systems: Building Hybrid Backends in 2026","Laravel is an excellent orchestration backend for AI agents. Queue-driven execution, structured API gateways for LLM systems, and proven SaaS patterns for hybrid architectures.","https:\u002F\u002Fmubaidr.js.org",{"type":238,"value":837,"toc":2763},[838,841,844,847,851,854,861,893,896,900,903,911,914,917,921,924,927,1375,1378,1404,1408,1411,1436,1439,1443,1446,1589,1596,1600,1603,2248,2251,2275,2279,2282,2285,2333,2336,2342,2432,2435,2560,2563,2567,2570,2708,2711,2715,2718,2744,2747,2751,2754,2757,2760],[258,839,833],{"id":840},"laravel-ai-agent-systems-building-hybrid-backends-in-2026",[241,842,843],{},"When I started building AI agent systems in early 2025, my first instinct was to reach for Python. Everyone was using Python for AI work — LangChain, LlamaIndex, FastAPI. But as I moved from prototypes to production SaaS products, I kept running into the same problems: no built-in queue system, weak job persistence, and a fragmented ecosystem for background workers.",[241,845,846],{},"I chose Laravel. Not because PHP is better at ML inference, but because Laravel is better at the orchestration layer that surrounds AI agents. This post explains the architecture I settled on after a year of iteration, and why I believe Laravel is the best choice for hybrid backends that connect traditional web applications with AI agent systems.",[258,848,850],{"id":849},"why-laravel-for-ai-orchestration","Why Laravel for AI Orchestration?",[241,852,853],{},"The misconception that AI backends require Python ignores a critical distinction: running ML models and orchestrating AI agents are two different problems. Model training and inference benefit from Python's scientific computing ecosystem. Agent orchestration benefits from battle-tested job queues, database abstractions, and API management — all areas where Laravel excels out of the box.",[241,855,856,857,860],{},"My production setup uses Laravel for everything that happens ",[274,858,859],{},"around"," AI agents:",[561,862,863,869,875,881,887],{},[564,864,865,868],{},[245,866,867],{},"Queue management",": Laravel Horizon with Redis for dispatching and monitoring agent tasks",[564,870,871,874],{},[245,872,873],{},"State persistence",": MySQL for agent session state, task history, and result storage",[564,876,877,880],{},[245,878,879],{},"API gateway",": Rate limiting, token budgeting, and response caching for LLM calls",[564,882,883,886],{},[245,884,885],{},"User management",": Authentication, team accounts, billing — the standard SaaS stack",[564,888,889,892],{},[245,890,891],{},"Event system",": Broadcasting agent status updates via Laravel Reverb for real-time UI",[241,894,895],{},"The AI agents themselves run as isolated PHP worker processes, with the heavy inference work delegated to external LLM APIs via the gateway layer. This separation means I can scale the orchestration and inference layers independently.",[258,897,899],{"id":898},"architecture-overview","Architecture Overview",[241,901,902],{},"The system breaks down into four layers that communicate through well-defined interfaces:",[619,904,909],{"className":905,"code":907,"language":908,"meta":183},[906],"language-text","User Request (HTTP)        API Gateway (REST)\n       |                        |\n       v                        v\n  Laravel App \u003C--> Queue Dispatcher (Redis\u002FHorizon)\n       |                        |\n       |                        v\n       |                   Agent Workers (PHP)\n       |                        |\n       |                        v\n       |                   LLM Gateway (Rate-limited, Cached)\n       |                        |\n       |                        v\n       v                   External LLM APIs\n  Database\u002FRedis         (OpenAI, Claude, etc.)\n","text",[285,910,907],{"__ignoreMap":183},[241,912,913],{},"The flow works like this: a user submits a request through the Laravel app. The app validates the input, creates a task record in the database, and dispatches a job to the queue. An agent worker picks up the job, processes it through a series of orchestrated LLM calls (all routed through the API gateway), and writes the result back to the database. An event fires, and the user's UI updates via server-sent events or WebSockets.",[241,915,916],{},"This async, queue-driven pattern is the core architectural decision. Every agent task goes through a queue, which gives me retry logic, failure handling, and horizontal scaling without modifying application code.",[258,918,920],{"id":919},"queue-driven-agent-execution","Queue-Driven Agent Execution",[241,922,923],{},"The heart of the system is a Laravel job that orchestrates multi-step AI agent tasks. I use a pattern where a single \"orchestrator\" job dispatches sub-tasks as separate jobs, giving me granular control over failures and retries.",[241,925,926],{},"Here is the core job class that processes a document analysis task:",[619,928,932],{"className":929,"code":930,"language":931,"meta":183,"style":183},"language-php shiki shiki-themes material-theme-lighter github-light github-dark monokai","\u003C?php\n\nnamespace App\\Jobs\\Agent;\n\nuse App\\Models\\AgentTask;\nuse App\\Services\\LlmGateway;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass ProcessAgentTask implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, SerializesModels;\n\n    public int $timeout = 300;\n    public int $tries = 3;\n    public array $backoff = [10, 30, 60];\n\n    public function __construct(\n        public AgentTask $task\n    ) {}\n\n    public function handle(LlmGateway $gateway): void\n    {\n        $this->task->update(['status' => 'processing']);\n\n        try {\n            \u002F\u002F Step 1: Analyze the document\n            $analysis = $gateway->chat(\n                systemPrompt: 'You are a document analysis agent...',\n                messages: [\n                    ['role' => 'user', 'content' => $this->task->input['document_text']],\n                ],\n                options: ['model' => 'gpt-4o', 'max_tokens' => 2000]\n            );\n\n            \u002F\u002F Step 2: Extract structured data\n            $extraction = $gateway->chat(\n                systemPrompt: 'Extract structured data from the analysis...',\n                messages: [\n                    ['role' => 'assistant', 'content' => $analysis],\n                    ['role' => 'user', 'content' => 'Return JSON with fields: summary, key_points, entities'],\n                ],\n                options: ['model' => 'gpt-4o', 'response_format' => 'json']\n            );\n\n            \u002F\u002F Step 3: Store results and dispatch follow-up jobs\n            $this->task->update([\n                'status' => 'completed',\n                'result' => json_decode($extraction, true),\n                'completed_at' => now(),\n            ]);\n\n            \u002F\u002F Dispatch post-processing as a separate job chain\n            PostProcessAgentResult::dispatch($this->task)\n                ->onQueue('post-processing');\n\n            Event::dispatch('agent.task.completed', [$this->task]);\n\n        } catch (\\Throwable $e) {\n            Log::error('Agent task failed', [\n                'task_id' => $this->task->id,\n                'error' => $e->getMessage(),\n            ]);\n\n            $this->task->update([\n                'status' => 'failed',\n                'error' => $e->getMessage(),\n                'attempts' => $this->attempts(),\n            ]);\n\n            throw $e;\n        }\n    }\n}\n","php",[285,933,934,939,944,949,953,958,963,968,973,979,984,989,995,1000,1005,1010,1015,1021,1026,1032,1038,1044,1049,1055,1061,1067,1072,1078,1084,1090,1095,1101,1107,1113,1119,1125,1131,1137,1143,1149,1154,1160,1166,1172,1177,1183,1189,1194,1200,1205,1210,1216,1222,1228,1234,1240,1246,1251,1257,1263,1269,1274,1280,1285,1291,1297,1303,1309,1314,1319,1324,1330,1335,1341,1346,1351,1357,1363,1369],{"__ignoreMap":183},[627,935,936],{"class":629,"line":115},[627,937,938],{},"\u003C?php\n",[627,940,941],{"class":629,"line":121},[627,942,943],{"emptyLinePlaceholder":135},"\n",[627,945,946],{"class":629,"line":14},[627,947,948],{},"namespace App\\Jobs\\Agent;\n",[627,950,951],{"class":629,"line":92},[627,952,943],{"emptyLinePlaceholder":135},[627,954,955],{"class":629,"line":104},[627,956,957],{},"use App\\Models\\AgentTask;\n",[627,959,960],{"class":629,"line":109},[627,961,962],{},"use App\\Services\\LlmGateway;\n",[627,964,965],{"class":629,"line":697},[627,966,967],{},"use Illuminate\\Contracts\\Queue\\ShouldQueue;\n",[627,969,970],{"class":629,"line":125},[627,971,972],{},"use Illuminate\\Foundation\\Bus\\Dispatchable;\n",[627,974,976],{"class":629,"line":975},9,[627,977,978],{},"use Illuminate\\Foundation\\Bus\\PendingDispatch;\n",[627,980,981],{"class":629,"line":119},[627,982,983],{},"use Illuminate\\Queue\\InteractsWithQueue;\n",[627,985,986],{"class":629,"line":97},[627,987,988],{},"use Illuminate\\Queue\\SerializesModels;\n",[627,990,992],{"class":629,"line":991},12,[627,993,994],{},"use Illuminate\\Support\\Facades\\Event;\n",[627,996,997],{"class":629,"line":27},[627,998,999],{},"use Illuminate\\Support\\Facades\\Log;\n",[627,1001,1003],{"class":629,"line":1002},14,[627,1004,943],{"emptyLinePlaceholder":135},[627,1006,1007],{"class":629,"line":102},[627,1008,1009],{},"class ProcessAgentTask implements ShouldQueue\n",[627,1011,1012],{"class":629,"line":113},[627,1013,1014],{},"{\n",[627,1016,1018],{"class":629,"line":1017},17,[627,1019,1020],{},"    use Dispatchable, InteractsWithQueue, SerializesModels;\n",[627,1022,1024],{"class":629,"line":1023},18,[627,1025,943],{"emptyLinePlaceholder":135},[627,1027,1029],{"class":629,"line":1028},19,[627,1030,1031],{},"    public int $timeout = 300;\n",[627,1033,1035],{"class":629,"line":1034},20,[627,1036,1037],{},"    public int $tries = 3;\n",[627,1039,1041],{"class":629,"line":1040},21,[627,1042,1043],{},"    public array $backoff = [10, 30, 60];\n",[627,1045,1047],{"class":629,"line":1046},22,[627,1048,943],{"emptyLinePlaceholder":135},[627,1050,1052],{"class":629,"line":1051},23,[627,1053,1054],{},"    public function __construct(\n",[627,1056,1058],{"class":629,"line":1057},24,[627,1059,1060],{},"        public AgentTask $task\n",[627,1062,1064],{"class":629,"line":1063},25,[627,1065,1066],{},"    ) {}\n",[627,1068,1070],{"class":629,"line":1069},26,[627,1071,943],{"emptyLinePlaceholder":135},[627,1073,1075],{"class":629,"line":1074},27,[627,1076,1077],{},"    public function handle(LlmGateway $gateway): void\n",[627,1079,1081],{"class":629,"line":1080},28,[627,1082,1083],{},"    {\n",[627,1085,1087],{"class":629,"line":1086},29,[627,1088,1089],{},"        $this->task->update(['status' => 'processing']);\n",[627,1091,1093],{"class":629,"line":1092},30,[627,1094,943],{"emptyLinePlaceholder":135},[627,1096,1098],{"class":629,"line":1097},31,[627,1099,1100],{},"        try {\n",[627,1102,1104],{"class":629,"line":1103},32,[627,1105,1106],{},"            \u002F\u002F Step 1: Analyze the document\n",[627,1108,1110],{"class":629,"line":1109},33,[627,1111,1112],{},"            $analysis = $gateway->chat(\n",[627,1114,1116],{"class":629,"line":1115},34,[627,1117,1118],{},"                systemPrompt: 'You are a document analysis agent...',\n",[627,1120,1122],{"class":629,"line":1121},35,[627,1123,1124],{},"                messages: [\n",[627,1126,1128],{"class":629,"line":1127},36,[627,1129,1130],{},"                    ['role' => 'user', 'content' => $this->task->input['document_text']],\n",[627,1132,1134],{"class":629,"line":1133},37,[627,1135,1136],{},"                ],\n",[627,1138,1140],{"class":629,"line":1139},38,[627,1141,1142],{},"                options: ['model' => 'gpt-4o', 'max_tokens' => 2000]\n",[627,1144,1146],{"class":629,"line":1145},39,[627,1147,1148],{},"            );\n",[627,1150,1152],{"class":629,"line":1151},40,[627,1153,943],{"emptyLinePlaceholder":135},[627,1155,1157],{"class":629,"line":1156},41,[627,1158,1159],{},"            \u002F\u002F Step 2: Extract structured data\n",[627,1161,1163],{"class":629,"line":1162},42,[627,1164,1165],{},"            $extraction = $gateway->chat(\n",[627,1167,1169],{"class":629,"line":1168},43,[627,1170,1171],{},"                systemPrompt: 'Extract structured data from the analysis...',\n",[627,1173,1175],{"class":629,"line":1174},44,[627,1176,1124],{},[627,1178,1180],{"class":629,"line":1179},45,[627,1181,1182],{},"                    ['role' => 'assistant', 'content' => $analysis],\n",[627,1184,1186],{"class":629,"line":1185},46,[627,1187,1188],{},"                    ['role' => 'user', 'content' => 'Return JSON with fields: summary, key_points, entities'],\n",[627,1190,1192],{"class":629,"line":1191},47,[627,1193,1136],{},[627,1195,1197],{"class":629,"line":1196},48,[627,1198,1199],{},"                options: ['model' => 'gpt-4o', 'response_format' => 'json']\n",[627,1201,1203],{"class":629,"line":1202},49,[627,1204,1148],{},[627,1206,1208],{"class":629,"line":1207},50,[627,1209,943],{"emptyLinePlaceholder":135},[627,1211,1213],{"class":629,"line":1212},51,[627,1214,1215],{},"            \u002F\u002F Step 3: Store results and dispatch follow-up jobs\n",[627,1217,1219],{"class":629,"line":1218},52,[627,1220,1221],{},"            $this->task->update([\n",[627,1223,1225],{"class":629,"line":1224},53,[627,1226,1227],{},"                'status' => 'completed',\n",[627,1229,1231],{"class":629,"line":1230},54,[627,1232,1233],{},"                'result' => json_decode($extraction, true),\n",[627,1235,1237],{"class":629,"line":1236},55,[627,1238,1239],{},"                'completed_at' => now(),\n",[627,1241,1243],{"class":629,"line":1242},56,[627,1244,1245],{},"            ]);\n",[627,1247,1249],{"class":629,"line":1248},57,[627,1250,943],{"emptyLinePlaceholder":135},[627,1252,1254],{"class":629,"line":1253},58,[627,1255,1256],{},"            \u002F\u002F Dispatch post-processing as a separate job chain\n",[627,1258,1260],{"class":629,"line":1259},59,[627,1261,1262],{},"            PostProcessAgentResult::dispatch($this->task)\n",[627,1264,1266],{"class":629,"line":1265},60,[627,1267,1268],{},"                ->onQueue('post-processing');\n",[627,1270,1272],{"class":629,"line":1271},61,[627,1273,943],{"emptyLinePlaceholder":135},[627,1275,1277],{"class":629,"line":1276},62,[627,1278,1279],{},"            Event::dispatch('agent.task.completed', [$this->task]);\n",[627,1281,1283],{"class":629,"line":1282},63,[627,1284,943],{"emptyLinePlaceholder":135},[627,1286,1288],{"class":629,"line":1287},64,[627,1289,1290],{},"        } catch (\\Throwable $e) {\n",[627,1292,1294],{"class":629,"line":1293},65,[627,1295,1296],{},"            Log::error('Agent task failed', [\n",[627,1298,1300],{"class":629,"line":1299},66,[627,1301,1302],{},"                'task_id' => $this->task->id,\n",[627,1304,1306],{"class":629,"line":1305},67,[627,1307,1308],{},"                'error' => $e->getMessage(),\n",[627,1310,1312],{"class":629,"line":1311},68,[627,1313,1245],{},[627,1315,1317],{"class":629,"line":1316},69,[627,1318,943],{"emptyLinePlaceholder":135},[627,1320,1322],{"class":629,"line":1321},70,[627,1323,1221],{},[627,1325,1327],{"class":629,"line":1326},71,[627,1328,1329],{},"                'status' => 'failed',\n",[627,1331,1333],{"class":629,"line":1332},72,[627,1334,1308],{},[627,1336,1338],{"class":629,"line":1337},73,[627,1339,1340],{},"                'attempts' => $this->attempts(),\n",[627,1342,1344],{"class":629,"line":1343},74,[627,1345,1245],{},[627,1347,1349],{"class":629,"line":1348},75,[627,1350,943],{"emptyLinePlaceholder":135},[627,1352,1354],{"class":629,"line":1353},76,[627,1355,1356],{},"            throw $e;\n",[627,1358,1360],{"class":629,"line":1359},77,[627,1361,1362],{},"        }\n",[627,1364,1366],{"class":629,"line":1365},78,[627,1367,1368],{},"    }\n",[627,1370,1372],{"class":629,"line":1371},79,[627,1373,1374],{},"}\n",[241,1376,1377],{},"Key design decisions in this job:",[561,1379,1380,1386,1392,1398],{},[564,1381,1382,1385],{},[245,1383,1384],{},"SerializesModels"," ensures the task model is re-retrieved from the database when the job executes, preventing stale data issues on retry.",[564,1387,1388,1391],{},[245,1389,1390],{},"The backoff array"," implements progressive delay: 10 seconds, then 30, then 60. Most transient LLM API failures resolve within 60 seconds.",[564,1393,1394,1397],{},[245,1395,1396],{},"Timeout of 300 seconds"," accounts for LLM API latency. GPT-4o responses can take 15-45 seconds for complex prompts, and multi-step chains multiply that.",[564,1399,1400,1403],{},[245,1401,1402],{},"Post-processing is a separate job"," on a different queue, isolating the main agent pipeline from side effects like indexing, notifications, or webhook delivery.",[258,1405,1407],{"id":1406},"event-driven-result-handling","Event-Driven Result Handling",[241,1409,1410],{},"Jobs complete asynchronously, so the user needs a way to get results without polling. I use Laravel's broadcasting system with Reverb to push status updates:",[619,1412,1414],{"className":929,"code":1413,"language":931,"meta":183,"style":183},"\u002F\u002F In a service provider boot method\nEvent::listen('agent.task.completed', function (AgentTask $task) {\n    broadcast(new AgentTaskCompleted($task))->toOthers();\n});\n",[285,1415,1416,1421,1426,1431],{"__ignoreMap":183},[627,1417,1418],{"class":629,"line":115},[627,1419,1420],{},"\u002F\u002F In a service provider boot method\n",[627,1422,1423],{"class":629,"line":121},[627,1424,1425],{},"Event::listen('agent.task.completed', function (AgentTask $task) {\n",[627,1427,1428],{"class":629,"line":14},[627,1429,1430],{},"    broadcast(new AgentTaskCompleted($task))->toOthers();\n",[627,1432,1433],{"class":629,"line":92},[627,1434,1435],{},"});\n",[241,1437,1438],{},"The frontend receives these events and updates the UI in real time. For users who close their browser, the system sends a notification once the task completes — handled by a separate listener that dispatches a notification job.",[258,1440,1442],{"id":1441},"hybrid-architecture-diagram","Hybrid Architecture Diagram",[241,1444,1445],{},"The following diagram shows the complete request flow through the hybrid backend:",[619,1447,1451],{"className":1448,"code":1449,"language":1450,"meta":183,"style":183},"language-mermaid shiki shiki-themes material-theme-lighter github-light github-dark monokai","sequenceDiagram\n    participant User as User\u002FClient\n    participant Laravel as Laravel App\n    participant Gateway as LLM API Gateway\n    participant Horizon as Queue (Redis\u002FHorizon)\n    participant Worker as Agent Worker\n    participant LLM as External LLM API\n\n    User->>Laravel: Submit document for analysis\n    Laravel->>Laravel: Validate input, create AgentTask\n    Laravel->>Horizon: Dispatch ProcessAgentTask job\n    Laravel-->>User: Return task ID (202 Accepted)\n\n    Horizon->>Worker: Pop job from queue\n    Worker->>Gateway: Request LLM analysis\n    Gateway->>Gateway: Check rate limit & token budget\n    Gateway->>LLM: Forward prompt\n    LLM-->>Gateway: Return analysis response\n    Gateway->>Gateway: Cache response, deduct tokens\n    Gateway-->>Worker: Return structured result\n\n    Worker->>Worker: Process & extract data\n    Worker->>Laravel: Update task status (completed)\n    Worker->>Horizon: Dispatch PostProcessAgentResult\n\n    Laravel-->>User: SSE\u002FWebSocket: task.completed\n    User->>Laravel: Fetch processed result by task ID\n    Laravel-->>User: Return full analysis\n","mermaid",[285,1452,1453,1458,1463,1468,1473,1478,1483,1488,1492,1497,1502,1507,1512,1516,1521,1526,1531,1536,1541,1546,1551,1555,1560,1565,1570,1574,1579,1584],{"__ignoreMap":183},[627,1454,1455],{"class":629,"line":115},[627,1456,1457],{},"sequenceDiagram\n",[627,1459,1460],{"class":629,"line":121},[627,1461,1462],{},"    participant User as User\u002FClient\n",[627,1464,1465],{"class":629,"line":14},[627,1466,1467],{},"    participant Laravel as Laravel App\n",[627,1469,1470],{"class":629,"line":92},[627,1471,1472],{},"    participant Gateway as LLM API Gateway\n",[627,1474,1475],{"class":629,"line":104},[627,1476,1477],{},"    participant Horizon as Queue (Redis\u002FHorizon)\n",[627,1479,1480],{"class":629,"line":109},[627,1481,1482],{},"    participant Worker as Agent Worker\n",[627,1484,1485],{"class":629,"line":697},[627,1486,1487],{},"    participant LLM as External LLM API\n",[627,1489,1490],{"class":629,"line":125},[627,1491,943],{"emptyLinePlaceholder":135},[627,1493,1494],{"class":629,"line":975},[627,1495,1496],{},"    User->>Laravel: Submit document for analysis\n",[627,1498,1499],{"class":629,"line":119},[627,1500,1501],{},"    Laravel->>Laravel: Validate input, create AgentTask\n",[627,1503,1504],{"class":629,"line":97},[627,1505,1506],{},"    Laravel->>Horizon: Dispatch ProcessAgentTask job\n",[627,1508,1509],{"class":629,"line":991},[627,1510,1511],{},"    Laravel-->>User: Return task ID (202 Accepted)\n",[627,1513,1514],{"class":629,"line":27},[627,1515,943],{"emptyLinePlaceholder":135},[627,1517,1518],{"class":629,"line":1002},[627,1519,1520],{},"    Horizon->>Worker: Pop job from queue\n",[627,1522,1523],{"class":629,"line":102},[627,1524,1525],{},"    Worker->>Gateway: Request LLM analysis\n",[627,1527,1528],{"class":629,"line":113},[627,1529,1530],{},"    Gateway->>Gateway: Check rate limit & token budget\n",[627,1532,1533],{"class":629,"line":1017},[627,1534,1535],{},"    Gateway->>LLM: Forward prompt\n",[627,1537,1538],{"class":629,"line":1023},[627,1539,1540],{},"    LLM-->>Gateway: Return analysis response\n",[627,1542,1543],{"class":629,"line":1028},[627,1544,1545],{},"    Gateway->>Gateway: Cache response, deduct tokens\n",[627,1547,1548],{"class":629,"line":1034},[627,1549,1550],{},"    Gateway-->>Worker: Return structured result\n",[627,1552,1553],{"class":629,"line":1040},[627,1554,943],{"emptyLinePlaceholder":135},[627,1556,1557],{"class":629,"line":1046},[627,1558,1559],{},"    Worker->>Worker: Process & extract data\n",[627,1561,1562],{"class":629,"line":1051},[627,1563,1564],{},"    Worker->>Laravel: Update task status (completed)\n",[627,1566,1567],{"class":629,"line":1057},[627,1568,1569],{},"    Worker->>Horizon: Dispatch PostProcessAgentResult\n",[627,1571,1572],{"class":629,"line":1063},[627,1573,943],{"emptyLinePlaceholder":135},[627,1575,1576],{"class":629,"line":1069},[627,1577,1578],{},"    Laravel-->>User: SSE\u002FWebSocket: task.completed\n",[627,1580,1581],{"class":629,"line":1074},[627,1582,1583],{},"    User->>Laravel: Fetch processed result by task ID\n",[627,1585,1586],{"class":629,"line":1080},[627,1587,1588],{},"    Laravel-->>User: Return full analysis\n",[241,1590,1591,1592,1595],{},"The critical insight in this architecture is the ",[245,1593,1594],{},"LLM API Gateway",". Every request to an external language model passes through this middleware layer, which handles concerns that would otherwise be scattered across every job class.",[258,1597,1599],{"id":1598},"building-the-llm-api-gateway","Building the LLM API Gateway",[241,1601,1602],{},"The gateway is a dedicated service class that wraps all external LLM calls. I built it because I discovered early that direct API calls from job classes lead to duplicate rate-limiting logic, inconsistent error handling, and no centralized observability.",[619,1604,1606],{"className":929,"code":1605,"language":931,"meta":183,"style":183},"\u003C?php\n\nnamespace App\\Services;\n\nuse App\\Models\\ApiTokenUsage;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass LlmGateway\n{\n    private const CACHE_TTL = 3600; \u002F\u002F 1 hour for identical prompts\n    private const RATE_LIMIT_KEY = 'llm_rate_limit:';\n    private const TOKEN_BUDGET_KEY = 'llm_token_budget:';\n    private const MAX_TOKENS_PER_MINUTE = 100000;\n    private const MAX_TOKENS_PER_DAY = 10000000;\n\n    public function chat(\n        string $systemPrompt,\n        array $messages,\n        array $options = []\n    ): string {\n        $cacheKey = $this->buildCacheKey($systemPrompt, $messages, $options);\n\n        \u002F\u002F Check cache first for identical requests\n        if ($cached = Cache::get($cacheKey)) {\n            Log::debug('LLM cache hit', ['cache_key' => $cacheKey]);\n            return $cached;\n        }\n\n        \u002F\u002F Enforce rate limits\n        $this->checkRateLimit();\n        $this->checkTokenBudget($options['max_tokens'] ?? 1000);\n\n        \u002F\u002F Make the API call\n        $response = Http::timeout(120)\n            ->withToken(config('services.openai.api_key'))\n            ->post('https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fchat\u002Fcompletions', [\n                'model' => $options['model'] ?? 'gpt-4o',\n                'messages' => array_merge(\n                    [['role' => 'system', 'content' => $systemPrompt]],\n                    $messages\n                ),\n                'max_tokens' => $options['max_tokens'] ?? 1000,\n                'temperature' => $options['temperature'] ?? 0.3,\n                'response_format' => $options['response_format'] ?? null,\n            ]);\n\n        if ($response->failed()) {\n            Log::error('LLM API call failed', [\n                'status' => $response->status(),\n                'body' => $response->body(),\n            ]);\n            throw new \\RuntimeException(\n                'LLM API error: ' . $response->body()\n            );\n        }\n\n        $result = $response->json('choices.0.message.content');\n        $tokensUsed = $response->json('usage.total_tokens');\n\n        \u002F\u002F Track usage\n        $this->trackTokenUsage($tokensUsed);\n\n        \u002F\u002F Cache identical requests\n        if ($tokensUsed > 50) {\n            Cache::put($cacheKey, $result, self::CACHE_TTL);\n        }\n\n        return $result;\n    }\n\n    private function checkRateLimit(): void\n    {\n        $key = self::RATE_LIMIT_KEY . (string) time();\n        $count = Cache::increment($key, 1, 60);\n\n        if ($count > 100) {\n            throw new \\RuntimeException('LLM rate limit exceeded');\n        }\n    }\n\n    private function checkTokenBudget(int $requestedTokens): void\n    {\n        $dailyKey = self::TOKEN_BUDGET_KEY . now()->format('Y-m-d');\n        $dailyUsage = Cache::get($dailyKey, 0);\n\n        if ($dailyUsage + $requestedTokens > self::MAX_TOKENS_PER_DAY) {\n            throw new \\RuntimeException('Daily token budget exhausted');\n        }\n\n        $minuteKey = self::TOKEN_BUDGET_KEY . now()->format('Y-m-d-H-i');\n        $minuteUsage = Cache::get($minuteKey, 0);\n\n        if ($minuteUsage + $requestedTokens > self::MAX_TOKENS_PER_MINUTE) {\n            throw new \\RuntimeException('Minute token budget exceeded');\n        }\n    }\n\n    private function trackTokenUsage(int $tokens): void\n    {\n        ApiTokenUsage::create([\n            'tokens' => $tokens,\n            'model' => 'gpt-4o',\n            'recorded_at' => now(),\n        ]);\n\n        Cache::increment(\n            self::TOKEN_BUDGET_KEY . now()->format('Y-m-d'),\n            $tokens\n        );\n        Cache::increment(\n            self::TOKEN_BUDGET_KEY . now()->format('Y-m-d-H-i'),\n            $tokens,\n            120\n        );\n    }\n\n    private function buildCacheKey(\n        string $system,\n        array $messages,\n        array $options\n    ): string {\n        return 'llm_response:' . md5(\n            serialize([$system, $messages, $options])\n        );\n    }\n}\n",[285,1607,1608,1612,1616,1621,1625,1630,1635,1640,1644,1648,1653,1657,1662,1667,1672,1677,1682,1686,1691,1696,1701,1706,1711,1716,1720,1725,1730,1735,1740,1744,1748,1753,1758,1763,1767,1772,1777,1782,1787,1792,1797,1802,1807,1812,1817,1822,1827,1831,1835,1840,1845,1850,1855,1859,1864,1869,1873,1877,1881,1886,1891,1895,1900,1905,1909,1914,1919,1924,1928,1932,1937,1941,1945,1950,1954,1959,1964,1968,1973,1978,1983,1988,1993,1999,2004,2010,2016,2021,2027,2033,2038,2043,2049,2055,2060,2066,2072,2077,2082,2087,2092,2097,2103,2109,2115,2121,2127,2132,2138,2144,2150,2156,2161,2167,2173,2179,2184,2189,2194,2200,2206,2211,2217,2222,2228,2233,2238,2243],{"__ignoreMap":183},[627,1609,1610],{"class":629,"line":115},[627,1611,938],{},[627,1613,1614],{"class":629,"line":121},[627,1615,943],{"emptyLinePlaceholder":135},[627,1617,1618],{"class":629,"line":14},[627,1619,1620],{},"namespace App\\Services;\n",[627,1622,1623],{"class":629,"line":92},[627,1624,943],{"emptyLinePlaceholder":135},[627,1626,1627],{"class":629,"line":104},[627,1628,1629],{},"use App\\Models\\ApiTokenUsage;\n",[627,1631,1632],{"class":629,"line":109},[627,1633,1634],{},"use Illuminate\\Support\\Facades\\Cache;\n",[627,1636,1637],{"class":629,"line":697},[627,1638,1639],{},"use Illuminate\\Support\\Facades\\Http;\n",[627,1641,1642],{"class":629,"line":125},[627,1643,999],{},[627,1645,1646],{"class":629,"line":975},[627,1647,943],{"emptyLinePlaceholder":135},[627,1649,1650],{"class":629,"line":119},[627,1651,1652],{},"class LlmGateway\n",[627,1654,1655],{"class":629,"line":97},[627,1656,1014],{},[627,1658,1659],{"class":629,"line":991},[627,1660,1661],{},"    private const CACHE_TTL = 3600; \u002F\u002F 1 hour for identical prompts\n",[627,1663,1664],{"class":629,"line":27},[627,1665,1666],{},"    private const RATE_LIMIT_KEY = 'llm_rate_limit:';\n",[627,1668,1669],{"class":629,"line":1002},[627,1670,1671],{},"    private const TOKEN_BUDGET_KEY = 'llm_token_budget:';\n",[627,1673,1674],{"class":629,"line":102},[627,1675,1676],{},"    private const MAX_TOKENS_PER_MINUTE = 100000;\n",[627,1678,1679],{"class":629,"line":113},[627,1680,1681],{},"    private const MAX_TOKENS_PER_DAY = 10000000;\n",[627,1683,1684],{"class":629,"line":1017},[627,1685,943],{"emptyLinePlaceholder":135},[627,1687,1688],{"class":629,"line":1023},[627,1689,1690],{},"    public function chat(\n",[627,1692,1693],{"class":629,"line":1028},[627,1694,1695],{},"        string $systemPrompt,\n",[627,1697,1698],{"class":629,"line":1034},[627,1699,1700],{},"        array $messages,\n",[627,1702,1703],{"class":629,"line":1040},[627,1704,1705],{},"        array $options = []\n",[627,1707,1708],{"class":629,"line":1046},[627,1709,1710],{},"    ): string {\n",[627,1712,1713],{"class":629,"line":1051},[627,1714,1715],{},"        $cacheKey = $this->buildCacheKey($systemPrompt, $messages, $options);\n",[627,1717,1718],{"class":629,"line":1057},[627,1719,943],{"emptyLinePlaceholder":135},[627,1721,1722],{"class":629,"line":1063},[627,1723,1724],{},"        \u002F\u002F Check cache first for identical requests\n",[627,1726,1727],{"class":629,"line":1069},[627,1728,1729],{},"        if ($cached = Cache::get($cacheKey)) {\n",[627,1731,1732],{"class":629,"line":1074},[627,1733,1734],{},"            Log::debug('LLM cache hit', ['cache_key' => $cacheKey]);\n",[627,1736,1737],{"class":629,"line":1080},[627,1738,1739],{},"            return $cached;\n",[627,1741,1742],{"class":629,"line":1086},[627,1743,1362],{},[627,1745,1746],{"class":629,"line":1092},[627,1747,943],{"emptyLinePlaceholder":135},[627,1749,1750],{"class":629,"line":1097},[627,1751,1752],{},"        \u002F\u002F Enforce rate limits\n",[627,1754,1755],{"class":629,"line":1103},[627,1756,1757],{},"        $this->checkRateLimit();\n",[627,1759,1760],{"class":629,"line":1109},[627,1761,1762],{},"        $this->checkTokenBudget($options['max_tokens'] ?? 1000);\n",[627,1764,1765],{"class":629,"line":1115},[627,1766,943],{"emptyLinePlaceholder":135},[627,1768,1769],{"class":629,"line":1121},[627,1770,1771],{},"        \u002F\u002F Make the API call\n",[627,1773,1774],{"class":629,"line":1127},[627,1775,1776],{},"        $response = Http::timeout(120)\n",[627,1778,1779],{"class":629,"line":1133},[627,1780,1781],{},"            ->withToken(config('services.openai.api_key'))\n",[627,1783,1784],{"class":629,"line":1139},[627,1785,1786],{},"            ->post('https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fchat\u002Fcompletions', [\n",[627,1788,1789],{"class":629,"line":1145},[627,1790,1791],{},"                'model' => $options['model'] ?? 'gpt-4o',\n",[627,1793,1794],{"class":629,"line":1151},[627,1795,1796],{},"                'messages' => array_merge(\n",[627,1798,1799],{"class":629,"line":1156},[627,1800,1801],{},"                    [['role' => 'system', 'content' => $systemPrompt]],\n",[627,1803,1804],{"class":629,"line":1162},[627,1805,1806],{},"                    $messages\n",[627,1808,1809],{"class":629,"line":1168},[627,1810,1811],{},"                ),\n",[627,1813,1814],{"class":629,"line":1174},[627,1815,1816],{},"                'max_tokens' => $options['max_tokens'] ?? 1000,\n",[627,1818,1819],{"class":629,"line":1179},[627,1820,1821],{},"                'temperature' => $options['temperature'] ?? 0.3,\n",[627,1823,1824],{"class":629,"line":1185},[627,1825,1826],{},"                'response_format' => $options['response_format'] ?? null,\n",[627,1828,1829],{"class":629,"line":1191},[627,1830,1245],{},[627,1832,1833],{"class":629,"line":1196},[627,1834,943],{"emptyLinePlaceholder":135},[627,1836,1837],{"class":629,"line":1202},[627,1838,1839],{},"        if ($response->failed()) {\n",[627,1841,1842],{"class":629,"line":1207},[627,1843,1844],{},"            Log::error('LLM API call failed', [\n",[627,1846,1847],{"class":629,"line":1212},[627,1848,1849],{},"                'status' => $response->status(),\n",[627,1851,1852],{"class":629,"line":1218},[627,1853,1854],{},"                'body' => $response->body(),\n",[627,1856,1857],{"class":629,"line":1224},[627,1858,1245],{},[627,1860,1861],{"class":629,"line":1230},[627,1862,1863],{},"            throw new \\RuntimeException(\n",[627,1865,1866],{"class":629,"line":1236},[627,1867,1868],{},"                'LLM API error: ' . $response->body()\n",[627,1870,1871],{"class":629,"line":1242},[627,1872,1148],{},[627,1874,1875],{"class":629,"line":1248},[627,1876,1362],{},[627,1878,1879],{"class":629,"line":1253},[627,1880,943],{"emptyLinePlaceholder":135},[627,1882,1883],{"class":629,"line":1259},[627,1884,1885],{},"        $result = $response->json('choices.0.message.content');\n",[627,1887,1888],{"class":629,"line":1265},[627,1889,1890],{},"        $tokensUsed = $response->json('usage.total_tokens');\n",[627,1892,1893],{"class":629,"line":1271},[627,1894,943],{"emptyLinePlaceholder":135},[627,1896,1897],{"class":629,"line":1276},[627,1898,1899],{},"        \u002F\u002F Track usage\n",[627,1901,1902],{"class":629,"line":1282},[627,1903,1904],{},"        $this->trackTokenUsage($tokensUsed);\n",[627,1906,1907],{"class":629,"line":1287},[627,1908,943],{"emptyLinePlaceholder":135},[627,1910,1911],{"class":629,"line":1293},[627,1912,1913],{},"        \u002F\u002F Cache identical requests\n",[627,1915,1916],{"class":629,"line":1299},[627,1917,1918],{},"        if ($tokensUsed > 50) {\n",[627,1920,1921],{"class":629,"line":1305},[627,1922,1923],{},"            Cache::put($cacheKey, $result, self::CACHE_TTL);\n",[627,1925,1926],{"class":629,"line":1311},[627,1927,1362],{},[627,1929,1930],{"class":629,"line":1316},[627,1931,943],{"emptyLinePlaceholder":135},[627,1933,1934],{"class":629,"line":1321},[627,1935,1936],{},"        return $result;\n",[627,1938,1939],{"class":629,"line":1326},[627,1940,1368],{},[627,1942,1943],{"class":629,"line":1332},[627,1944,943],{"emptyLinePlaceholder":135},[627,1946,1947],{"class":629,"line":1337},[627,1948,1949],{},"    private function checkRateLimit(): void\n",[627,1951,1952],{"class":629,"line":1343},[627,1953,1083],{},[627,1955,1956],{"class":629,"line":1348},[627,1957,1958],{},"        $key = self::RATE_LIMIT_KEY . (string) time();\n",[627,1960,1961],{"class":629,"line":1353},[627,1962,1963],{},"        $count = Cache::increment($key, 1, 60);\n",[627,1965,1966],{"class":629,"line":1359},[627,1967,943],{"emptyLinePlaceholder":135},[627,1969,1970],{"class":629,"line":1365},[627,1971,1972],{},"        if ($count > 100) {\n",[627,1974,1975],{"class":629,"line":1371},[627,1976,1977],{},"            throw new \\RuntimeException('LLM rate limit exceeded');\n",[627,1979,1981],{"class":629,"line":1980},80,[627,1982,1362],{},[627,1984,1986],{"class":629,"line":1985},81,[627,1987,1368],{},[627,1989,1991],{"class":629,"line":1990},82,[627,1992,943],{"emptyLinePlaceholder":135},[627,1994,1996],{"class":629,"line":1995},83,[627,1997,1998],{},"    private function checkTokenBudget(int $requestedTokens): void\n",[627,2000,2002],{"class":629,"line":2001},84,[627,2003,1083],{},[627,2005,2007],{"class":629,"line":2006},85,[627,2008,2009],{},"        $dailyKey = self::TOKEN_BUDGET_KEY . now()->format('Y-m-d');\n",[627,2011,2013],{"class":629,"line":2012},86,[627,2014,2015],{},"        $dailyUsage = Cache::get($dailyKey, 0);\n",[627,2017,2019],{"class":629,"line":2018},87,[627,2020,943],{"emptyLinePlaceholder":135},[627,2022,2024],{"class":629,"line":2023},88,[627,2025,2026],{},"        if ($dailyUsage + $requestedTokens > self::MAX_TOKENS_PER_DAY) {\n",[627,2028,2030],{"class":629,"line":2029},89,[627,2031,2032],{},"            throw new \\RuntimeException('Daily token budget exhausted');\n",[627,2034,2036],{"class":629,"line":2035},90,[627,2037,1362],{},[627,2039,2041],{"class":629,"line":2040},91,[627,2042,943],{"emptyLinePlaceholder":135},[627,2044,2046],{"class":629,"line":2045},92,[627,2047,2048],{},"        $minuteKey = self::TOKEN_BUDGET_KEY . now()->format('Y-m-d-H-i');\n",[627,2050,2052],{"class":629,"line":2051},93,[627,2053,2054],{},"        $minuteUsage = Cache::get($minuteKey, 0);\n",[627,2056,2058],{"class":629,"line":2057},94,[627,2059,943],{"emptyLinePlaceholder":135},[627,2061,2063],{"class":629,"line":2062},95,[627,2064,2065],{},"        if ($minuteUsage + $requestedTokens > self::MAX_TOKENS_PER_MINUTE) {\n",[627,2067,2069],{"class":629,"line":2068},96,[627,2070,2071],{},"            throw new \\RuntimeException('Minute token budget exceeded');\n",[627,2073,2075],{"class":629,"line":2074},97,[627,2076,1362],{},[627,2078,2080],{"class":629,"line":2079},98,[627,2081,1368],{},[627,2083,2085],{"class":629,"line":2084},99,[627,2086,943],{"emptyLinePlaceholder":135},[627,2088,2089],{"class":629,"line":21},[627,2090,2091],{},"    private function trackTokenUsage(int $tokens): void\n",[627,2093,2095],{"class":629,"line":2094},101,[627,2096,1083],{},[627,2098,2100],{"class":629,"line":2099},102,[627,2101,2102],{},"        ApiTokenUsage::create([\n",[627,2104,2106],{"class":629,"line":2105},103,[627,2107,2108],{},"            'tokens' => $tokens,\n",[627,2110,2112],{"class":629,"line":2111},104,[627,2113,2114],{},"            'model' => 'gpt-4o',\n",[627,2116,2118],{"class":629,"line":2117},105,[627,2119,2120],{},"            'recorded_at' => now(),\n",[627,2122,2124],{"class":629,"line":2123},106,[627,2125,2126],{},"        ]);\n",[627,2128,2130],{"class":629,"line":2129},107,[627,2131,943],{"emptyLinePlaceholder":135},[627,2133,2135],{"class":629,"line":2134},108,[627,2136,2137],{},"        Cache::increment(\n",[627,2139,2141],{"class":629,"line":2140},109,[627,2142,2143],{},"            self::TOKEN_BUDGET_KEY . now()->format('Y-m-d'),\n",[627,2145,2147],{"class":629,"line":2146},110,[627,2148,2149],{},"            $tokens\n",[627,2151,2153],{"class":629,"line":2152},111,[627,2154,2155],{},"        );\n",[627,2157,2159],{"class":629,"line":2158},112,[627,2160,2137],{},[627,2162,2164],{"class":629,"line":2163},113,[627,2165,2166],{},"            self::TOKEN_BUDGET_KEY . now()->format('Y-m-d-H-i'),\n",[627,2168,2170],{"class":629,"line":2169},114,[627,2171,2172],{},"            $tokens,\n",[627,2174,2176],{"class":629,"line":2175},115,[627,2177,2178],{},"            120\n",[627,2180,2182],{"class":629,"line":2181},116,[627,2183,2155],{},[627,2185,2187],{"class":629,"line":2186},117,[627,2188,1368],{},[627,2190,2192],{"class":629,"line":2191},118,[627,2193,943],{"emptyLinePlaceholder":135},[627,2195,2197],{"class":629,"line":2196},119,[627,2198,2199],{},"    private function buildCacheKey(\n",[627,2201,2203],{"class":629,"line":2202},120,[627,2204,2205],{},"        string $system,\n",[627,2207,2209],{"class":629,"line":2208},121,[627,2210,1700],{},[627,2212,2214],{"class":629,"line":2213},122,[627,2215,2216],{},"        array $options\n",[627,2218,2220],{"class":629,"line":2219},123,[627,2221,1710],{},[627,2223,2225],{"class":629,"line":2224},124,[627,2226,2227],{},"        return 'llm_response:' . md5(\n",[627,2229,2230],{"class":629,"line":46},[627,2231,2232],{},"            serialize([$system, $messages, $options])\n",[627,2234,2236],{"class":629,"line":2235},126,[627,2237,2155],{},[627,2239,2241],{"class":629,"line":2240},127,[627,2242,1368],{},[627,2244,2246],{"class":629,"line":2245},128,[627,2247,1374],{},[241,2249,2250],{},"The gateway solves three problems that every production AI system faces:",[561,2252,2253,2259,2265],{},[564,2254,2255,2258],{},[245,2256,2257],{},"Rate limiting",": Prevents accidental burst requests from hitting the LLM API simultaneously. I use Redis atomic counters with sliding windows — one for per-minute, one for per-day. When the budget exhausts, jobs fail gracefully and retry via the queue's backoff mechanism.",[564,2260,2261,2264],{},[245,2262,2263],{},"Response caching",": Many agent workflows repeatedly ask the same questions (analyzing similar documents, checking the same policies). The cache key uses a hash of the full prompt, and I cache responses for one hour. In my production data, this gives a 22% cache hit rate, saving roughly $400 per month on LLM API costs.",[564,2266,2267,2270,2271,2274],{},[245,2268,2269],{},"Token tracking",": Every call logs usage to an ",[285,2272,2273],{},"api_token_usage"," table, which feeds into billing dashboards and cost forecasting. Cache increments on Redis keys let me reject requests before they hit the API, rather than discovering the overage in a monthly bill.",[258,2276,2278],{"id":2277},"real-world-example-saas-document-processing-pipeline","Real-World Example: SaaS Document Processing Pipeline",[241,2280,2281],{},"I built a SaaS product that processes legal documents using AI agents. Users upload contracts, and the system extracts clauses, identifies risks, and generates summaries. The architecture follows the pattern described above with one addition: a multi-stage pipeline.",[241,2283,2284],{},"The pipeline has five stages, each as a separate Laravel job on a dedicated queue:",[2286,2287,2288,2297,2306,2315,2324],"ol",{},[564,2289,2290,283,2293,2296],{},[245,2291,2292],{},"Document ingestion",[285,2294,2295],{},"queue:ingestion","): Validates file types, extracts text via OCR if needed, stores in S3",[564,2298,2299,283,2302,2305],{},[245,2300,2301],{},"Classification",[285,2303,2304],{},"queue:classification","): Identifies document type (NDA, employment contract, lease) using a lightweight classification prompt",[564,2307,2308,283,2311,2314],{},[245,2309,2310],{},"Analysis",[285,2312,2313],{},"queue:analysis","): Full clause extraction and risk assessment using GPT-4o — this is the most expensive stage",[564,2316,2317,283,2320,2323],{},[245,2318,2319],{},"Review",[285,2321,2322],{},"queue:review","): Cross-references extracted clauses against a database of known legal terms and company policies",[564,2325,2326,283,2329,2332],{},[245,2327,2328],{},"Reporting",[285,2330,2331],{},"queue:reporting","): Generates the PDF report and sends notification",[241,2334,2335],{},"Each stage dispatches the next stage only on success. If analysis fails, the pipeline stops and notifies the user with partial results.",[241,2337,2338,2339,617],{},"The queue configuration for this pipeline in ",[285,2340,2341],{},"config\u002Fqueue.php",[619,2343,2345],{"className":929,"code":2344,"language":931,"meta":183,"style":183},"'connections' => [\n    'redis' => [\n        'driver' => 'redis',\n        'connection' => 'default',\n        'queue' => [\n            'default',\n            'ingestion',\n            'classification',\n            'analysis',\n            'review',\n            'reporting',\n            'post-processing',\n        ],\n        'retry_after' => 360,\n        'block_for' => null,\n    ],\n],\n",[285,2346,2347,2352,2357,2362,2367,2372,2377,2382,2387,2392,2397,2402,2407,2412,2417,2422,2427],{"__ignoreMap":183},[627,2348,2349],{"class":629,"line":115},[627,2350,2351],{},"'connections' => [\n",[627,2353,2354],{"class":629,"line":121},[627,2355,2356],{},"    'redis' => [\n",[627,2358,2359],{"class":629,"line":14},[627,2360,2361],{},"        'driver' => 'redis',\n",[627,2363,2364],{"class":629,"line":92},[627,2365,2366],{},"        'connection' => 'default',\n",[627,2368,2369],{"class":629,"line":104},[627,2370,2371],{},"        'queue' => [\n",[627,2373,2374],{"class":629,"line":109},[627,2375,2376],{},"            'default',\n",[627,2378,2379],{"class":629,"line":697},[627,2380,2381],{},"            'ingestion',\n",[627,2383,2384],{"class":629,"line":125},[627,2385,2386],{},"            'classification',\n",[627,2388,2389],{"class":629,"line":975},[627,2390,2391],{},"            'analysis',\n",[627,2393,2394],{"class":629,"line":119},[627,2395,2396],{},"            'review',\n",[627,2398,2399],{"class":629,"line":97},[627,2400,2401],{},"            'reporting',\n",[627,2403,2404],{"class":629,"line":991},[627,2405,2406],{},"            'post-processing',\n",[627,2408,2409],{"class":629,"line":27},[627,2410,2411],{},"        ],\n",[627,2413,2414],{"class":629,"line":1002},[627,2415,2416],{},"        'retry_after' => 360,\n",[627,2418,2419],{"class":629,"line":102},[627,2420,2421],{},"        'block_for' => null,\n",[627,2423,2424],{"class":629,"line":113},[627,2425,2426],{},"    ],\n",[627,2428,2429],{"class":629,"line":1017},[627,2430,2431],{},"],\n",[241,2433,2434],{},"And the Horizon configuration for scaling:",[619,2436,2438],{"className":929,"code":2437,"language":931,"meta":183,"style":183},"'defaults' => [\n    'supervisor-1' => [\n        'connection' => 'redis',\n        'queue' => ['default'],\n        'balance' => 'auto',\n        'minProcesses' => 1,\n        'maxProcesses' => 5,\n        'tries' => 3,\n    ],\n    'supervisor-2' => [\n        'connection' => 'redis',\n        'queue' => ['analysis', 'classification'],\n        'balance' => 'auto',\n        'minProcesses' => 2,\n        'maxProcesses' => 10,\n        'tries' => 3,\n    ],\n    'supervisor-3' => [\n        'connection' => 'redis',\n        'queue' => ['ingestion', 'review', 'reporting'],\n        'balance' => 'auto',\n        'minProcesses' => 1,\n        'maxProcesses' => 3,\n        'tries' => 5,\n    ],\n],\n",[285,2439,2440,2445,2450,2455,2460,2465,2470,2475,2480,2484,2489,2493,2498,2502,2507,2512,2516,2520,2525,2529,2534,2538,2542,2547,2552,2556],{"__ignoreMap":183},[627,2441,2442],{"class":629,"line":115},[627,2443,2444],{},"'defaults' => [\n",[627,2446,2447],{"class":629,"line":121},[627,2448,2449],{},"    'supervisor-1' => [\n",[627,2451,2452],{"class":629,"line":14},[627,2453,2454],{},"        'connection' => 'redis',\n",[627,2456,2457],{"class":629,"line":92},[627,2458,2459],{},"        'queue' => ['default'],\n",[627,2461,2462],{"class":629,"line":104},[627,2463,2464],{},"        'balance' => 'auto',\n",[627,2466,2467],{"class":629,"line":109},[627,2468,2469],{},"        'minProcesses' => 1,\n",[627,2471,2472],{"class":629,"line":697},[627,2473,2474],{},"        'maxProcesses' => 5,\n",[627,2476,2477],{"class":629,"line":125},[627,2478,2479],{},"        'tries' => 3,\n",[627,2481,2482],{"class":629,"line":975},[627,2483,2426],{},[627,2485,2486],{"class":629,"line":119},[627,2487,2488],{},"    'supervisor-2' => [\n",[627,2490,2491],{"class":629,"line":97},[627,2492,2454],{},[627,2494,2495],{"class":629,"line":991},[627,2496,2497],{},"        'queue' => ['analysis', 'classification'],\n",[627,2499,2500],{"class":629,"line":27},[627,2501,2464],{},[627,2503,2504],{"class":629,"line":1002},[627,2505,2506],{},"        'minProcesses' => 2,\n",[627,2508,2509],{"class":629,"line":102},[627,2510,2511],{},"        'maxProcesses' => 10,\n",[627,2513,2514],{"class":629,"line":113},[627,2515,2479],{},[627,2517,2518],{"class":629,"line":1017},[627,2519,2426],{},[627,2521,2522],{"class":629,"line":1023},[627,2523,2524],{},"    'supervisor-3' => [\n",[627,2526,2527],{"class":629,"line":1028},[627,2528,2454],{},[627,2530,2531],{"class":629,"line":1034},[627,2532,2533],{},"        'queue' => ['ingestion', 'review', 'reporting'],\n",[627,2535,2536],{"class":629,"line":1040},[627,2537,2464],{},[627,2539,2540],{"class":629,"line":1046},[627,2541,2469],{},[627,2543,2544],{"class":629,"line":1051},[627,2545,2546],{},"        'maxProcesses' => 3,\n",[627,2548,2549],{"class":629,"line":1057},[627,2550,2551],{},"        'tries' => 5,\n",[627,2553,2554],{"class":629,"line":1063},[627,2555,2426],{},[627,2557,2558],{"class":629,"line":1069},[627,2559,2431],{},[241,2561,2562],{},"Key insight: the analysis queue has the most workers because it makes the slowest LLM calls. The ingestion and reporting queues are lightweight and need fewer workers. Separating them means a backlog in ingestion never blocks analysis results from being processed.",[258,2564,2566],{"id":2565},"laravel-vs-python-for-ai-backends","Laravel vs Python for AI Backends",[241,2568,2569],{},"After a year of building hybrid systems in both ecosystems, here is my honest comparison:",[374,2571,2572,2585],{},[377,2573,2574],{},[380,2575,2576,2579,2582],{},[383,2577,2578],{"align":385},"Criteria",[383,2580,2581],{"align":385},"Laravel",[383,2583,2584],{"align":385},"Python (FastAPI + Celery)",[394,2586,2587,2598,2609,2620,2631,2642,2653,2664,2675,2686,2697],{},[380,2588,2589,2592,2595],{},[399,2590,2591],{"align":385},"Queue system",[399,2593,2594],{"align":385},"Built-in with Horizon dashboard",[399,2596,2597],{"align":385},"Celery + Redis\u002FRabbitMQ, manual monitoring",[380,2599,2600,2603,2606],{},[399,2601,2602],{"align":385},"Job persistence",[399,2604,2605],{"align":385},"MySQL\u002FPostgres out of the box",[399,2607,2608],{"align":385},"Requires result backend configuration",[380,2610,2611,2614,2617],{},[399,2612,2613],{"align":385},"API gateway features",[399,2615,2616],{"align":385},"Native rate limiting & caching middleware",[399,2618,2619],{"align":385},"Custom implementation or third-party lib",[380,2621,2622,2625,2628],{},[399,2623,2624],{"align":385},"LLM SDK ecosystem",[399,2626,2627],{"align":385},"Limited, community-maintained packages",[399,2629,2630],{"align":385},"Rich (OpenAI, Anthropic, LangChain)",[380,2632,2633,2636,2639],{},[399,2634,2635],{"align":385},"Developer productivity",[399,2637,2638],{"align":385},"High — conventions, ORM, artisan commands",[399,2640,2641],{"align":385},"Medium — more boilerplate for same patterns",[380,2643,2644,2647,2650],{},[399,2645,2646],{"align":385},"Runtime performance",[399,2648,2649],{"align":385},"Good for I\u002FO-bound orchestration",[399,2651,2652],{"align":385},"Better for CPU-bound computation",[380,2654,2655,2658,2661],{},[399,2656,2657],{"align":385},"Team skill availability",[399,2659,2660],{"align":385},"Large pool of Laravel\u002FPHP developers",[399,2662,2663],{"align":385},"Large pool, but fewer with queue expertise",[380,2665,2666,2669,2672],{},[399,2667,2668],{"align":385},"Deployment simplicity",[399,2670,2671],{"align":385},"Single server, Forge\u002FEnvoyer ecosystem",[399,2673,2674],{"align":385},"Multiple services, more moving parts",[380,2676,2677,2680,2683],{},[399,2678,2679],{"align":385},"Cost for typical SaaS",[399,2681,2682],{"align":385},"Lower — one server for app + queue",[399,2684,2685],{"align":385},"Higher — separate worker infrastructure",[380,2687,2688,2691,2694],{},[399,2689,2690],{"align":385},"Real-time capabilities",[399,2692,2693],{"align":385},"Laravel Reverb (WebSockets)",[399,2695,2696],{"align":385},"WebSocket libraries, no built-in solution",[380,2698,2699,2702,2705],{},[399,2700,2701],{"align":385},"ML inference on same server",[399,2703,2704],{"align":385},"Not practical",[399,2706,2707],{"align":385},"Possible with ONNX, TensorFlow Lite",[241,2709,2710],{},"This table highlights the tradeoff that defines the hybrid approach: Laravel wins on developer experience and operational simplicity for the orchestration layer; Python wins for anything involving actual model computation. The pragmatic solution is to use both where each excels, connected through the queue or API gateway layer.",[258,2712,2714],{"id":2713},"when-laravel-is-the-wrong-choice","When Laravel Is the Wrong Choice",[241,2716,2717],{},"I want to be clear about the limitations. There are scenarios where Laravel is not the right tool for an AI backend:",[561,2719,2720,2726,2732,2738],{},[564,2721,2722,2725],{},[245,2723,2724],{},"Heavy ML training or fine-tuning",": If your system needs to train models on user data, use Python with PyTorch or TensorFlow. Laravel has no role here.",[564,2727,2728,2731],{},[245,2729,2730],{},"Real-time streaming inference",": Applications that require sub-100ms response times for every token (like interactive chatbots) benefit from Python's asyncio ecosystem with FastAPI streaming responses. Laravel's request lifecycle adds overhead.",[564,2733,2734,2737],{},[245,2735,2736],{},"On-device GPU workloads",": If you need to run local models with GPU acceleration, Python (or Rust) is the only practical choice. PHP's FFI layer is not suitable for this.",[564,2739,2740,2743],{},[245,2741,2742],{},"Large-scale embeddings pipelines",": Processing millions of documents through embedding models is better served by Python batch processing systems or dedicated vector database pipelines.",[241,2745,2746],{},"In all these cases, Laravel can still serve as the management layer — handling user authentication, billing, and job dispatching — while Python workers handle the computation. That is the hybrid approach in practice.",[258,2748,2750],{"id":2749},"the-2026-sweet-spot","The 2026 Sweet Spot",[241,2752,2753],{},"Hybrid architectures are the pragmatic middle ground between the hype of \"AI-native\" stacks and the reliability of traditional web frameworks. After building multiple production systems, I have settled on a stack that uses Laravel for the orchestration surface and delegates actual model work to specialized services — whether external LLM APIs, Python microservices, or serverless functions.",[241,2755,2756],{},"This approach gives me the best of both worlds. I get Laravel's mature queue system, excellent developer tooling, and large ecosystem for the parts of the application that handle user management, billing, and workflow orchestration. I get access to the latest AI models through a clean API gateway that manages costs, rate limits, and caching. And I avoid the operational complexity of maintaining a pure Python stack for what is fundamentally a CRUD application with AI features bolted on.",[241,2758,2759],{},"If you are building a SaaS product in 2026 that includes AI agent features, consider this architecture. Start with Laravel for the application layer, add queue-driven agent jobs, build a proper API gateway for LLM calls, and only reach for Python when you need actual model computation. Your deployment will be simpler, your costs lower, and your team will thank you.",[797,2761,2762],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"title":183,"searchDepth":121,"depth":121,"links":2764},[2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775],{"id":840,"depth":121,"text":833},{"id":849,"depth":121,"text":850},{"id":898,"depth":121,"text":899},{"id":919,"depth":121,"text":920},{"id":1406,"depth":121,"text":1407},{"id":1441,"depth":121,"text":1442},{"id":1598,"depth":121,"text":1599},{"id":2277,"depth":121,"text":2278},{"id":2565,"depth":121,"text":2566},{"id":2713,"depth":121,"text":2714},{"id":2749,"depth":121,"text":2750},"2026-05-26","Laravel as the orchestration backend for AI agents — queue-driven execution, API gateway for LLM systems, and real-world SaaS architecture patterns.",{"readingTime":2779},"12 min read","\u002Fblog\u002F42-laravel-ai-agent-hybrid-backends",{"title":833,"description":2777},"Hybrid Backend Architecture","Building production backends that bridge traditional frameworks with AI systems",{"src":2785,"mime":2786,"alt":2787,"width":2788,"height":2789},"\u002Fimg\u002Fblog\u002F42-laravel-ai-agent-hybrid-backends\u002Fbanner.svg","svg","Hybrid backend architecture showing Laravel orchestration layer connecting to AI agent systems via queues and API gateway",1200,680,"blog\u002F42-laravel-ai-agent-hybrid-backends",[2581,2792,2793,2794,2795,2796],"AI","Agents","Backend Architecture","Hybrid Systems","PHP","OgbvWYVgnZO7O5bgKEjq8URB2lodlALjguQvvkw8GZs",1782250476941]