Providers
An AiResearchProvider is the LLM backend. Two reference providers ship, both over PSR-18
(no SDK dependency - the SDK is only a suggest).
interface AiResearchProvider
{
/** @param list<ResearchTool> $tools */
public function research(ResearchRequest $request, array $tools): ResearchResult;
}
Anthropic
use Gawrys\Counterparty\Ai\Research\AnthropicResearchProvider;
$provider = new AnthropicResearchProvider(
http: $jsonHttpClient,
apiKey: $anthropicKey,
model: 'claude-haiku-4-5', // default
);
Calls the Messages API, exposes the tools for native tool use, and returns the validated findings JSON.
OpenAI
use Gawrys\Counterparty\Ai\Research\OpenAiResearchProvider;
$provider = new OpenAiResearchProvider(
http: $jsonHttpClient,
apiKey: $openAiKey,
model: 'gpt-4o-mini', // default
);
Calls Chat Completions in JSON mode, with tools for function calling.
Swapping LLMs is one line
Because both implement the same port, switching is a constructor change. On the bridges, it is driven by which key is present / by config - see Recipes -> Swap LLM.
Writing your own provider
Extend AbstractAiResearchProvider and implement a single method - the base class handles
the force-JSON / parse / validate / retry loop and the tool-use loop scaffolding:
use Gawrys\Counterparty\Ai\Research\{AbstractAiResearchProvider, ResearchRequest};
final readonly class MyLlmProvider extends AbstractAiResearchProvider
{
protected function complete(ResearchRequest $request, array $tools): string
{
// 1. send $request->systemPrompt / $request->userPrompt to your model
// 2. expose $tools for function calling (use $this->runTool(), $this->encodeToolResult())
// 3. return the final raw findings JSON string
}
}
Helpers available to subclasses: runTool(), encodeToolResult(), objectList(),
stringKeyedArguments(), and maxToolHops. Malformed output is retried up to maxAttempts
and never trusted; on exhaustion the strategy falls back to “human review required”.
Choosing a model
- Use a cheaper, fast model for triage (the defaults are deliberately small).
- Reach for a stronger model for deep dives on flagged counterparties.
- Keep
maxTokensmodest; the findings JSON is small.