Exceptions
All three extend Kirby\Exception\Exception. Only ProviderException carries a structured details payload; the other two are raised with a message alone:
| Exception | Thrown by | Trigger |
|---|---|---|
ProviderException | Providers | Upstream call failed |
AuthException | Providers, Client | Missing API key |
InvalidArgumentException | Resolver, PanelContext | Missing or unknown provider config; a mistyped option in debug mode |
ProviderException
JohannSchopplich\Copilot\AI\Exception\ProviderException extends Kirby\Exception\Exception. Thrown when an upstream AI provider call fails.
final class ProviderException extends \Kirby\Exception\Exception
{
protected static string $defaultKey = 'copilot.ai.provider';
protected static int $defaultHttpCode = 502;
public function __construct(
ProviderName $providerName,
string $reason,
string|null $model = null,
string|null $responseId = null,
string|null $responseExcerpt = null,
int|null $httpCode = null,
Throwable|null $previous = null,
);
}
Details Payload
null when the failure happened before model resolution.response.id, Anthropic's message.id).The original upstream error is attached as previous. The HTTP status (when available) is on the httpCode constructor argument and accessible via getHttpCode().
Message Format
The exception message is built from the constructor arguments:
<provider> provider error: <reason> (model: <model>, request: <responseId>, response: <excerpt>)
Example:
openai provider error: request failed: Rate limit reached (model: gpt-5.6-luna, request: resp_abc123, response: Rate limit exceeded for organization …)
Catching
use JohannSchopplich\Copilot\AI\Client;
use JohannSchopplich\Copilot\AI\Exception\ProviderException;
try {
$result = Client::instance()->generateObject($messages, $schema);
} catch (ProviderException $error) {
$details = $error->getDetails();
// [
// 'providerName' => ProviderName::OpenAI,
// 'model' => 'gpt-5.6-luna',
// 'responseId' => 'resp_abc123',
// 'responseExcerpt' => 'Rate limit exceeded …',
// ]
$http = $error->getHttpCode(); // 429
$upstream = $error->getPrevious(); // OpenAI\Exceptions\RateLimitException
}
The default HTTP code 502 applies whenever the failure carries no upstream status – an unparsable response, for example.
AuthException
Kirby\Exception\AuthException is thrown when an API key is missing.
| Trigger | Message |
|---|---|
Provider called without apiKey set | Missing API key in "johannschopplich.copilot.providers.<name>.apiKey" |
Client::requireApiKey() preflight check | Same message – preflight before kicking off long-running batch ops |
use JohannSchopplich\Copilot\AI\Client;
use Kirby\Exception\AuthException;
try {
$client = Client::instance();
$client->requireApiKey();
} catch (AuthException $error) {
// Show a config hint to the user
}
InvalidArgumentException
Kirby\Exception\InvalidArgumentException is thrown by Resolver::fromKirbyOptions() for misconfiguration.
| Trigger | Message |
|---|---|
Missing provider config | Missing required option "johannschopplich.copilot.provider" |
| Unknown provider name | Unknown provider "<name>" – set "johannschopplich.copilot.provider" to one of: openai, anthropic, google, mistral |
Config Shape Errors
While building the Panel's context, Copilot also checks the shape of every option it reads. These checks raise InvalidArgumentException only when Kirby's debug option is on; with debug off, the offending option falls back to its default so that one typo can't take the whole Panel down.
| Trigger | Message |
|---|---|
| Option has the wrong type | Invalid <path>: expected <types>, got <type> |
| Option is outside its set of valid values | Invalid <path>: <value>. Must be one of: <values> |
| Provider keys differing only in case | Conflicting provider keys: <names> – provider names are case-insensitive |