Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
92af6bc
Add Queue::whenJobRunsLongerThan
binaryfire Nov 3, 2026
2c03498
Update Queue facade docblocks
binaryfire Nov 3, 2026
716cfe8
Fix named arguments in Cookie::queue()
binaryfire Nov 3, 2026
7117045
Test that ImageFile::default() returns an ImageFile rule
binaryfire Nov 3, 2026
e97447c
Assert pagination contract render() types
binaryfire Nov 3, 2026
a86353c
Test update and delete with order and limit
binaryfire Nov 3, 2026
647f247
Preserve error_get_last() for suppressed errors
binaryfire Nov 3, 2026
1132e65
Add array replacements and insert helpers to middleware configuration
binaryfire Nov 3, 2026
869382d
Show released jobs as released in queue:work output
binaryfire Nov 3, 2026
c67c4fc
Fix forceIndex() generating invalid SQL on SQLite
binaryfire Nov 3, 2026
d6d58b2
Dispatch ScheduledTaskSkipped for tasks run elsewhere and add Schedul…
binaryfire Nov 3, 2026
6b8ea2b
Update Schedule facade docblocks
binaryfire Nov 3, 2026
99d9b7b
Test savepoint rollback on nested concurrency errors
binaryfire Nov 3, 2026
8c3b358
Test toResourceCollection() on filtered collections
binaryfire Nov 3, 2026
25c59ea
Test that lazy images are freed without the garbage collector
binaryfire Nov 3, 2026
81576aa
Clarify the key:generate --show help text
binaryfire Nov 3, 2026
5b29a43
Record failed batch jobs and release unique locks when the model is m…
binaryfire Nov 3, 2026
7ca0670
Fix broadcasting to multiple encrypted Pusher channels
binaryfire Nov 3, 2026
68a510d
Fix Str::remove() with a Traversable subject and Str::substrCount() o…
binaryfire Nov 3, 2026
a990b8a
Add Grammar::compileExistsColumn()
binaryfire Nov 3, 2026
99d6940
Test cursorPaginate() on union queries with multiple orders
binaryfire Nov 3, 2026
8cbb712
Detect Vitess QueryList.TerminateAll() as a lost connection
binaryfire Nov 3, 2026
dd30d47
Add commit IDs for read-after-write checks on read replicas
binaryfire Nov 3, 2026
7511fac
Update the DB facade docblocks for commit IDs
binaryfire Nov 3, 2026
83d0257
Focus local verification on affected behavior
binaryfire Nov 3, 2026
fb91879
Remove unused reusable client methods from PendingRequest
binaryfire Nov 3, 2026
c6b63b0
Add the migration duration to MigrationEnded
binaryfire Nov 3, 2026
6b9df18
Bump follow-redirects to 1.16.2
binaryfire Nov 3, 2026
94bac2c
Treat empty and numeric-key JSON objects as objects in type assertions
binaryfire Nov 3, 2026
151028b
Port MCP app domain opt-out, OAuth discovery API and structured conte…
binaryfire Nov 3, 2026
a719833
Handle insufficient credits and non-string provider error messages
binaryfire Nov 3, 2026
bdb2c61
Report stored message IDs in RUN_FINISHED metadata and expose raw cla…
binaryfire Nov 3, 2026
4caa7a0
Update the default Mistral models
binaryfire Nov 3, 2026
8379602
Wait only for missing MariaDB domains when checking a commit ID
binaryfire Nov 3, 2026
8bccada
Report the task's own runtime when a scheduled task fails after finis…
binaryfire Nov 3, 2026
7196188
Correct the suppressed-error note on HandleExceptions::handleError()
binaryfire Nov 3, 2026
cae1c80
Support stdClass targets in data_get, data_set and data_forget wildcards
binaryfire Nov 3, 2026
21c968e
Stop wildcard validation from changing objects in the input
binaryfire Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/databases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
services:
mysql:
image: mysql:8.4
command: --gtid-mode=ON --enforce-gtid-consistency=ON
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
Expand Down Expand Up @@ -167,6 +168,7 @@ jobs:
services:
mariadb:
image: mariadb:10.11
command: --log-bin --server-id=1
env:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: testing
Expand Down Expand Up @@ -214,6 +216,7 @@ jobs:
services:
mariadb:
image: mariadb:11
command: --log-bin --server-id=1
env:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: testing
Expand Down Expand Up @@ -261,6 +264,7 @@ jobs:
services:
mariadb:
image: mariadb:12.3
command: --log-bin --server-id=1
env:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: testing
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ Anything found follows When to Stop and Report — "the task didn't ask me to fi

Do not routinely run formatting or PHPStan during edits. Run targeted PHPStan earlier only when investigating a typing issue or when its feedback is needed to guide implementation.

**At the end of a coherent implementation slice**, run `composer analyse` and the affected tests, in that order. Use PHPUnit for one test file or `composer test:parallel -- --processes=4 tests/PackageName` for multiple files. Use `--filter` when needed to narrow the selection.
At the end of a coherent implementation slice, run `composer analyse` and any outstanding tests needed to verify the changed behavior and affected callers. Use PHPUnit for one test file or `composer test:parallel -- --processes=4` for multiple files, selecting the relevant files or using `--filter`. Reuse passing results unless subsequent changes invalidate them.

Run `composer lint:fix` before a scheduled code review or commit checkpoint, not after individual changes within a batch.

Run the full test suite only when the change has framework-wide impact. In that case, use `composer fix`, which includes formatting, analysis and all test suites; do not run those checks separately first. For focused changes, include separate Testbench or dogfood checks when the changed behavior affects those workflows.
Run only the local tests needed to give strong confidence in the change. Leave full suites and CI matrices to CI, unless a specific unresolved issue requires broader local testing.

After a failure or review correction, fix the cause, repeat only checks affected by the correction, and finish any outstanding verification. Do not restart the entire sequence merely because another review round occurred.

Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/ai/src/Gateway/Concerns/AnswersQuestions.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,14 @@ public function classify(
}
}

return new ClassificationResponse(
return (new ClassificationResponse(
$answers,
new TextUsage(
inputTokens: $data['usage']['input_tokens'] ?? 0,
outputTokens: $data['usage']['output_tokens'] ?? 0,
),
new Meta($provider->name(), $this->answeringModel($data, $model)),
);
))->withRawResponse($response);
}

/**
Expand Down
56 changes: 50 additions & 6 deletions src/ai/src/Gateway/Concerns/HandlesFailoverErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ protected function withErrorHandling(string $providerName, Closure $callback): m
} catch (RequestException $requestException) {
$status = $requestException->response->status();

if ($status === 429) {
throw RateLimitedException::forProvider(
if ($this->isInsufficientCredits($requestException)) {
throw InsufficientCreditsException::forProvider(
$providerName,
$requestException->getCode(),
$requestException
);
}

if ($status === 402) {
throw InsufficientCreditsException::forProvider(
if ($status === 429) {
throw RateLimitedException::forProvider(
$providerName,
$requestException->getCode(),
$requestException
Expand All @@ -65,8 +65,10 @@ protected function withErrorHandling(string $providerName, Closure $callback): m
);
}

if ($patterns = $this->insufficientCreditPatterns()) {
$message = strtolower($requestException->response->json('error.message', ''));
$message = $requestException->response->json('error.message');

if (is_string($message) && $patterns = $this->insufficientCreditPatterns()) {
$message = strtolower($message);

foreach ($patterns as $pattern) {
if (str_contains($message, (string) $pattern)) {
Expand All @@ -92,6 +94,10 @@ protected function retryAfterSeconds(Throwable $exception): ?int
return null;
}

if ($this->isInsufficientCredits($exception)) {
return null;
}

$status = $exception->response->status();

if ($status !== 429 && ! in_array($status, $this->overloadedStatusCodes(), true)) {
Expand All @@ -103,6 +109,27 @@ protected function retryAfterSeconds(Throwable $exception): ?int
return ctype_digit($retryAfter) && (int) $retryAfter <= 10 ? (int) $retryAfter : null;
}

/**
* Determine if the response is a 402, or a 429 insufficient credits or quota error rather than a rate limit.
*/
protected function isInsufficientCredits(RequestException $exception): bool
{
$status = $exception->response->status();

if ($status === 402) {
return true;
}

if ($status !== 429) {
return false;
}

$codes = $this->insufficientCreditCodes();

return in_array($exception->response->json('error.code'), $codes, true)
|| in_array($exception->response->json('error.type'), $codes, true);
}

/**
* Get the status codes that indicate a provider is transiently unavailable and the request should fail over.
*
Expand All @@ -122,4 +149,21 @@ protected function insufficientCreditPatterns(): array
{
return [];
}

/**
* Get the error codes or types that indicate insufficient credits or quota on a 429 response.
*
* @return list<string>
*/
protected function insufficientCreditCodes(): array
{
// OpenAI sends these with a 429 status, and may still use "insufficient_quota" as the error type.
return [
'insufficient_quota',
'credit_balance_exhausted',
'organization_spend_limit_exceeded',
'project_spend_limit_exceeded',
'organization_usage_limit_exceeded',
];
}
}
4 changes: 2 additions & 2 deletions src/ai/src/Gateway/OpenAi/OpenAiClassificationGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ public function classify(
}
}

return new ClassificationResponse(
return (new ClassificationResponse(
$answers,
new TextUsage(
inputTokens: $data['usage']['input_tokens'] ?? 0,
outputTokens: $data['usage']['output_tokens'] ?? 0,
),
new Meta($provider->name(), $data['model'] ?? $model),
);
))->withRawResponse($response);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/ai/src/Providers/MistralProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public function transcriptionGateway(): TranscriptionGateway
*/
public function defaultTextModel(): string
{
return $this->config['models']['text']['default'] ?? 'mistral-large-2512';
return $this->config['models']['text']['default'] ?? 'mistral-large-4-0';
}

/**
Expand All @@ -106,23 +106,23 @@ public function cheapestTextModel(): string
*/
public function smartestTextModel(): string
{
return $this->config['models']['text']['smartest'] ?? 'mistral-medium-3-5';
return $this->config['models']['text']['smartest'] ?? 'mistral-large-4-0';
}

/**
* Get the name of the default audio (TTS) model.
*/
public function defaultAudioModel(): string
{
return $this->config['models']['audio']['default'] ?? 'voxtral-mini-tts-2603';
return $this->config['models']['audio']['default'] ?? 'voxtral-mini-tts-3-0';
}

/**
* Get the name of the default transcription (STT) model.
*/
public function defaultTranscriptionModel(): string
{
return $this->config['models']['transcription']['default'] ?? 'voxtral-mini-2602';
return $this->config['models']['transcription']['default'] ?? 'voxtral-mini-transcribe-3-0';
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/ai/src/Responses/ClassificationResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use ArrayAccess;
use Countable;
use Hypervel\Ai\Responses\Concerns\HasRawResponse;
use Hypervel\Ai\Responses\Data\Answer;
use Hypervel\Ai\Responses\Data\Meta;
use Hypervel\Ai\Responses\Data\TextUsage;
Expand All @@ -19,6 +20,8 @@

class ClassificationResponse implements Arrayable, ArrayAccess, Countable, IteratorAggregate, JsonSerializable
{
use HasRawResponse;

/**
* Create a new classification response instance.
*
Expand Down
10 changes: 6 additions & 4 deletions src/ai/src/Streaming/Protocols/AgentUserInteractionProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,18 @@ protected function stepFinishedPart(): array
*/
protected function runFinishedPart(array $attributes = []): array
{
$assistantMessageId = $this->response?->assistantMessageId;
$userMessageId = $this->response?->userMessageId;
$messageIds = array_filter([
'messageId' => $this->response?->assistantMessageId,
'userMessageId' => $this->response?->userMessageId,
]);

return [
'type' => 'RUN_FINISHED',
'threadId' => $this->threadId,
'runId' => $this->runId,
...($assistantMessageId ? ['messageId' => $assistantMessageId] : []),
...($userMessageId ? ['userMessageId' => $userMessageId] : []),
...$messageIds,
...$attributes,
...($messageIds ? ['metadata' => [...($attributes['metadata'] ?? []), ...$messageIds]] : []),
];
}

Expand Down
10 changes: 8 additions & 2 deletions src/broadcasting/src/Broadcasters/PusherBroadcaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,18 @@ public function broadcast(array $channels, string $event, array $payload = []):

$parameters = $socket !== null ? ['socket_id' => $socket] : [];

$channels = new Collection($this->formatChannels($channels));
[$encryptedChannels, $channels] = (new Collection($this->formatChannels($channels)))
->partition(fn ($channel) => str_starts_with($channel, 'private-encrypted-'));

try {
$channels->chunk(100)->each(function ($channels) use ($event, $payload, $parameters) {
ConnectionResolver::releaseIdleConnections();
$this->pusher->trigger($channels->toArray(), $event, $payload, $parameters);
$this->pusher->trigger($channels->values()->all(), $event, $payload, $parameters);
});

$encryptedChannels->each(function ($channel) use ($event, $payload, $parameters) {
ConnectionResolver::releaseIdleConnections();
$this->pusher->trigger([$channel], $event, $payload, $parameters);
});
} catch (ApiErrorException $e) {
throw new BroadcastException(
Expand Down
12 changes: 8 additions & 4 deletions src/collections/src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ function data_get($target, $key, $default = null)
}

if ($segment === '*') {
// Unlike upstream, wildcards also select stdClass properties, since they may target any key of an array or object.
if ($target instanceof Collection) {
$target = $target->all();
} elseif (! is_iterable($target)) {
} elseif (! is_iterable($target) && ! $target instanceof stdClass) {
return value($default);
}

Expand Down Expand Up @@ -143,7 +144,8 @@ function data_set(&$target, $key, $value, $overwrite = true)
$segments = is_array($key) ? $key : explode('.', (string) $key);

if (($segment = array_shift($segments)) === '*') {
if (! Arr::accessible($target)) {
// Unlike upstream, wildcards also set stdClass properties instead of replacing the object with an array.
if (! Arr::accessible($target) && ! $target instanceof stdClass) {
$target = [];
}

Expand Down Expand Up @@ -202,7 +204,8 @@ function data_forget(&$target, $key)
{
$segments = is_array($key) ? $key : explode('.', (string) $key);

if (($segment = array_shift($segments)) === '*' && Arr::accessible($target)) {
// Unlike upstream, wildcards also remove values from stdClass properties.
if (($segment = array_shift($segments)) === '*' && (Arr::accessible($target) || $target instanceof stdClass)) {
if ($segments) {
foreach ($target as &$inner) {
data_forget($inner, $segments);
Expand All @@ -217,7 +220,8 @@ function data_forget(&$target, $key)
} elseif (is_object($target)) {
if ($segments && isset($target->{$segment})) {
data_forget($target->{$segment}, $segments);
} elseif (isset($target->{$segment})) {
} elseif (! $segments) {
// Unlike upstream, this does not check isset(), so null properties are removed too.
unset($target->{$segment});
}
}
Expand Down
15 changes: 9 additions & 6 deletions src/console/src/Commands/ScheduleRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,10 @@ protected function runSingleServerEvent(Event $event, CarbonInterface $startedAt
if ($this->schedule->serverShouldRun($event, $startedAt)) {
$this->runEvent($event);
} else {
$this->dispatchTaskSkipped($event);

$this->components->info(sprintf(
'Skipping [%s], as command already run on another server.',
'Skipping [%s] because the command already ran on another server.',
$event->getSummaryForDisplay()
));
}
Expand Down Expand Up @@ -488,16 +490,15 @@ protected function runEvent(Event $event): void

$failed = false;
$skippedBecauseOverlapping = false;
$runtime = null;

try {
$event->run($this->hypervel);
$runtime = round(microtime(true) - $start, 2);
$skippedBecauseOverlapping = $event->wasSkippedDueToOverlapping();

if ($this->dispatcher->hasListeners(ScheduledTaskFinished::class)) {
$this->dispatcher->dispatch(new ScheduledTaskFinished(
$event,
round(microtime(true) - $start, 2)
));
$this->dispatcher->dispatch(new ScheduledTaskFinished($event, $runtime));
}

$this->eventsRan = true;
Expand All @@ -524,10 +525,12 @@ protected function runEvent(Event $event): void
CoroutineContext::set(self::TASK_FAILED_CONTEXT_KEY, true);

if ($this->dispatcher->hasListeners(ScheduledTaskFailed::class)) {
// Unlike upstream, a run that fails after the task finished (a failing exit code or a throwing
// Finished listener) reports the task's own runtime, without the time its Finished listeners took.
$this->dispatcher->dispatch(new ScheduledTaskFailed(
$event,
$e,
round(microtime(true) - $start, 2)
$runtime ?? round(microtime(true) - $start, 2)
));
}

Expand Down
Loading
Loading