Recipe: swap the LLM provider
All providers implement AiResearchProvider, so switching is a constructor change.
Anthropic or OpenAI
use Gawrys\Counterparty\Ai\Research\{AnthropicResearchProvider, OpenAiResearchProvider};
$provider = $useOpenAi
? new OpenAiResearchProvider($http, $openAiKey) // gpt-4o-mini by default
: new AnthropicResearchProvider($http, $anthropicKey); // claude-haiku-4-5 by default
Pick by environment
The bundled demos choose by which key is present:
$anthropic = getenv('ANTHROPIC_API_KEY') ?: null;
$openai = getenv('OPENAI_API_KEY') ?: null;
$provider = $anthropic !== null
? new AnthropicResearchProvider($http, $anthropic)
: new OpenAiResearchProvider($http, (string) $openai);
Your own backend (Gemini, Mistral, local, …)
Implement one method; the base class handles JSON forcing, validation, retry and the tool loop:
use Gawrys\Counterparty\Ai\Research\{AbstractAiResearchProvider, ResearchRequest};
final readonly class GeminiResearchProvider extends AbstractAiResearchProvider
{
public function __construct(
private \Gawrys\Counterparty\Http\JsonHttpClient $http,
private string $apiKey,
private string $model = 'gemini-1.5-flash',
\Gawrys\Counterparty\Ai\Research\ResearchResultParser $parser = new \Gawrys\Counterparty\Ai\Research\ResearchResultParser(),
) {
parent::__construct($parser);
}
protected function complete(ResearchRequest $request, array $tools): string
{
// POST to your model with $request->systemPrompt / $request->userPrompt,
// expose $tools for function calling, and return the raw findings JSON.
// Use $this->runTool(), $this->encodeToolResult(), $this->objectList() for the loop.
}
}
That is the entire cost of a new LLM backend: one complete() implementation.