Provider-independent AI gateway for ProcessWire β chat, content, embeddings, images and tool-use behind one clean API. (Formerly AiWire.)

Squad connects your ProcessWire site to every major AI provider through a single, uniform PHP API. Write code once and switch providers (or fall back between them) without changing a line. Keys are stored encrypted, never in plaintext config.
π Full API reference + 25 worked examples β DOCUMENTATION.md Β· changes β CHANGELOG.md
$ai = $modules->get('Squad');
echo $ai->chat('Write a one-line tagline for a dentist in Boston.');
chat() / ask() with system prompts, multi-turn history, temperature, token limits.stream() forwards provider deltas as they arrive while returning the complete normalized response.webSearch flag maps to OpenRouterβs web
plugin, Anthropic web search, OpenAI/xAI Responses search or native Google
Search grounding; normalized citations are returned as sources.embed() for one string or a batch (OpenAI, Google, Qwen, Zhipu). Powers RAG (see the Atlas module).image() for text-to-image (xAI Grok Imagine, OpenAI gpt-image-1 / DALLΒ·E 3).vision() analyzes up to four bounded local images through multimodal Anthropic or OpenAI-compatible models (8 MB/image, 20 MB total, 12,000 px/side, 32 MP).run() drives a multi-step tool-calling loop (OpenAI and Anthropic tool formats).askWithFallback() walks every enabled key, then other providers, until one succeeds.config.php), so a database dump never exposes them. env:NAME references are also supported.temperature/sampling params on models that reject them (Claude Opus 4.7/4.8, Fable/Mythos), so calls donβt 400.D/W/M/Y, custom like 2W) with optional page scoping.askAndSave() / generate() write AI copy straight to page fields (skip if already filled).models.json + live model refresh (OpenAI/OpenRouter) + per-key custom model IDs.site/modules/Squad/.$modules->get('Squad') in your templates/modules.site/modules/Squad/
βββ Squad.module.php # main module + provider catalogue + admin UI
βββ SquadProvider.php # HTTP client for all providers (chat / embed / image / tools)
βββ SquadCache.php # file-based response cache
βββ SquadKeys.php # encrypted key storage (libsodium)
βββ models.json # editable provider/model catalogue
βββ README.md Β· DOCUMENTATION.md Β· CHANGELOG.md Β· LICENSE
| Provider | Console |
|---|---|
| Anthropic | console.anthropic.com |
| OpenAI | platform.openai.com/api-keys |
| Google (Gemini) | aistudio.google.com/apikey |
| xAI | console.x.ai |
| OpenRouter | openrouter.ai/keys |
| DeepSeek / Qwen / Kimi / GLM / MiniMax / Yi / Doubao / Ernie / Hunyuan | each providerβs own console (see models.json for endpoints) |
$ai = $modules->get('Squad');
// 1) Simple text
echo $ai->chat('What is ProcessWire?');
// 2) Full response with metadata
$res = $ai->ask('Explain embeddings in one sentence.', ['maxTokens' => 500]);
if ($res['success']) echo $res['content'];
// 3) Fallback across providers
$res = $ai->askWithFallback('Summarise thisβ¦', [
'provider' => 'anthropic', 'fallbackProviders' => ['openai', 'google'],
]);
// 4) Embeddings (one string or an array β vectors)
$vec = $ai->embed('hello world')['embedding']; // [float, β¦]
$vecs = $ai->embed(['a', 'b', 'c'])['embeddings']; // [[β¦], [β¦], [β¦]]
// 5) Image generation
$img = $ai->image('a calm coastal sunrise, photographic', ['aspect' => '16:9']);
echo $img['url']; // or $img['b64']
// 6) Tool use (agent loop)
$res = $ai->run([
'message' => 'What is 19 * 23?',
'tools' => [['name' => 'multiply', 'description' => 'Multiply two numbers',
'parameters' => ['type'=>'object','properties'=>['a'=>['type'=>'number'],'b'=>['type'=>'number']],'required'=>['a','b']]]],
'onTool' => fn($name, $in) => (string) ($in['a'] * $in['b']),
]);
echo $res['content'];
// 7) Write AI copy into page fields
$ai->generate($page, [
['field' => 'summary', 'prompt' => "One-sentence summary of {$page->title}"],
['field' => 'body', 'prompt' => "Two paragraphs about {$page->title}"],
], ['cache' => 'M']);
| Method | Returns | Description |
|---|---|---|
chat($msg, $opts) |
string |
Text only, '' on error |
ask($msg, $opts) |
array |
success, content, usage, raw, cached |
stream($msg, $onDelta, $opts) |
array |
Calls $onDelta for each text delta and returns the final response |
askWithFallback($msg, $opts) |
array |
Tries all keys/providers until success |
askMultiple($msg, $providers) |
array |
Same prompt to several providers |
embed($input, $opts) |
array |
embedding (single) / embeddings (batch), model, usage |
image($prompt, $opts) |
array |
url / b64, model, provider |
run($opts) |
array |
Tool-use loop: content, steps, messages, usage |
generate($page, $blocks, $opts) |
array |
Multi-block field generation |
askAndSave($page, $fields, $msg) |
array |
Ask + save to field (skip if filled) |
saveTo / loadFrom |
bool / ?string |
Manual field storage |
getProvidersStatus() |
array |
Providers + key status |
getDefaultEmbedProvider() / getDefaultImageProvider() |
?string |
First capable provider with a key |
clearCache / clearAllCache / cacheStats |
β | Cache management |
Common $opts: provider, model, systemPrompt, maxTokens, temperature,
history, keyIndex, cache, timeout, webSearch and
webSearchMaxResults (1β10, default 5). Search may add provider charges and
latency. OpenRouter supports search for every routed model; direct search is
supported for Anthropic, OpenAI, Google and xAI. Other direct adapters return a
clear unsupported error instead of silently answering without search.
$result = $modules->get('Squad')->ask(
'What changed in Australian herbal liqueurs this year?',
[
'provider' => 'openrouter',
'webSearch' => true,
'webSearchMaxResults' => 5,
]
);
foreach($result['sources'] ?? [] as $source) {
echo $source['title'] . ': ' . $source['url'];
}
Embeddings/images also accept provider/model; image() takes aspect,
resolution, size, n. vision($prompt, $images, $opts) accepts bounded
local image paths or image data URLs.
squad_keys table as libsodium ciphertext; the encryption key is derived from a secret in config.php ($config->squadSecret β tableSalt β userAuthSalt), never from the database β so a DB dump only ever contains ciphertext. Donβt change that salt after storing keys, or theyβd need re-entering.env: references β store a key as env:OPENAI_API_KEY to read it from an environment variable instead of the database.MIT. Author: Maxim Semenov β smnv.org Β· built for the ProcessWire community.