One of the best things about the Laravel AI SDK is that it doesn't lock you into a provider. Swap OpenAI for Anthropic, add Gemini as a failover, and your agent code barely changes. That's the entire pitch, and it mostly holds up.
Until your agent needs to browse the web.
The SDK ships two provider tools for giving agents access to the web: WebSearch
and WebFetch
. They're convenient, a couple lines and your agent can search or fetch a page. But they're not really part of the SDK's unified layer, they're implemented natively by whichever AI provider you're using, which means their availability depends entirely on which model you picked.
WebSearch
works on Anthropic, OpenAI, Gemini, and OpenRouter. WebFetch
only works on Anthropic and Gemini. So if you configure a failover chain that includes Groq, DeepSeek, Mistral, or xAI, and one of those becomes the active provider, your agent quietly loses the ability to browse. Not an error, not a warning, just an agent that stops citing sources or stops noticing the page it was supposed to check even changed.
That's a rough thing to discover in production. You built a failover chain specifically so a rate limit or outage wouldn't take your feature down, and it turns out one of your fallback providers was silently missing a capability the whole time.
Set the provider problem aside for a second. Even on a provider where WebSearch
and WebFetch
are both available, what you get back is raw: search results or fetched page content that the model has to reason over itself. There's no schema. There's no guaranteed citation tracking. If you want your agent to return a structured, sourced answer, that reasoning happens somewhere in the model's own inference, not in a layer you control or can test.
For a lot of use cases, that's fine. For anything where the answer needs to be defensible or reproducible, a research assistant, a competitive brief, anything a user might ask "where did this come from," it's not enough.
The fix is to stop relying on provider-native web tools and give your agent a custom tool instead, one backed by an API rather than whichever model happens to be answering the prompt. That's exactly what a Tool
class in the Laravel AI SDK is for.
juststeveking/tabstack
gives you the client to build it on. Its agent()->research()
method takes a question, searches multiple sources, synthesizes an answer, and streams back a report with citations, all as one call.
Since the package doesn't ship Laravel integration out of the box, the first step is binding it in a service provider so it can be injected anywhere:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use JustSteveKing\Tabstack\Tabstack;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Tabstack::class, fn () => Tabstack::make(
apiKey: config('services.tabstack.key'),
));
}
}
Add the key to config/services.php
:
'tabstack' => [
'key' => env('TABSTACK_API_KEY'),
],
Now the tool itself. This wraps agent()->research()
and hands the agent back a report with its sources listed:
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use JustSteveKing\Tabstack\Requests\AgentResearch;
use JustSteveKing\Tabstack\Requests\ResearchMode;
use JustSteveKing\Tabstack\Tabstack;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class WebResearch implements Tool
{
public function __construct(
private readonly Tabstack $tabstack,
) {}
public function description(): Stringable|string
{
return 'Answers a question by researching multiple sources on the web and returns a synthesized, cited report. Use this when you need current information not in your training data.';
}
public function handle(Request $request): Stringable|string
{
$result = $this->tabstack->agent()->research(
params: new AgentResearch(
query: $request['query'],
mode: ResearchMode::Fast,
),
)->result();
$sources = collect($result->metadata->get('citedPages', []))
->map(fn ($page) => "- {$page['title']}: {$page['url']}")
->implode("\n");
return "{$result->report}\n\nSources:\n{$sources}";
}
public function schema(JsonSchema $schema): array
{
return [
'query' => $schema->string()
->required()
->description('The question to research'),
];
}
}
The ->result()
call blocks until the stream finishes and hands back a typed ResearchResult
rather than making you consume events yourself, which keeps the tool's handle
method simple. If you'd rather stream progress back to the agent as it happens, the client also gives you ->each()
and raw iteration, but for a tool call the blocking result is usually what you want.
Wire it into any agent through the tools
method, same as any other tool:
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\WebResearch;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
class MarketAnalyst implements Agent, HasTools
{
use Promptable;
public function instructions(): string
{
return 'You help analyze markets and competitors. Use the research tool whenever you need current information. Always mention your sources.';
}
public function tools(): iterable
{
return [
app(WebResearch::class),
];
}
}
Now switch providers all you want:
$response = (new MarketAnalyst)->prompt(
'What are the current pricing trends for cloud browser automation APIs?',
provider: [Lab::OpenAI, Lab::Anthropic, Lab::Groq],
);
The agent's ability to research the web doesn't depend on which of those three ends up answering the prompt. The tool is yours, it runs the same regardless of which model called it.
You could build something similar with WebFetch
on a supported provider, pointing it at a specific known URL. But that only works when you already know which page has the answer. /research
is built for the opposite case, a question with no known source, where the value is in picking the right sources, reading them, and synthesizing a single answer with the receipts attached. That's a genuinely different job than fetching one page, and it's the piece the SDK's built-in tools don't attempt.
The Laravel AI SDK's provider independence is one of its best features, right up until you reach for a tool that isn't actually part of that independence. Web access shouldn't be something your agent loses depending on which model answered the prompt. Building it as a real Tool
class backed by an API, rather than leaning on whatever the current provider happens to expose, keeps that promise intact.
If you're building agents with the Laravel AI SDK and want this kind of tool ready to go, juststeveking/tabstack on Packagist is where I'd start.